mirror of
https://github.com/block/buzz.git
synced 2026-10-31 22:41:12 +00:00
## Summary - ship **Buzz Term** end to end: the terminal engine/runtime, mounted desktop substrate, and user-visible naming - add Quinn's tape-deck-inspired banner: a beveled chassis filled by the `buzz term` wordmark, surrounded by a complete-hex field - derive the wordmark's three-stop sweep from each theme's terminal palette so primary, secondary, and accent roles remain visibly distinct across all 62 shipped themes, including light themes - paint the banner once on its own pointer-transparent canvas; PTY rendering beneath it remains unchanged ## Banner behavior - uses the renderer's shared `8.4 × 17` cell metrics and production aspect ratio `2.0238` - regenerates only for viewport/theme changes; palette switches repaint correctly while the banner is visible - dismisses on non-empty output from the active terminal session; empty output and inactive sessions do not dismiss it - fails closed below **70 columns** rather than squeezing or clipping the wordmark - adds **8 lines** to `terminalRenderer.ts` for shared cell metrics and **zero lines inside `paint()`** ## Screenshots | Buzz (light) | Buzz Dark | |---|---| |  |  | | Kanagawa Lotus (light) | Red | |---|---| |  |  | Additional production-aspect finals: [Vesper](https://buzz.block.builderlab.xyz/media/9ca6514b63f8cfb2107a85ca46f16a940c0883848e6fbc718e411af94aa13100.png), [Min Dark](https://buzz.block.builderlab.xyz/media/f67bd2970e5d64ffb07b1ae78ab58c847e6ebc23e7e7a48e067eb024dba64ec8.png), and [Dark Plus](https://buzz.block.builderlab.xyz/media/290fee08924f37d064abc687ecf3e9526ab05b87e8e56d610f23048949793dbe.png). The screenshot harness was checked against the shipped painter at this exact head: all **2,541 draw calls** matched on color, glyph, x, and y; four deliberate divergence controls fired. ## Verification at `98ebc8f9048bd5f0ceb7e843b67874d642f0b7fd` - desktop tests: **3,946 / 3,946** - TypeScript: clean - checks: pass (two pre-existing informational `useTemplate` notices only) - integration/e2e: PASS (independent exact-SHA lane; artifacts recorded in the originating Buzz thread) - artifact/dead-path sweep: clean - redteam G1–G7: PASS - all six named banner emitter-deletion mutants die - independent handwritten five-row full-wordmark fixture kills Quinn's seven-mutant battery, including a one-pixel glyph change - real `112 × 46` canvas-rect dismissal tests separately cover active non-empty, active empty, and inactive non-empty output - layer-drop and zero-draw painter mutants die; z-order and pointer-events verified - CI's `tsc && vite build` includes all three banner modules - performance at DPR 2 (worst-case measured envelope): - one-time content paint: **~0.7–0.8 ms**, paid only when the banner is built or its palette changes - busy compositor, CSS `1277 × 697`, backing `2554 × 1394`: **470–497 µs/frame** for the full banner (**2.82–2.98%** of a 60 Hz frame) - busy compositor, CSS `1920 × 1080`, backing `3840 × 2160`: **1,139–1,212 µs/frame** (**6.83–7.27%**) - empty, one-glyph, and full-banner controls converge: compositor cost follows backing-layer area and DPR rather than painted-cell count - in the actual idle welcome state, cost is below both vsync-clamped rigs' resolution; it is not claimed as zero - **Pane cross-rig spread: resolved at matched loop rate.** Two independent rigs initially differed 2.3× (58–68 vs 136 µs/Mpx of backing store; pane, CSS 1277×697 / backing 2554×1394, DPR 2). The cause of *that* spread is rAF loop rate: the higher figure came from a free-running loop at ~1600fps. Throttled to ~200–236fps, both rigs read 58–68 µs/Mpx (1.25–1.44% of a 60Hz frame). The busy-composite figures quoted above remain the **unthrottled worst case** and are conservative by ~2.3× at the pane. Not established: the mechanism and sign of free-running distortion (one rig under-charges ~15%, the other over-charges 2.3×), and the 1080p figure has not been re-measured throttled. - the layer paints only on generation/theme/resize and dismisses on first non-empty active-session output, so the measurable busy cost is a short-lived worst case rather than a persistent PTY paint-path tax ## Follow-ups in this PR These are intentionally subsequent commits after the certified static-banner head, not claims about `98ebc8f90`: 1. close the compositor metrology: remeasure the 1080p point throttled and characterize the opposite-sign free-running rAF distortion, with each measurement regime stated 2. add Tyler's animated honeycomb color waves, gated by `prefers-reduced-motion`, a full 62-theme phase-sweep contrast check, and DPR-2 per-tick performance certification 3. land the already-proven mounted theme-switch regression probe from `RESEARCH/BUZZ_TERM_G3A_PROBE/` 4. bound the slow/hang-shaped G1-c mutant `waitFor` 5. optionally trim the generator to its ink bounding box, reducing the minimum viewport from 70 to 62 columns --------- Signed-off-by: tlongwell-block <109685178+tlongwell-block@users.noreply.github.com> Signed-off-by: npub1mprnacetjua2xx3p5eddmhxyk6wv929ymm5py8kd2xfxurxahspqqlgyta <d8473ee32b973aa31a21a65adddcc4b69cc2a8a4dee8121ecd51926e0cddbc02@buzz.block.builderlab.xyz> Signed-off-by: npub1jh9wn95s0472h86ahapupaf7m6kx4v9sx2n0atj2hltcfer8k06s5n3pyf <95cae996907d7cab9f5dbf43c0f53edeac6ab0b032a6feae4abfd784e467b3f5@buzz.block.builderlab.xyz> Signed-off-by: npub17jjz49l9jjmhhk7cac63j8yt9z555n9cw8vk7v5jz4vzw4ppld5qgj57cc <f4a42a97e594b77bdbd8ee35191c8b28a94a4cb871d96f32921558275421fb68@buzz.block.builderlab.xyz> Signed-off-by: npub1qyvc0c5kl4gqv2fd97fsk46tu378sqgy35vc83rvgfwne90sel7s0ed67d <011987e296fd5006292d2f930b574be47c7801048d1983c46c425d3c95f0cffd@buzz.block.builderlab.xyz> Co-authored-by: Dawn (sprout agent) <c6237ef84fa537c78dcee78efd2d4e59f728859c7f194da42ac51ededfa0be05@sprout-oss.stage.blox.sqprod.co> Co-authored-by: npub1t2tgm7d8f995uqvmnm8h88sg3wnpp9a5xysjf6dg3tjmgt3ltulqdp8ehr <5a968df9a7494b4e019b9ecf739e088ba61097b4312124e9a88ae5b42e3f5f3e@buzz.block.builderlab.xyz> Co-authored-by: npub1cc3ha7z055mu0rwwu7806t2wt8mj3pvu0uv5mfp2c50dahaqhczshdalg6 <c6237ef84fa537c78dcee78efd2d4e59f728859c7f194da42ac51ededfa0be05@buzz.block.builderlab.xyz> Co-authored-by: npub17jjz49l9jjmhhk7cac63j8yt9z555n9cw8vk7v5jz4vzw4ppld5qgj57cc <f4a42a97e594b77bdbd8ee35191c8b28a94a4cb871d96f32921558275421fb68@buzz.block.builderlab.xyz> Co-authored-by: npub1jh9wn95s0472h86ahapupaf7m6kx4v9sx2n0atj2hltcfer8k06s5n3pyf <95cae996907d7cab9f5dbf43c0f53edeac6ab0b032a6feae4abfd784e467b3f5@buzz.block.builderlab.xyz> Co-authored-by: npub1mprnacetjua2xx3p5eddmhxyk6wv929ymm5py8kd2xfxurxahspqqlgyta <d8473ee32b973aa31a21a65adddcc4b69cc2a8a4dee8121ecd51926e0cddbc02@buzz.block.builderlab.xyz> Co-authored-by: npub1qyvc0c5kl4gqv2fd97fsk46tu378sqgy35vc83rvgfwne90sel7s0ed67d <011987e296fd5006292d2f930b574be47c7801048d1983c46c425d3c95f0cffd@buzz.block.builderlab.xyz>
139 lines
6.0 KiB
Rust
139 lines
6.0 KiB
Rust
//! Login-shell resolution for spawned PTY children.
|
|
//!
|
|
//! Tyler asked for the user's shell of choice, so the resolution order is the
|
|
//! user's own: `$SHELL`, then the passwd entry, then `/bin/sh`. What matters
|
|
//! is the *validity* test applied at each step, and it is not "the path
|
|
//! exists".
|
|
//!
|
|
//! `portable-pty` gates both steps on `access(X_OK)` (`cmdbuilder.rs:545-553`
|
|
//! for `$SHELL`, `:43-71` for passwd). `access(X_OK)` answers "may I execute
|
|
//! this" for *any* file type, and a directory carries the execute bit to mean
|
|
//! "may I traverse it" — so `access("/tmp", X_OK)` returns 0. Verified by C
|
|
//! repro and end-to-end through a real PTY: with `SHELL=/tmp`,
|
|
//! `CommandBuilder::get_shell()` returns `"/tmp"`, `spawn_command` returns
|
|
//! `Ok`, and the child dies with exit code 1 after printing
|
|
//! `fatal runtime error: assertion failed: output.write(&bytes).is_ok()`.
|
|
//! The user gets a terminal that opens and instantly dies with a Rust runtime
|
|
//! panic, and every layer above reported success.
|
|
//!
|
|
//! So we require an **executable regular file**, following symlinks: `stat`
|
|
//! rather than `lstat` semantics, because `/bin/sh` is legitimately a symlink
|
|
//! on many systems. A directory or a non-executable file falls through to the
|
|
//! next candidate instead of becoming an unspawnable child.
|
|
|
|
use std::path::Path;
|
|
|
|
/// Last-resort shell. POSIX guarantees `/bin/sh`; if this is not executable
|
|
/// the machine has bigger problems than our terminal.
|
|
pub const FALLBACK_SHELL: &str = "/bin/sh";
|
|
|
|
/// Returns true if `path` is a regular file this process may execute.
|
|
///
|
|
/// The conjunction is load-bearing and neither half suffices:
|
|
///
|
|
/// - `access(X_OK)` alone accepts a **directory** — the execute bit means
|
|
/// *traverse* there, so `access("/tmp", X_OK) == 0`. That is the bug
|
|
/// inherited from `portable-pty` (`cmdbuilder.rs:545-553`): with
|
|
/// `SHELL=/tmp` the child aborts with a Rust runtime panic while every
|
|
/// layer reports success.
|
|
/// - Raw `mode & 0o111` alone accepts a file the caller **cannot** execute.
|
|
/// The bits say *some* class has execute permission, not the applicable
|
|
/// one, and they do not evaluate ACLs. Verified with a self-owned regular
|
|
/// file at mode `0o010`: `mode & 0o111` is true, `access(X_OK)` is -1, and
|
|
/// running it gives `Permission denied`.
|
|
///
|
|
/// So: regular-file metadata (following symlinks, because `/bin/sh -> dash`
|
|
/// is legitimate) **and** effective executability via `access(X_OK)`.
|
|
#[cfg(unix)]
|
|
pub fn is_executable_file(path: &Path) -> bool {
|
|
let Ok(meta) = std::fs::metadata(path) else {
|
|
return false;
|
|
};
|
|
meta.is_file() && can_execute(path)
|
|
}
|
|
|
|
/// `access(path, X_OK)`: does the *effective* user have execute permission,
|
|
/// accounting for the applicable permission class and ACLs?
|
|
#[cfg(unix)]
|
|
fn can_execute(path: &Path) -> bool {
|
|
use std::os::unix::ffi::OsStrExt;
|
|
|
|
let Ok(c_path) = std::ffi::CString::new(path.as_os_str().as_bytes()) else {
|
|
return false; // interior NUL: not a path we can ask about
|
|
};
|
|
// SAFETY: `c_path` is a valid NUL-terminated C string for the duration of
|
|
// the call, and `access` only reads it.
|
|
unsafe { libc::access(c_path.as_ptr(), libc::X_OK) == 0 }
|
|
}
|
|
|
|
/// Resolves the shell to spawn: `$SHELL`, then the passwd entry, then
|
|
/// [`FALLBACK_SHELL`]. Each candidate must pass [`is_executable_file`].
|
|
///
|
|
/// `shell_env` is the caller's view of `$SHELL` so the resolution order is
|
|
/// testable without mutating process-global state; production passes
|
|
/// `std::env::var_os("SHELL")`.
|
|
#[cfg(unix)]
|
|
pub fn resolve_shell(shell_env: Option<&str>) -> String {
|
|
// One validation path for every candidate, deliberately. Validating each
|
|
// branch separately leaves the passwd branch's check untestable on any
|
|
// machine whose passwd shell happens to be valid — a mutant that deletes
|
|
// it survives because nothing can distinguish it. Sharing `validated`
|
|
// means the `$SHELL` arm's coverage is the passwd arm's coverage.
|
|
let candidates = [shell_env.map(str::to_owned), passwd_shell()];
|
|
candidates
|
|
.into_iter()
|
|
.flatten()
|
|
.find(|candidate| validated(candidate))
|
|
.unwrap_or_else(|| FALLBACK_SHELL.to_owned())
|
|
}
|
|
|
|
/// The single validity test every shell candidate must pass.
|
|
#[cfg(unix)]
|
|
fn validated(candidate: &str) -> bool {
|
|
is_executable_file(Path::new(candidate))
|
|
}
|
|
|
|
/// The current user's login shell from the passwd database, unvalidated:
|
|
/// `resolve_shell` applies the shared [`validated`] check to it.
|
|
///
|
|
/// This is the step that matters for a Finder- or launchd-started app, which
|
|
/// can have no `$SHELL` at all: without it we would hand a zsh user `/bin/sh`
|
|
/// and call it their shell of choice.
|
|
#[cfg(unix)]
|
|
pub(crate) fn passwd_shell() -> Option<String> {
|
|
// SAFETY: `getpwuid` returns a pointer to a static passwd struct owned by
|
|
// libc, valid until the next passwd-database call. We copy the string out
|
|
// before returning and make no other libc calls in between.
|
|
let shell = unsafe {
|
|
let ent = libc::getpwuid(libc::getuid());
|
|
if ent.is_null() {
|
|
return None;
|
|
}
|
|
let pw_shell = (*ent).pw_shell;
|
|
if pw_shell.is_null() {
|
|
return None;
|
|
}
|
|
std::ffi::CStr::from_ptr(pw_shell).to_str().ok()?.to_owned()
|
|
};
|
|
|
|
Some(shell)
|
|
}
|
|
|
|
/// The login-shell `argv[0]` convention: the shell's basename prefixed with
|
|
/// `-`. This is what tells any shell — zsh, bash, fish, tcsh, nu — to run as
|
|
/// a login shell, without sniffing its name or guessing its flag grammar.
|
|
///
|
|
/// `portable-pty` applies this itself for a default program
|
|
/// (`cmdbuilder.rs:510-517`); we compute it here so the contract is asserted
|
|
/// against a value we own rather than against the dependency's behaviour.
|
|
pub fn login_argv0(shell: &str) -> String {
|
|
let basename = shell.rsplit('/').next().unwrap_or(shell);
|
|
format!("-{basename}")
|
|
}
|
|
|
|
/// Resolve the command shell on Windows from `ComSpec`, falling back to cmd.
|
|
#[cfg(windows)]
|
|
pub fn resolve_shell(shell_env: Option<&str>) -> String {
|
|
shell_env.unwrap_or("cmd.exe").to_owned()
|
|
}
|