diff --git a/assets/icons/circle-info.svg b/assets/icons/circle-info.svg new file mode 100644 index 00000000..ec30e25e --- /dev/null +++ b/assets/icons/circle-info.svg @@ -0,0 +1 @@ + diff --git a/assets/icons/copy.svg b/assets/icons/copy.svg new file mode 100644 index 00000000..55af1b1f --- /dev/null +++ b/assets/icons/copy.svg @@ -0,0 +1 @@ + diff --git a/assets/icons/ellipsis.svg b/assets/icons/ellipsis.svg new file mode 100644 index 00000000..2cbbc2be --- /dev/null +++ b/assets/icons/ellipsis.svg @@ -0,0 +1 @@ + diff --git a/assets/icons/eye.svg b/assets/icons/eye.svg new file mode 100644 index 00000000..6b0f0238 --- /dev/null +++ b/assets/icons/eye.svg @@ -0,0 +1 @@ + diff --git a/assets/icons/file.svg b/assets/icons/file.svg new file mode 100644 index 00000000..fab17eac --- /dev/null +++ b/assets/icons/file.svg @@ -0,0 +1 @@ + diff --git a/assets/icons/folder-closed.svg b/assets/icons/folder-closed.svg new file mode 100644 index 00000000..be3c5989 --- /dev/null +++ b/assets/icons/folder-closed.svg @@ -0,0 +1 @@ + diff --git a/assets/icons/folder-open.svg b/assets/icons/folder-open.svg new file mode 100644 index 00000000..2c0a9527 --- /dev/null +++ b/assets/icons/folder-open.svg @@ -0,0 +1 @@ + diff --git a/assets/icons/folder.svg b/assets/icons/folder.svg new file mode 100644 index 00000000..be3c5989 --- /dev/null +++ b/assets/icons/folder.svg @@ -0,0 +1 @@ + diff --git a/assets/icons/git-branch.svg b/assets/icons/git-branch.svg index b809d259..c85b0a16 100644 --- a/assets/icons/git-branch.svg +++ b/assets/icons/git-branch.svg @@ -1 +1 @@ - + diff --git a/assets/icons/info.svg b/assets/icons/info.svg index ec30e25e..2f5b329c 100644 --- a/assets/icons/info.svg +++ b/assets/icons/info.svg @@ -1 +1 @@ - + diff --git a/assets/icons/list.svg b/assets/icons/list.svg index 981d0c33..f840375b 100644 --- a/assets/icons/list.svg +++ b/assets/icons/list.svg @@ -1 +1 @@ - + diff --git a/assets/icons/plus.svg b/assets/icons/plus.svg new file mode 100644 index 00000000..b9457a10 --- /dev/null +++ b/assets/icons/plus.svg @@ -0,0 +1 @@ + diff --git a/assets/icons/search.svg b/assets/icons/search.svg new file mode 100644 index 00000000..4fcdc073 --- /dev/null +++ b/assets/icons/search.svg @@ -0,0 +1 @@ + diff --git a/src/ui/assets.rs b/src/ui/assets.rs index af8c1cd9..0122bb34 100644 --- a/src/ui/assets.rs +++ b/src/ui/assets.rs @@ -35,14 +35,28 @@ impl AssetSource for Assets { /// The bytes of a bundled agent brand mark, or `None` if `path` isn't one of /// ours. Kept as an explicit match (rather than `rust-embed`) because the set is /// tiny and fixed, and `include_bytes!` needs no extra build dependency. +/// +/// Note that matching on the *path* makes every arm here an app-wide override, +/// not a local one: `gpui_component_macros::icon_named!` derives `IconName` from +/// the downstream asset filenames, so `IconName::Search.path()` is literally +/// `"icons/search.svg"` and every `Icon::new(IconName::Search)` in the tree — +/// tty7's and gpui-component's own — resolves through the arm below. Adding a +/// name that upstream also ships redraws it everywhere; check the call sites +/// before doing so, and prefer a name upstream *doesn't* use (`circle-info`) +/// when only one place should change. fn agent_icon(path: &str) -> Option<&'static [u8]> { let bytes: &'static [u8] = match path { // Flush `>_` prompt glyph for the plain-shell tab avatar (Lucide's // unboxed `terminal`, which gpui-component doesn't bundle — it only // ships the boxed `square-terminal`). "icons/terminal.svg" => include_bytes!("../../assets/icons/terminal.svg"), - // Lucide's `git-branch`, for the sidebar row's branch line (gpui-component - // doesn't bundle a git glyph). + // A git glyph on the detail-panel spec below (gpui-component bundles + // none). Serves both the sidebar row's branch line and the Changes tab. + // Drawn as an actual commit graph — a trunk with a node at each end, + // branching once — rather than lucide's long arc slung between two + // floating rings: that one is the loosest, most lopsided shape in a row + // of four, and at the sidebar's 11px a stroked ring's hole collapses into + // a blur anyway. Nodes are filled here. "icons/git-branch.svg" => include_bytes!("../../assets/icons/git-branch.svg"), // Filled-panel dock glyphs for the window chrome: an outlined frame with // one inset solid block marking which dock is open (the "commercial-app" @@ -50,13 +64,62 @@ fn agent_icon(path: &str) -> Option<&'static [u8]> { // tty7 carries its own filled variants for the title bar controls. "icons/panel-left.svg" => include_bytes!("../../assets/icons/panel-left.svg"), "icons/panel-right.svg" => include_bytes!("../../assets/icons/panel-right.svg"), - // Lucide's `list`, for the detail panel's Outline tab: the recorded - // commands are a plain list, not a terminal (`square-terminal` read as - // "this is a shell", which every pane already is). + // The chrome glyphs that sit beside those dock tiles, redrawn to share + // their weight and their solid-mass vocabulary. Lucide's stock `plus` is + // a thin bare cross that reads far lighter than the framed dock icon + // next to it, and its `ellipsis` strokes three `r=1` circles — at 18px + // the cap overlaps its own fill and the dots blur into grey smudges. + // + // `plus` is the one deliberate departure from the spec below: a bare + // cross has no enclosing shape to carry weight, so it runs stroke 2.4 on + // a tighter 6→18 span. Matching the spec exactly would leave it visibly + // frailer than the framed tile beside it. + "icons/plus.svg" => include_bytes!("../../assets/icons/plus.svg"), + "icons/ellipsis.svg" => include_bytes!("../../assets/icons/ellipsis.svg"), + // The detail panel's own set: four tab tiles at 18px and four controls at + // 13px, all in one panel, so they're drawn to one spec instead of taken + // from lucide as-is. + // + // stroke 1.9 (except `plus` — see above) + // radius 2.6–3.2, never 2.0 + // span 3.2→20.8, both axes; circles widen to ~2.5→21.5, since a + // round shape reads smaller at equal geometry + // nodes always filled, r ≥ 1.35 — a stroked dot hazes below 16px + // + // The stock glyphs share none of that: they mix stroke weights, sit a + // 21-wide circle next to an 18-wide folder, and leave so much dead space + // inside the frame that the row reads as four glyphs from four sets. + // + // Shape choices worth keeping: `info` is a panel with two lines written + // in it — a picture of what the tab actually opens (cwd, shell, branch, + // changes) — rather than the circled `i`, which is the most-drawn icon + // there is and says "help" as readily as "details"; Outline's last row is + // cut short, because three full-width rules read as a hamburger menu + // rather than a list; + // `folder-closed` drops its lid rule, which at 18px only crowded the top + // of the box; `copy`'s back sheet wraps three sides and stops on its own + // curves instead of poking two raw stubs out of an L. "icons/list.svg" => include_bytes!("../../assets/icons/list.svg"), - // A refined `info` glyph for the Info tab: a solid dot over a round-capped - // stem in a slightly inset circle, cleaner than the stock hairline `i`. + "icons/folder-closed.svg" => include_bytes!("../../assets/icons/folder-closed.svg"), + "icons/folder-open.svg" => include_bytes!("../../assets/icons/folder-open.svg"), "icons/info.svg" => include_bytes!("../../assets/icons/info.svg"), + "icons/eye.svg" => include_bytes!("../../assets/icons/eye.svg"), + "icons/search.svg" => include_bytes!("../../assets/icons/search.svg"), + "icons/copy.svg" => include_bytes!("../../assets/icons/copy.svg"), + // `folder` and `file` carry no detail-panel role of their own — they're + // here because overriding `folder-open` above would otherwise split the + // file tree down the middle, drawing expanded rows on this spec and + // collapsed ones (and every file) from stock lucide. Same shape as + // `folder-closed`: upstream's only difference between the two is the lid + // rule this set drops anyway. + "icons/folder.svg" => include_bytes!("../../assets/icons/folder.svg"), + "icons/file.svg" => include_bytes!("../../assets/icons/file.svg"), + // The circled `i` that `info.svg` used to be, kept under its own name for + // the Settings nav's About row — there the glyph labels a section rather + // than a detail tab, and "panel with two lines written in it" says nothing + // about *About*. No upstream `IconName` maps here, so it's referenced by + // path (see `settings.rs`). + "icons/circle-info.svg" => include_bytes!("../../assets/icons/circle-info.svg"), "icons/agents/claude.svg" => include_bytes!("../../assets/icons/agents/claude.svg"), "icons/agents/codex.svg" => include_bytes!("../../assets/icons/agents/codex.svg"), "icons/agents/gemini.svg" => include_bytes!("../../assets/icons/agents/gemini.svg"), diff --git a/src/ui/settings.rs b/src/ui/settings.rs index abc9de61..8ad3ffee 100644 --- a/src/ui/settings.rs +++ b/src/ui/settings.rs @@ -684,7 +684,7 @@ impl Tty7App { // active it also carries a trailing `(N)` count of that section's matching // settings — the full section nav stays put and is annotated with // per-section hit counts, rather than collapsing into a flat result list. - let nav_item = |label: &'static str, target: SettingsSection, icon: IconName| { + let nav_item = |label: &'static str, target: SettingsSection, icon: Icon| { let view = cx.entity(); let count = if query.is_empty() { 0 @@ -692,7 +692,7 @@ impl Tty7App { section_match_count(target, &query) }; let item = SidebarMenuItem::new(label) - .icon(Icon::new(icon)) + .icon(icon) .active(section == target) .on_click(move |_, _window, cx| { view.update(cx, |this, cx| this.select_settings_section(target, cx)); @@ -715,7 +715,7 @@ impl Tty7App { .child(nav_item( "Appearance", SettingsSection::Appearance, - IconName::Palette, + Icon::new(IconName::Palette), )) // Sliders for Terminal (it's the tuning page), the `>_` // prompt glyph for Shell (it configures the prompt's @@ -723,28 +723,43 @@ impl Tty7App { .child(nav_item( "Terminal", SettingsSection::Terminal, - IconName::Settings2, + Icon::new(IconName::Settings2), )) .child(nav_item( "Shell", SettingsSection::Shell, - IconName::SquareTerminal, + Icon::new(IconName::SquareTerminal), + )) + .child(nav_item( + "SSH", + SettingsSection::Ssh, + Icon::new(IconName::Globe), + )) + .child(nav_item( + "Agents", + SettingsSection::Agents, + Icon::new(IconName::Bot), )) - .child(nav_item("SSH", SettingsSection::Ssh, IconName::Globe)) - .child(nav_item("Agents", SettingsSection::Agents, IconName::Bot)) .child(nav_item( "Window & Tabs", SettingsSection::WindowTabs, - IconName::WindowRestore, + Icon::new(IconName::WindowRestore), )) // The icon set ships no keyboard glyph; CaseSensitive ("Aa") // is the closest key-ish cue available. .child(nav_item( "Keybindings", SettingsSection::Keybindings, - IconName::CaseSensitive, + Icon::new(IconName::CaseSensitive), )) - .child(nav_item("About", SettingsSection::About, IconName::Info)); + // Not `IconName::Info`: `icons/info.svg` is overridden app-wide with + // the detail panel's "panel with two lines" glyph, which reads as a + // document, not as *About*. This row keeps the circled `i`. + .child(nav_item( + "About", + SettingsSection::About, + Icon::empty().path("icons/circle-info.svg"), + )); let sidebar = Sidebar::new("settings-sidebar") .collapsible(SidebarCollapsible::None)