Skip to content

feat: make help width configurable instead of hardcoded 80 - #758

Closed
maxandersen wants to merge 3 commits into
aeshell:masterfrom
maxandersen:responsive-help-width
Closed

maxandersen wants to merge 3 commits into
aeshell:masterfrom
maxandersen:responsive-help-width

Conversation

@maxandersen

@maxandersen maxandersen commented Nov 7, 2026 •

Copy link
Copy Markdown
Contributor

What

Help text was always wrapped to the hardcoded ProcessedCommand.HELP_WIDTH (80), regardless of terminal size or user preference. This makes the width resolvable — including from the real terminal — while staying safe where there is no terminal.

Resolution order

ProcessedCommand.helpWidth() resolves, at render time:

  1. aesh.help.width system property
  2. COLUMNS environment variable
  3. the live terminal width, via an opt-in supplier (see below)
  4. the HELP_WIDTH default (80)

Any value is clamped to a 40-column minimum; non-numeric overrides are ignored.

Querying the terminal: lazy, opt-in, and from the controlling tty

Two design points, both driven by real constraints:

1. The query must be lazy and opt-in — only when usage actually prints.
printHelp() also runs where there is no terminal (documentation generation, programmatic/LazyHelp rendering). Reading the terminal unconditionally would fail there, or — with a lazy shell — force a terminal connection the caller never wanted, as a side effect of building a string. So the width comes from an optional IntSupplier that a caller with a terminal installs (getHelpInfo), consulted here and only here, and fully guarded (any failure / non-positive size falls through).

2. It must read the controlling terminal, not the shell's stdout.
A common launcher pattern runs the app via command substitution — out=$(app …) — which makes the app's stdout a pipe. The shell's own terminal then looks absent and reports the default width, even though the user is sitting at a real terminal. Interactive UIs avoid this because they talk to the tty directly; help rendering did not.

ProcessedCommand.detectTerminalWidth() fixes that: on POSIX it asks the controlling terminal directly with stty size </dev/tty, which is correct regardless of stdin/stdout redirection. It is guarded and time-boxed (no tty / no stty / any error → -1); Windows returns -1. getHelpInfo resolves width by probing the controlling terminal first, then the shell's reported size, then the default — in both AeshCommandInvocation and DefaultCommandInvocation (the latter is what the one-shot AeshRuntimeRunner uses).

The supplier is propagated to child commands so subcommand-list entries wrap to the same width.

Tests

HelpWidthTest covers: default; aesh.help.width override; min clamp; non-numeric ignored; terminal-width supplier used when no override; override beats supplier; supplier exception / non-positive falls back; tiny size clamped; detectTerminalWidth() is safe (no throw, -1-or-positive) with no terminal; and that a long description wraps onto more lines at a narrower width. All existing help/formatter tests pass (152 in the help + formatter suites).

Help text was always wrapped to the hardcoded HELP_WIDTH (80), regardless
of terminal size or user preference. Add resolveHelpWidth(): an explicit
override wins, in order the `aesh.help.width` system property then the
`COLUMNS` environment variable (as many CLIs honour), otherwise the 80
default. A resolved value is clamped to a 40-column minimum so a tiny or
bogus override cannot garble wrapping, and non-numeric values are ignored.

printHelp() and the command-list description wrap now use the resolved
width. HELP_WIDTH stays as the documented default.
@maxandersen

Copy link
Copy Markdown
Contributor Author

this first version is not enough ...examiing.

Build on the configurable help width: besides the aesh.help.width system
property and COLUMNS, resolve the live terminal width when help is actually
printed interactively.

Why a lazily-queried hook rather than reading the terminal eagerly:
printHelp() also runs where there is no terminal — documentation
generation and programmatic/LazyHelp rendering — so querying a terminal
unconditionally would either fail or force a terminal connection that the
caller never wanted. The width is therefore resolved through an optional
IntSupplier installed only by a caller that owns a terminal
(AeshCommandInvocation.getHelpInfo), consulted here and only here (when
usage prints), and fully guarded: a missing or misbehaving terminal (or a
non-positive size) falls back to the override, then to the 80 default.
Any resolved value is clamped to a 40-column minimum.

Resolution order in ProcessedCommand.helpWidth(): aesh.help.width ->
COLUMNS -> live terminal (if a supplier is set) -> HELP_WIDTH. The static
resolveHelpWidth() keeps the terminal-free override-or-default behaviour for
callers with no render context. The supplier is propagated to child
commands so subcommand list entries wrap to the same width.
The terminal-width supplier relied on the shell's own terminal, which is
blind when the command's stdout is captured/redirected (e.g. a launcher
that runs the app via command substitution): the shell sees a non-terminal
stdout and reports the default width, even though a real terminal is right
there on the controlling tty.

Add ProcessedCommand.detectTerminalWidth(): on POSIX it asks the
controlling terminal directly via `stty size </dev/tty`, correct regardless
of stdin/stdout redirection. It is guarded and time-boxed, so no tty, no
stty, or any failure yields -1; Windows returns -1. It is only invoked on
demand, when usage actually prints, via the width supplier.

Both getHelpInfo paths (AeshCommandInvocation and DefaultCommandInvocation,
the latter used by the one-shot AeshRuntimeRunner) now resolve the width by
probing the controlling terminal first and falling back to the shell size.
@maxandersen

Copy link
Copy Markdown
Contributor Author

ok - this isn't pretty ... @stalep suggestions on making it better / possible to utilize terminal api without circular dependency ? :)

@stalep

stalep commented Nov 10, 2026

Copy link
Copy Markdown
Member

I've added support for terminal size in aesh-readline:terminal-detect. Doing a new release of that now.

stalep added a commit that referenced this pull request Nov 10, 2026
helpWidth: aesh.help.width sysprop, then COLUMNS, then the
controlling terminal size, then 80; clamped to a 40-column
minimum. Consumes the new terminal-detect query instead of
spawning a probe, so every render path (interactive, one-shot,
docgen) resolves the same way with no per-path plumbing.
@stalep

stalep commented Nov 10, 2026

Copy link
Copy Markdown
Member

@maxandersen I've fixed this by using aesh-readline upstream so closing it. The feature is in aesh though!

@maxandersen

Copy link
Copy Markdown
Contributor Author

close but no cigar :) see #763

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.

2 participants