crates/gunnar-ui-web/src/pages/cli.rs
//! **Get the `gunnar` CLI** (E4, owner 2026-10-11): the signed binaries this
//! site's own release build carries, with their sha256 and signature and the
//! one line that checks both against the published root — and `cargo install`
//! as the second way. Everything shown comes from the front's
//! `/download/index.json`; every decision is in [`crate::cli`].
use leptos::prelude::*;
use crate::cli::{fetch_line, size, url, Binary, Manifest, Root, CARGO_INSTALL, MANIFEST};
use crate::pages::resolved;
use crate::ui::{Hint, PageHeader, Panel};
fn origin() -> String {
web_sys::window().and_then(|w| w.location().origin().ok()).unwrap_or_default()
}
#[component]
fn Line(#[prop(into)] text: String) -> impl IntoView {
view! { <pre class="mono cli-line"><code>{text}</code></pre> }
}
#[component]
fn Download(binary: Binary, root: Root) -> impl IntoView {
let o = origin();
let fetch = fetch_line(&o, &binary, &root);
view! {
<Panel title=binary.target.clone()>
<div class="panel-body">
<p>
<a class="btn" href=url(&o, &binary.file) download=binary.file.clone()>{format!("Download {}", binary.file)}</a>
" "{size(binary.bytes)}" · "
<a href=url(&o, &binary.signature)>"signature (.p7s)"</a>" · "
<a href=url(&o, &format!("{}.sha256", binary.file))>"sha256"</a>
</p>
<dl class="cli-sums">
<dt>"sha256"</dt><dd class="mono">{binary.sha256.clone()}</dd>
<dt>"blake3"</dt><dd class="mono">{binary.blake3.clone()}</dd>
</dl>
<Hint line="Fetch, then verify (sha256 and signature against the published root):"
more="The signature is a detached CMS over the file's BLAKE3 digest, made by the release build's \
signing certificate, which chains to the root below. The verify line needs openssl 3, b3sum and xxd."/>
<Line text=fetch/>
<Line text=binary.verify.clone()/>
<Line text=format!("chmod +x {0} && mv {0} ~/.local/bin/gunnar", binary.file)/>
</div>
</Panel>
}
}
/// `/cli`.
#[component]
pub fn Cli() -> impl IntoView {
let manifest = LocalResource::new(|| async { crate::api::get::<Manifest>(MANIFEST).await });
view! {
<PageHeader title="The Gunnar CLI"/>
<Hint line="Encrypted repositories: clone, unlock and seal them with it on your computer."/>
{move || match manifest.get() {
// No manifest here (a console with no front behind it): not a failure. Say so
// and offer crates.io; draw no download that is not there (GUN-FIELD-TEST).
Some(Err(_)) => view! {
<Panel title="From crates.io">
<div class="panel-body">
<p>{crate::cli::NO_DOWNLOADS}</p>
<Line text=CARGO_INSTALL.to_owned()/>
</div>
</Panel>
}.into_any(),
state => resolved(state, None, |m: Manifest| {
let cargo = if m.cargo_install.is_empty() { CARGO_INSTALL.to_owned() } else { m.cargo_install.clone() };
let root = m.root.clone();
view! {
<p class="meta">{format!("Release {}", m.version)}</p>
{m.absent.clone().map(|why| view! { <p class="fail">{why}</p> })}
{root.clone().map(|r| m.binaries.clone().into_iter().map(|b| view! { <Download binary=b root=r.clone()/> }).collect_view())}
{root.map(|r| view! {
<Panel title="Trust root">
<div class="panel-body">
<p><a href=url(&origin(), &r.file)>{r.file.clone()}</a></p>
<dl class="cli-sums"><dt>"sha256"</dt><dd class="mono">{r.sha256.clone()}</dd></dl>
</div>
</Panel>
})}
<Panel title="Or from crates.io">
<div class="panel-body"><Line text=cargo/></div>
</Panel>
}
}),
}}
}
}
/// **The hint beside a flow that needs the CLI**: its one line, and the link here.
#[component]
pub fn NeedsCli(needs: crate::cli::Needs) -> impl IntoView {
view! {
<p class="hint-line cli-hint">
{needs.line()}" "
<a href=crate::cli::PAGE>{crate::cli::LINK}</a>
</p>
}
}