Skip to content

feat(tui): add optional Nerd Font icons - #313

Closed
frigidplatypus wants to merge 9 commits into
outlmd:mainfrom
frigidplatypus:feat/tui-icons-emoji-default
Closed

frigidplatypus wants to merge 9 commits into
outlmd:mainfrom
frigidplatypus:feat/tui-icons-emoji-default

Conversation

@frigidplatypus

@frigidplatypus frigidplatypus commented Sep 17, 2026 •

Copy link
Copy Markdown
Contributor

Problem

The TUI hardcodes emoji for its chrome icons. Emoji rendering varies by terminal and font, including color, width, and visual style, and some terminal users do not want emoji in a terminal UI.

There is currently no supported way to use a compact monochrome icon set instead.

Why Now

The TUI already has a broad icon surface across the header, sidebar, overlays, warnings, reminders, and placeholders. Adding the preference now prevents more icon usage from becoming hardcoded and inconsistent.

Solution

  • Keep emoji as the default for compatibility with ordinary terminal fonts.
  • Add [tui] icons = "nerd-font" as an opt-in.
  • Route all TUI-owned icons through the selected icon set.
  • Document and test both modes.

This is a user-facing configuration improvement, not an exploratory RFC.

Verification

  • cargo fmt --all
  • cargo test -p outl-config -p outl-tui
  • cargo clippy -p outl-config -p outl-tui --all-targets -- -D warnings
  • RUSTDOCFLAGS="-D warnings" cargo doc -p outl-config -p outl-tui --no-deps
  • scripts/check-file-size.sh

Review follow-up (dfc861f, c07e79f)

All three Copilot findings are addressed:

  1. Desktop settings wiped [tui] (dfc861f): restore_unmodeled_sections now copies on_disk.tui back, so a settings-modal save can no longer silently rewrite icons = "nerd-font" (or mouse_capture) to defaults. Pinned by save_restores_the_tui_section_the_desktop_never_models.
  2. auto-run / run hardcoded ▶ (c07e79f): IconSet gains a play field (nf-fa-play U+F04B for Nerd Font, ▶ unchanged for Emoji); property_glyph("auto-run") and command_glyph("run") route through it. Pinned by play_routes_through_the_icon_set.
  3. Icon style applied after the first load (c07e79f): the icon style is now an App::new parameter, so the parse-warning status chip is stamped with the configured set from the very first load_current() and stays clearable by later reloads. Pinned by boot_stamps_the_warning_chip_with_the_configured_icon_set.

The param addition pushed five test files over the file-size ratchet line, so 63195d2 keeps them under it: tests construct through App::new_for_tests (same five-arg shape as before, emoji set pinned behind it), and the boot orchestrator calls the real constructor.

Review follow-up 2 (02ddb7b) — suppressed comments

  • Mixed icon style with icons = "nerd-font": fair — ☑ ● ⟳ ◌ ⇇ (header/footer chips) and ▼/▶ (fold markers + help legend) were still literals. They are IconSet roles now; the markers render through IconSet::fold_span (glyph + colour together). The emoji set keeps the exact pre-PR glyphs, and docs/tui.md now names what deliberately stays Unicode in both styles: task checkboxes (☐/◐/☑ mirror document state), calendar day dots, and scrollbar symbols.
  • iso* commands rendered # instead of 🔢 in emoji mode: regression caught, hashtag is 🔢 for Emoji and \u{f292} only for Nerd Font. Pinned by emoji_preserves_the_pre_iconset_glyphs.

Note on the macOS CI failure

the_cycle_dense_generator_rejects_far_more_moves_than_the_shared_one (outl-core/tests/convergence_property.rs, pre-existing on main) is a statistical pin on a randomly-seeded proptest — it draws a fresh sample each run and has a narrow floor (≥180/200 rejections), so it flakes on unlucky seeds independent of any PR. This branch touches no outl-core code; re-running the job passes.

@avelino

avelino commented Sep 17, 2026

Copy link
Copy Markdown
Collaborator

@frigidplatypus saw that the CI is breaking, can you fix it?

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

Three moderate findings and one documentation nit remain unresolved.

Get a fresh assessment by requesting another Copilot review.

Pull request overview

Adds optional Nerd Font icons to the TUI while retaining emoji defaults.

Changes:

  • Adds [tui].icons configuration.
  • Routes TUI rendering through the selected icon set.
  • Adds documentation and configuration/icon-selection tests.
File summaries
File Summary
docs/tui.md Documents icon configuration. Nit (2 votes): add an issue link or concrete problem statement.
docs/config.md Adds the icon configuration reference.
crates/outl-tui/src/view/warnings_banner.rs Uses the selected warning icon.
crates/outl-tui/src/view/toasts.rs Uses selected toast icons.
crates/outl-tui/src/view/sidebar.rs Uses selected sidebar icons.
crates/outl-tui/src/view/overlays.rs Uses selected overlay icons.
crates/outl-tui/src/view/outline.rs Routes property and inline rendering through icons.
crates/outl-tui/src/view/namespace.rs Uses the selected file icon.
crates/outl-tui/src/view/inline.rs Adds icon-aware inline rendering.
crates/outl-tui/src/view/chrome.rs Uses selected chrome icons and dynamic widths.
crates/outl-tui/src/view/backlinks.rs Uses the selected fallback file icon.
crates/outl-tui/src/state.rs Stores the icon set in application state.
crates/outl-tui/src/runtime.rs Loads icon configuration. Moderate (1 vote): selection occurs after the initial load, leaving startup warning chips inconsistent.
crates/outl-tui/src/lib.rs Registers the icon module.
crates/outl-tui/src/icons.rs Defines emoji and Nerd Font sets. Moderate (1 vote): auto-run remains hard-coded to ▶.
crates/outl-tui/src/app.rs Updates rendering test calls.
crates/outl-tui/src/actions/reminders.rs Uses the selected reminder icon.
crates/outl-tui/src/actions/lifecycle/mod.rs Initializes default icons.
crates/outl-tui/src/actions/lifecycle/loading.rs Uses the selected warning marker.
crates/outl-config/src/tui.rs Defines icon configuration and tests. Moderate (1 vote): desktop settings saves can reset the persisted icon preference.
crates/outl-config/src/schema.rs Moves TUI configuration definitions.
crates/outl-config/src/lib.rs Exports the new configuration types.
crates/outl-config/CLAUDE.md Documents the configuration setting.
Review details

Suppressed comments (3)

crates/outl-config/src/tui.rs:11

  • This new option is not preserved by the desktop settings writer: Settings::save constructs Config with TuiCfg::default() and restore_unmodeled_sections currently never copies on_disk.tui back (see crates/outl-desktop/src-tauri/src/settings.rs:193,249-275). Editing any desktop setting therefore silently rewrites [tui].icons = "nerd-font" to emoji. Restore the whole TuiCfg in that adapter before shipping the new preference.
    pub icons: TuiIconStyle,

crates/outl-tui/src/icons.rs:40

  • The auto-run property is still hard-coded to ▶, so selecting icons = "nerd-font" does not change this property icon even though render_block now routes property rendering through IconSet::property_glyph. Give it a style-specific play glyph (preserving ▶ for Emoji) instead of returning a literal.
            "auto-run" => Some("▶"),

crates/outl-tui/src/runtime.rs:422

  • App::new() has already called load_current() at actions/lifecycle/mod.rs:147, and that first load formats the parse-warning status chip with the default emoji icon. Replacing app.icons here leaves a Nerd Font launch with an emoji chip; later reloads no longer recognize or clear it because loading.rs matches the current warning glyph. Initialize the IconSet before the first load (for example, pass the style into App::new) and cover the startup-warning case.
    app.icons = crate::icons::IconSet::new(icon_style);
  • Files reviewed: 23/23 changed files
  • Comments generated: 1
  • Review effort level: Lite

💡 Configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread docs/tui.md
@frigidplatypus

Copy link
Copy Markdown
Contributor Author

I'm unable to get the Mac ci to pass, although it seems unrelated to my changes.

into() hardcodes TuiCfg::default() (icons = emoji), and
restore_unmodeled_sections never carried [tui] back from disk, so every
settings save silently rewrote a hand-set icons = "nerd-font" or
mouse_capture = true. The code comment claimed the restore existed;
the pin test makes the claim true.
Two review findings on the icon opt-in:

- property_glyph("auto-run") and command_glyph("run") hardcoded "▶"
  outside the IconSet, so a nerd-font terminal still drew the emoji
  play glyph. IconSet gains a play field (nf-fa-play for Nerd Font).

- App::new runs the first load_current(), which stamps the
  parse-warning status chip with icons.warning; the runtime only
  assigned the configured IconSet afterwards, so a nerd-font launch
  booted with an emoji chip that no later reload could clear (the
  clear path only recognises its own marker). Icon style is now an
  App::new parameter, applied before the first load.
@frigidplatypus

Copy link
Copy Markdown
Contributor Author

Pushed dfc861f + c07e79f addressing all three review findings (details in the updated description).

On CI: the macOS failure is the pre-existing flaky statistical pin the_cycle_dense_generator_rejects_far_more_moves_than_the_shared_one in outl-core/tests/convergence_property.rs — a randomly-seeded proptest with a narrow rejection floor that flakes on unlucky seeds on main independent of this PR. This branch changes no outl-core code; a re-run of the macos job should go green. Happy to file an upstream issue to widen the floor / seed it deterministically if you want that tracked.

Adding App::new's icon_style param pushed every test call site over its
ratchet line (+1 across five files, +6 in runtime.rs where the widened
call went vertical). Tests now go through App::new_for_tests, which pins
the emoji set behind the same five-arg shape they had before; the boot
orchestrator calls the real constructor and its arg list fits the
100-column single line by naming the root param root.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 Needs a closer look

Two unresolved moderate icon-routing and default-rendering issues remain.

Review details

Suppressed comments (2)

crates/outl-tui/src/icons.rs:14

  • This catalog only covers the emoji/placeholder subset, but the TUI's own chrome still emits other icon glyphs directly: view::chrome uses ☑, ●, ⟳, ◌, and ⇇, while view::outline and the help text use ▼/▶. With [tui] icons = "nerd-font", users therefore get a mixed icon style instead of the advertised routing of all TUI-owned icons through the selected set. Add roles for these glyphs and route their renderers through IconSet, or narrow the feature/documentation to the subset it actually controls.
pub(crate) struct IconSet {
    pub(crate) calendar: &'static str,
    pub(crate) file: &'static str,
    pub(crate) image: &'static str,
    pub(crate) clock: &'static str,

crates/outl-tui/src/icons.rs:91

  • The default emoji mode no longer preserves the existing iso* command glyph: command_icon previously returned 🔢, but IconSet::emoji now returns #. This changes the default rendering of /iso-date and related commands despite the PR promising emoji compatibility; keep the emoji value here and reserve \u{f292} for the Nerd Font set.
            hashtag: "#",
  • Files reviewed: 39/39 changed files
  • Comments generated: 0 new
  • Review effort level: Lite

The nerd-font set only covered property/command glyphs, so a Nerd Font
user got ☑ ● ⟳ ◌ ⇇ and ▼/▶ chrome next to PUA chevrons — a mixed style
from the option that promises one coherent set. The chips, the fold
markers (now a fold_span accessor sharing glyph + colour) and the help
legend all read from the selected set; the emoji set keeps the exact
pre-PR glyphs.

Also fixes the emoji default: iso* commands rendered # where they used
to render 🔢. The hashtag role keeps 🔢 for emoji and \u{f292} for
Nerd Font. docs/tui.md now names what stays Unicode in both styles
(task checkboxes, calendar dots, scrollbar symbols).
@frigidplatypus

Copy link
Copy Markdown
Contributor Author

Both suppressed comments are addressed in 02ddb7b.

Chrome glyphs under nerd-font: the header/footer chips (☑ ● ⟳ ◌ ⇇) and the fold markers (▼/▶, plus the help legend that names them) now read from the selected set; the fold marker renders through IconSet::fold_span, which pairs the glyph with its colour so the two can't drift. Emoji mode is byte-identical to the old renderer (pinned by emoji_preserves_the_pre_iconset_glyphs). What stays Unicode in both styles is now stated in docs/tui.md: task checkboxes mirror the document's own TODO/DOING/DONE state, and calendar dots / scrollbar symbols are geometry, not icons — routing those through the set would be style for style's sake.

# vs 🔢 on iso* commands: real regression in the emoji default, fixed — hashtag is 🔢 for Emoji, \u{f292} only for Nerd Font.

@avelino avelino added kind:feature New capability / surface area:tui outl-tui: terminal UI area:config outl-config: config.toml schema, parsing, defaults area:docs docs/ and CLAUDE.md files area:desktop outl-desktop: Tauri 2 macOS/Linux/Windows client priority:P3 Nice-to-have, when time permits labels Sep 18, 2026

@avelino avelino left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the shape is right: opt-in, emoji stays the default, docs updated, and right_segments_width stopped being a hardcoded 34 and started measuring with unicode_width. what blocks it is transcription.

the emoji set is not a copy of the literals it deleted. five glyphs change for people who never touched the config, and the test named emoji_preserves_the_pre_iconset_glyphs checks four fields, which happen to be the four that did not regress. it is the same class as the 🔢 you already fixed in the previous round, the fix just did not generalise to the rest of the table.

one more thing missing: a CHANGELOG.md entry under ## [Unreleased] / ### Added. every recent feat: has one, and the closest precedent is [tui] mouse_capture, which got its own.

Comment thread crates/outl-tui/src/icons.rs Outdated
star: "⭐",
history: "🕘",
bolt: "⚡",
search: "🔍",

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this drops 🔎. pre-PR overlays.rs used 🔎 (U+1F50E) for both the Search category and the search / find commands. self.search is 🔍 (U+1F50D), a different codepoint.

two more in the same table, both from routing to an existing field: command_glyph line 106 sends week* to self.calendar (was 📆), line 107 sends stamp to self.clock (was 🕒).

i grepped the workspace: 🔎, 📆 and 🕒 existed nowhere else, so they leave the repo entirely. the PR body says the emoji set keeps the exact pre-PR glyphs, and emoji is the default, so this lands on everyone.

Suggested change
search: "🔍",
search: "🔎",

week and stamp need their own fields, or an explicit note saying the collision is intended.

Comment thread crates/outl-tui/src/icons.rs Outdated
warning: "⚠",
save: "💾",
clipboard: "📋",
moon: "🌙",

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💤 (overlays.rs:668, the snoozed-reminder chip) became 🌙. this one is not just a different codepoint, it reads differently: 💤 means snoozed, 🌙 reads as night mode.

Suggested change
moon: "🌙",
moon: "💤",

}

#[test]
fn emoji_preserves_the_pre_iconset_glyphs() {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the name says it preserves the pre-IconSet glyphs. the body checks four fields, and those four are exactly the ones that did not regress, so it goes green while the contract it names is broken. that is how the five above survived three review rounds.

assert every field of emoji() against the literal table you deleted, not a sample. this is a constant-extraction refactor: exhaustive is cheap here, and it is the only thing that catches a transcription typo.

// model it. Restore it so a modal save can't wipe a hand-set icon
// style or mouse-capture toggle (the `into()` conversion leaves
// `TuiCfg::default()`, and a default here silently means "emoji").
cfg.tui = on_disk.tui.clone();

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

real fix, thanks. [snapshot] and [storage] have the identical bug though, and the comments at lines 194 and 197 already claim save restores them from disk.

Config has 12 sections, this function restores 6, Settings models 5. a hand-set op_threshold or lru_cap gets reset to the default on every modal save, silently, same as icons did.

Suggested change
cfg.tui = on_disk.tui.clone();
cfg.tui = on_disk.tui.clone();
cfg.snapshot = on_disk.snapshot.clone();
cfg.storage = on_disk.storage.clone();

and extend save_restores_the_tui_section_the_desktop_never_models to cover them.

ToastKind::Success => ("✓", Color::LightGreen),
ToastKind::Info => ("ℹ", Color::LightCyan),
ToastKind::Warning => ("⚠", Color::LightYellow),
ToastKind::Warning => (icons.warning, Color::LightYellow),

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

one of four arms routed. Success (✓), Info (ℹ) and Error (✕) stay literal right above and below, so with icons = "nerd-font" the same widget paints a PUA glyph on a warning toast and unicode on the next one.

either all four go through the IconSet, or warning comes back out and the toast stays unicode. the exception list in the icons.rs module doc names ✕ but not ✓ or ℹ, so as it stands the list and the code disagree whichever way you read it.

Comment thread crates/outl-tui/src/runtime.rs Outdated
frigidplatypus and others added 3 commits September 18, 2026 12:01
The snoozed-reminder chip rendered 🌙; upstream draws 💤 for it, so the
Emoji set now matches (field renamed moon -> snooze to match upstream's
own `snoozed` vocabulary, nerd variant nf-fa-bell-slash-o).

All four toast accents (success/info/warning/error) route through
IconSet now — only warning was, so Nerd Font mode still leaked three
colour emoji. Each value matches upstream's toasts.rs literal byte for
byte.

Restores the issue outlmd#142 backlinks rationale comment and the
workspace_root param name in runtime.rs (unrelated churn from the
icon-style plumbing); the ratchet ceiling rises accordingly.
Saving the settings modal rebuilds `Config` from the flat wire shape and
restored only some of the sections it does not model. `[snapshot]` and
`[storage]` were missing, so a save silently reset the boot-cache policy
and the op-log LRU cap to defaults — the same class of loss the `[tui]`
and `[backup]` restores exist to prevent.

Restore both from disk and cover them in the renamed
save_restores_the_sections_the_desktop_never_models test.
@avelino

avelino commented Sep 21, 2026

Copy link
Copy Markdown
Collaborator

@copilot resolve the merge conflicts in this pull request

@avelino

avelino commented Sep 21, 2026

Copy link
Copy Markdown
Collaborator

Merge done, thanks for the contribution!

@avelino avelino closed this Sep 21, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area:config outl-config: config.toml schema, parsing, defaults area:desktop outl-desktop: Tauri 2 macOS/Linux/Windows client area:docs docs/ and CLAUDE.md files area:tui outl-tui: terminal UI kind:feature New capability / surface priority:P3 Nice-to-have, when time permits

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants