Skip to content

Width-aware rendering: the banner now fits any terminal#3

Merged
veteranbv merged 12 commits into
mainfrom
feat/width-aware-rendering
Jul 14, 2026
Merged

Width-aware rendering: the banner now fits any terminal#3
veteranbv merged 12 commits into
mainfrom
feat/width-aware-rendering

Conversation

@veteranbv

@veteranbv veteranbv commented Jul 13, 2026

Copy link
Copy Markdown
Owner

The banner has always rendered at full FIGlet width and let the terminal wrap it, which turns narrow windows — split tmux panes, phone SSH sessions — into garbled block characters. This PR makes sysgreet measure the terminal before printing and degrade gracefully instead.

Width ladder

When the art would overflow, sysgreet steps down in order:

  1. Full hostname in the configured font
  2. Hostname without its domain (pve1.home.lanPVE1; the full name moves to an info line)
  3. Progressively narrower fonts (standard, then the newly embedded Small)
  4. A single ruled header — ═════ PVE1 ═════ — that fits any width

Body lines that would still overflow are clipped with an ellipsis. Width comes from the TTY, falling back to COLUMNS, and can be capped with layout.max_width or previewed with --width.

Color handling fixes

New internal/terminal package owns the ANSI palette (previously duplicated and drifted between ascii and render) and detects capability once at startup:

  • NO_COLOR now covers the whole banner — the hostname gradient used to keep its escapes
  • Piped output is always plain text; TERM=dumb honored
  • Windows legacy consoles get virtual terminal processing enabled, or plain text if they refuse, instead of raw ←[94m
  • Truecolor terminals (COLORTERM=truecolor) get a smooth 24-bit gradient interpolated from the configured stops

Other fixes

  • --text and --demo now honor the loaded config (both silently ignored it)
  • Compact mode emits an actual single line instead of embedding the multi-line art
  • Unknown font names fall back to the default deterministically (logged under SYSGREET_DEBUG) instead of picking a random font every login
  • Collectors run concurrently under a shared 250 ms deadline — a stuck metric source drops its section instead of hanging the login, and Windows no longer pays its 100 ms CPU sample serially
  • golangci-lint config migrated to v2 and the CI action pinned; version: latest had started resolving to v2, which rejects the old config outright, so lint was silently broken
  • .gitignore's sysgreet entry anchored to the repo root — it was ignoring new files under cmd/sysgreet/

New surface

  • Flags: --json (structured output for scripting, never prompts), --font, --width, --no-color, --config, --list-fonts
  • Config: layout.max_width / SYSGREET_LAYOUT_MAX_WIDTH

Validation

  • ~40 new tests: width ladder, terminal detection, clipping, compact mode, JSON, collector concurrency (race-detector clean), and the new flags exercised through the built binary
  • Verified line width never exceeds the terminal at widths 25–200
  • Startup benchmark unchanged (~0.2 ms with stub collectors); golangci-lint clean; cross-compiles for windows/amd64 and darwin/arm64
  • golang.org/x/term pinned at v0.20.0 to keep the Go 1.22 floor

Review follow-up in 4a74b74: compact-mode output is clipped to width, --text content is never rewritten at dots (domain shortening is hostname-only now), layout.max_width caps the whole banner rather than just the header, and ascii.monochrome suppresses threshold highlights again.

The banner previously rendered at full FIGlet width and let the terminal
wrap it, which turned narrow windows (tmux panes, phone SSH sessions)
into garbled block characters. Sysgreet now detects the terminal width
and walks a fallback ladder before printing: full hostname in the
configured font, hostname without its domain (full name moves to an
info line), progressively narrower fonts ending with the newly embedded
Small font, and finally a single ruled header line that fits any width.
Body lines that still overflow are clipped with an ellipsis.

Color handling moves into a new internal/terminal package that owns the
ANSI palette (previously duplicated and drifted between ascii and
render) and detects capability once at startup. This fixes NO_COLOR
only applying to resource highlights while the banner gradient kept its
escapes, stops ANSI codes leaking into piped output, honors TERM=dumb,
and enables virtual terminal processing on Windows so legacy consoles
get plain text instead of raw escapes. Truecolor terminals now get a
smooth 24-bit gradient interpolated from the configured stops.

Also fixed along the way: --text and --demo previously ignored the
loaded config entirely; compact mode embedded the multi-line art in its
"single line" output; and a typo in ascii.font silently picked a random
font every login instead of falling back to the default.

New: --json for scripting (structured sections with raw percentages,
never prompts), --font, --width, --no-color, --config, --list-fonts,
and layout.max_width / SYSGREET_LAYOUT_MAX_WIDTH to cap the width.
Collectors previously ran one after another, so on Windows the 100 ms
CPU sample alone blew the 50 ms startup budget, and any single slow
metric source could hang the login shell indefinitely. Each collector
now gathers in its own goroutine (they write disjoint Snapshot fields)
and shares a 250 ms deadline; a collector that misses it just drops its
section, which the banner already degrades around.
The lint job pinned golangci-lint-action@v6 with version: latest, which
now resolves to golangci-lint v2 and rejects the v1-format .golangci.yml
outright. Migrated the config with `golangci-lint migrate` and pinned
the action (v8) and linter (v2.5.0) so lint results stop depending on
whatever "latest" means this week.
README highlights the width degradation ladder and script-friendly
output, documents the new flags (--json, --font, --width, --no-color,
--config, --list-fonts) and layout.max_width. CHANGELOG gains a v1.1.0
entry. CONTRIBUTING notes the new internal/terminal package plus the
output rules (width and color must flow through it), and the examples
show narrow-terminal degradation and jq-based scripting.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 174b7ef797

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread cmd/sysgreet/main.go
Comment thread internal/render/layout.go
Comment thread internal/ascii/fonts.go Outdated
Comment thread internal/render/layout.go Outdated
Four follow-ups to the width-aware rendering work:

- Compact mode output is now clipped to the terminal width like every
  other line; it previously returned before the clipping path ran.
- Domain shortening (pve1.home.lan -> PVE1) is now opt-in via
  RenderOptions.ShortenDomain and only the hostname header sets it, so
  --text "v2.1.0" can shrink or clip but never silently loses
  everything after the first dot.
- layout.max_width now caps the whole banner, not just the header art:
  the config folds into the terminal env before the layout renderer is
  built (render.ApplyConfig), with --width still winning over both.
- ascii.monochrome once again suppresses the resource threshold
  highlights; keying the colorizer off the terminal profile alone had
  regressed that.
@veteranbv

Copy link
Copy Markdown
Owner Author

@codex review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 4a74b74264

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread internal/collectors/system.go Outdated
Comment thread internal/banner/banner.go Outdated
Comment thread internal/render/layout.go Outdated
Comment thread cmd/sysgreet/main.go
- Gather no longer waits past its deadline: collectors send apply
  functions over a channel and the loop selects against ctx.Done, so a
  collector stuck in a blocking syscall (ignoring cancellation) is
  abandoned at 250 ms with the finished collectors' results kept. The
  snapshot is only touched from the calling goroutine, so stragglers
  cannot race the caller.
- env.Width is now the single width authority: the banner header uses
  it directly instead of re-applying layout.max_width, so --width 100
  with max_width 40 renders one consistent width instead of a 40-column
  header over 100-column body lines.
- Section titles are clipped like body lines; --width 8 no longer leaks
  a 9-column "Resources" heading.
- An explicit config path (--config / SYSGREET_CONFIG) is exclusive:
  when the file is missing, sysgreet uses built-in defaults instead of
  silently falling back to a different config file on the host.
@veteranbv

Copy link
Copy Markdown
Owner Author

@codex

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: e621775bb7

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread internal/render/json.go
The width ladder's hostname fallback line was leaking into the JSON
header: at narrow widths buildHeader shortens the art and prepends the
full hostname to Header.Lines, so header[0] flipped between the OS line
and the hostname depending on terminal size. JSON mode now builds
against a neutral environment, so scripted output is stable regardless
of where it runs.
@veteranbv

Copy link
Copy Markdown
Owner Author

@codex review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 76f5814302

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread internal/render/layout.go Outdated
Comment thread internal/render/layout.go
Comment thread internal/collectors/system.go
When the width ladder shortens a dotted hostname, buildHeader adds the
full name as an info line for the art's benefit. Compact mode already
leads with the full hostname, so that line rendered it twice
(PVE1.HOME.LAN | pve1.home.lan | ...). Compact output now skips it.
Two edges in the width guarantee: a timed-out system collector left a
blank OS header line (and an empty JSON header element), and widths of
one or two columns overflowed because the fixed two-space body indent
was prepended after clipping. Blank OS lines are now omitted and the
indent is dropped when the width cap cannot fit it.
@veteranbv

Copy link
Copy Markdown
Owner Author

@codex review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 8a4a910e01

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread internal/collectors/system.go
Comment thread internal/render/layout.go Outdated
Two more review findings. Clipping counted runes, so double-width
characters (a CJK username or home path) could pass the check while
overflowing the terminal on screen; the terminal package now owns
RuneWidth/DisplayWidth/Clip covering the East Asian wide and emoji
ranges, and the render and ascii layers measure with them. And when the
system collector misses the gather deadline, the banner rendered
Time: Mon, 01 Jan 0001 00:00:00 UTC and User: unknown from the
zero-valued snapshot; those lines are now omitted when the data is
absent, matching how every other missing metric degrades.
@veteranbv

Copy link
Copy Markdown
Owner Author

@codex review

go-figure's strict mode kills the process with log.Fatal on non-ASCII
input, so --text "日本語" died before printing anything; non-strict mode
instead drops uncovered characters, which silently mangles mixed text
like web-サーバ. Rendering is now non-strict as a backstop, and any text
outside printable ASCII skips the FIGlet ladder entirely in favor of
the plain ruled header, which preserves it verbatim and clips by
display columns.
@veteranbv

Copy link
Copy Markdown
Owner Author

@codex review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: f830a532da

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread internal/ascii/fonts.go Outdated
Comment thread cmd/sysgreet/main.go
The plain-header fallback capped its rule at 60 columns even when the
terminal was wider, clipping a 70-column name that a 100-column pane
could show in full; 60 is now just the preferred rule length and long
names widen the rule up to the available width instead of losing
characters. And bootstrapping --config /path/config.toml wrote YAML
into the .toml file, which the loader then failed to parse; the default
config is now marshaled in the format the target extension implies.
@veteranbv

Copy link
Copy Markdown
Owner Author

@codex review

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. More of your lovely PRs please.

Reviewed commit: 22fa00de53

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

@veteranbv
veteranbv merged commit fe19579 into main Jul 14, 2026
4 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant