Skip to content

fix(tui): plain top-left screens; raw-mode rows back to column 0; rows cut to width - #234

Merged
duyet merged 3 commits into
mainfrom
fix/tui-render-plain
Oct 27, 2026
Merged

duyet merged 3 commits into
mainfrom
fix/tui-render-plain

Conversation

@duyet

@duyet duyet commented Oct 27, 2026

Copy link
Copy Markdown
Contributor

The 0.1.34 anyr auth status picker drifted right on each row, a long scopes error wrapped across lines, and a paid account showed plan —. This PR fixes all three. It also redraws the launcher as plain top-left text, and adds terminal-render tests that run the real binary in a pseudo-terminal.

Root causes

Staircase. term::pick_inline_raw wrote each frame with writeln! while raw mode was on. Raw mode turns off the terminal's \n → \r\n translation, so each row began in the column where the last one ended. The checkbox picker already handled this; the inline picker didn't. Both now call one writer, term::write_raw_frame, which ends every line in \r\n and cuts it to the terminal width. A wrapped line also threw off the MoveUp count on the next redraw. The relay live view draws through the same function (term::raw_frame_bytes). No other screen enables raw mode: the ratatui screens use the alternate screen, and prompt_secret only turns echo off.

Long rows. term::label_rows now sits on a pure layout core, term::label_lines(groups, width). It cuts each value to the width with …, measured with unicode-width. The divider is capped at the width too, so it no longer wraps at 40 columns. A failed lookup now prints a short reason, e.g. unavailable (timed out). The full error stays in --json (key_error, identity_error), and auth status --verbose prints it on stderr.

"Network Error: Network Error". This was not the CLI wrapping the error twice. ureq 2 nests an I/O error inside its own Network Error kind, so its Display repeats the kind. A new http::unreachable() names the cause once: Could not reach AnyRouter: <url>: timed out, could not connect, or DNS lookup failed.

Timeout. The API is not slow. 20 sequential calls to GET /api/v1/key with the smoke key took min 0.23 s, p50 0.26 s and max 0.53 s; the first cold call took 0.98 s. /v1/me p50 was about 0.22 s. These numbers are from this host, not the owner's Mac, so no API issue was filed. The 3 s client budget left little room for a slow first DNS lookup and TLS handshake. It is now 4 s. An existing test pins that status answers within 5 s when the gateway hangs, so 4 s is the limit.

Plan row. The dash appeared because /key timed out, not because a field was missing. The plan now comes from /me's plan when the server sends one (duyet/anyrouter#6653), then from is_free_tier (free or paid). When neither answered, the row is hidden. The CLI never prints a bare dash for the plan.

Launcher

The orange frame, backdrop, pixel logo and centered card are gone. The launcher draws from row 0, column 0:

  • the account and credits rows, laid out by term::label_lines
  • a thin line
  • the filter input
  • the agent list: agent, model and key last 4 (the account shows only when it differs from the active profile)
  • the configure options
  • a one-line key hint

Every capability stays: fuzzy filter, mouse hits and the background credits refresh. key… is gone (#232). The menu, settings and picker screens follow the same rule: a title row, status rows, one line and the list, with no frame. With NO_COLOR or TERM=dumb, the ratatui theme keeps only bold. TERM=dumb also uses the typed numbered prompt instead of cursor moves.

Tests

tests/tty_render.rs (unix only) runs the real binary in a pty (libc::openpty, with the pty as the controlling terminal). It feeds the output into a vt100 emulator and asserts on the rendered screen. The binary runs as <shell> -c "exec anyr …" under bash, zsh, fish and sh, at 40, 80 and 200 columns. The cases use a temp HOME, fake agents on PATH and a local scripted API.

  • Every non-blank row starts within 4 columns of the left edge. No row soft-wraps (vt100::Screen::row_wrapped). No frame characters appear.
  • The status menu labels all start in the same column. This is the staircase check.
  • The scopes row is cut with …. The plan row says paid.
  • The launcher's content starts on row 0 or 1, and the account, credits, agent, configure and quit rows are all present.
  • With NO_COLOR, no SGR codes appear. With TERM=dumb, there are no escape bytes at all, and the typed prompt is used.
  • Piped auth status and menu output has no escape bytes on stdout or stderr.

A missing shell prints SKIP <shell>: not installed on this host. CI sets ANYR_PTY_REQUIRE_SHELLS=1, so a missing shell fails there instead of skipping. CI installs zsh and fish with apt on Linux (both the coverage job and the build matrix) and installs fish with brew on macOS. ANYR_TTY_BIN points the cases at another build, such as a release binary.

This host has no fish, so it ran bash, zsh and sh locally. fish runs in CI.

Proof it fails on 0.1.34. These are the same cases with ANYR_TTY_BIN pointing at a 0.1.34 build. Four of five fail. The piped case already passed.

Unit tests cover:

  • raw_frame_bytes: every line ends in \r\n, and the cut keeps color codes whole.
  • label_lines: no row is wider than 40, 80 or 200 columns.
  • http::unreachable against a server that never answers: the error reads timed out once.
  • short_reason, run on the exact 0.1.34 error string.
  • The plan order: /me plan, then is_free_tier, then hidden.

Before / after (VT100 screen dumps from the tests)

anyr auth status, 80 columns, 0.1.34:

|account   Duyet · me@example.com
|credits   $133.83
|plan      paid
|────────────────────────────────────────────────────────────
|key       laptop · sk-ar-v1-testk…6789
|scopes    /api/v1/endpoint-number-0, /api/v1/endpoint-number-1, /api/v1/endpoint
|-number-2, /api/v1/endpoint-number-3, /api/v1/endpoint-number-4, /api/v1/endpoin
|t-number-5, /api/v1/endpoint-number-6, /api/v1/endpoint-number-7, /api/v1/endpoi
|nt-number-8, /api/v1/endpoint-number-9, /api/v1/endpoint-number-10, /api/v1/endp
|oint-number-11, /api/v1/endpoint-number-12, /api/v1/endpoint-number-13, /api/v1/
|endpoint-number-14, /api/v1/endpoint-number-15, /api/v1/endpoint-number-16, /api
|/v1/endpoint-number-17, /api/v1/endpoint-number-18, /api/v1/endpoint-number-19,
|/api/v1/endpoint-number-20, /api/v1/endpoint-number-21, /api/v1/endpoint-number-
|22, /api/v1/endpoint-number-23, /api/v1/endpoint-number-24, /api/v1/endpoint-num
|ber-25, /api/v1/endpoint-number-26, /api/v1/endpoint-number-27, /api/v1/endpoint
|-number-28, /api/v1/endpoint-number-29, /api/v1/endpoint-number-30, /api/v1/endp
|oint-number-31, /api/v1/endpoint-number-32, /api/v1/endpoint-number-33, /api/v1/
|endpoint-number-34, /api/v1/endpoint-number-35, /api/v1/endpoint-number-36, /api
|/v1/endpoint-number-37, /api/v1/endpoint-number-38, /api/v1/endpoint-number-39
|last use  2026-10-27 15:31 UTC
|profile   default
|model     anyrouter/auto
|base url  http://127.0.0.1:40603/api
|config    /tmp/anyr-tty-1005493-2/.anyrouter/config.yaml
|  1. Switch account
|                     2. Change default model
|                                              3. Log out
|                                                          4. Open dashboard
|                                                                             5.
|Quit
|
|    ↑↓ move · enter select · esc quit

After:

|account   Duyet · me@example.com
|credits   $133.83
|plan      paid
|────────────────────────────────────────────────────────────
|key       laptop · sk-ar-v1-testk…6789
|scopes    /api/v1/endpoint-number-0, /api/v1/endpoint-number-1, /api/v1/endpoin…
|last use  2026-10-27 15:31 UTC
|profile   default
|model     anyrouter/auto
|base url  http://127.0.0.1:38535/api
|config    /tmp/anyr-tty-1052619-14/.anyrouter/config.yaml
|› 1. Switch account
|  2. Change default model
|  3. Log out
|  4. Open dashboard
|  5. Quit
|
|↑↓ move · enter select · esc quit

Launcher, 40 columns, 0.1.34:

|
|
|
|
|
|
|
|
|
|
|
| ┌ anyr ──────────────────────────────┐
| │                                    │
| │     ▄▄ ▄▄▄   account  duyet · me@e │
| │   ▄█▀▀█▄▄█▀  credits  $133.83      │
| │  ▀▀    ▀▀▀▀                        │
| │ ────────────────────────────────── │
| │ ❯ █                                │
| │                                    │
| │   LAUNCH                           │
| │ ❯ › claude anyrouter/auto  ·  defa │
| │   › codex anyrouter/auto  ·  defau │
| │                                    │
| │   CONFIGURE · CLAUDE               │
| │   model…                for claude │
| │   account…              for claude │
| │   key…                  for claude │
| │   exacto          off · for claude │
| │   tools           off · for claude │
| │   1M ctx          off · for claude │
| │                                    │
| │   MORE                             │
| │   install…  install a coding agent │
| │   config…  accounts · keys · agent │
| │                                    │
| │ type to filter    ↑↓  pick agent   │
| │                                    │
| └────────────────────────────────────┘

Launcher, 80 columns, after:

|account  duyet · me@example.com
|credits  $133.83
|────────────────────────────────────────────────────────────
|❯ █
|  launch
|❯ › claude    anyrouter/auto  ·  …6789
|  › codex     anyrouter/auto  ·  …6789
|
|  configure · claude
|  model…    for claude
|  account…  for claude
|  exacto    off · for claude
|  tools     off · for claude
|  1M ctx    off · for claude
|
|  more
|  install…  install a coding agent
|  config…   accounts · keys · agent
|  quit      esc works too
|
|type to filter    ↑↓  pick agent    ↵  launch / switch    esc  quit

Checks

cargo fmt --check, cargo clippy --locked --all-targets -- -D warnings, cargo check --lib --no-default-features and cargo test --locked --all-targets all pass locally (linux x86_64).

🤖 Generated with Claude Code

duyet and others added 2 commits October 28, 2026 01:00
…ws cut to width

Co-authored-by: Duyet Le <me@duyet.net>
Co-authored-by: duyetbot <bot@duyet.net>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Duyet Le <me@duyet.net>
Co-authored-by: duyetbot <bot@duyet.net>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

@duyetbot duyetbot left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Re-ran tests/tty_render (5) and tests/status (5): all pass; fish skipped locally (not installed), runs in CI.
Prod pty (80x50, smoke key, temp HOME): auth status has no bare LF after raw mode starts; rows account, credits $53.08, plan free; no Network Error; picker rows aligned.
Launcher raw output has no box chars and no logo glyph; no key... text; filter, credits refresh and configure rows present.
Diff reviewed: mouse/key handling in tui/live.rs untouched, no secrets, no orphaned code found.
Verdict: approve once CI is green.

Co-authored-by: Duyet Le <me@duyet.net>
Co-authored-by: duyetbot <bot@duyet.net>
@github-actions

Copy link
Copy Markdown
Contributor

anyr size and startup

Startup is wall time for a cold anyr --version / anyr --help (median of 21 runs). Size is the stripped release binary, or the .wasm for the browser demo.

Size budget: anyr-linux-x86_64 must stay ≤ 4.0 MiB (4194304 bytes) stripped.

Asset Kind Size --version median --help median
anyr-darwin-arm64 native 2.4 MiB 9.08 ms 8.44 ms
anyr-darwin-x86_64 native 2.9 MiB 45.93 ms 45.53 ms
anyr-linux-arm64 native 2.9 MiB 0.77 ms 0.8 ms
anyr-linux-x86_64 native 3.4 MiB 0.84 ms 0.85 ms
anyr-windows-x86_64.exe native 2.9 MiB 11.0 ms 11.02 ms
anyr.wasm wasm 252.0 KiB — —

Budget check: ok

raw timings
[
  {
    "asset": "anyr-darwin-arm64",
    "kind": "native",
    "path": "/Users/runner/work/cli/cli/anyr-darwin-arm64",
    "bytes": 2537272,
    "size": "2.4 MiB",
    "version": "0.1.34 (built 2026-10-27 18:38:00)",
    "target": "aarch64-apple-darwin",
    "os": "macos-latest",
    "startup_version": {
      "n": 21,
      "min_ms": 5.37,
      "median_ms": 9.08,
      "p95_ms": 31.23,
      "mean_ms": 12.64
    },
    "startup_help": {
      "n": 21,
      "min_ms": 4.43,
      "median_ms": 8.44,
      "p95_ms": 17.02,
      "mean_ms": 11.01
    }
  },
  {
    "asset": "anyr-darwin-x86_64",
    "kind": "native",
    "path": "/Users/runner/work/cli/cli/anyr-darwin-x86_64",
    "bytes": 3037648,
    "size": "2.9 MiB",
    "version": "0.1.34 (built 2026-10-27 18:37:45)",
    "target": "x86_64-apple-darwin",
    "os": "macos-latest",
    "startup_version": {
      "n": 21,
      "min_ms": 35.17,
      "median_ms": 45.93,
      "p95_ms": 58.97,
      "mean_ms": 45.96
    },
    "startup_help": {
      "n": 21,
      "min_ms": 40.07,
      "median_ms": 45.53,
      "p95_ms": 57.06,
      "mean_ms": 48.44
    }
  },
  {
    "asset": "anyr-linux-arm64",
    "kind": "native",
    "path": "/home/runner/work/cli/cli/anyr-linux-arm64",
    "bytes": 3020048,
    "size": "2.9 MiB",
    "version": "0.1.34 (built 2026-10-27 18:37:12)",
    "target": "aarch64-unknown-linux-gnu",
    "os": "ubuntu-24.04-arm",
    "startup_version": {
      "n": 21,
      "min_ms": 0.71,
      "median_ms": 0.77,
      "p95_ms": 0.86,
      "mean_ms": 0.78
    },
    "startup_help": {
      "n": 21,
      "min_ms": 0.76,
      "median_ms": 0.8,
      "p95_ms": 0.88,
      "mean_ms": 0.81
    }
  },
  {
    "asset": "anyr-linux-x86_64",
    "kind": "native",
    "path": "/home/runner/work/cli/cli/anyr-linux-x86_64",
    "bytes": 3520712,
    "size": "3.4 MiB",
    "version": "0.1.34 (built 2026-10-27 18:39:47)",
    "target": "x86_64-unknown-linux-gnu",
    "os": "ubuntu-latest",
    "startup_version": {
      "n": 21,
      "min_ms": 0.82,
      "median_ms": 0.84,
      "p95_ms": 0.88,
      "mean_ms": 0.85
    },
    "startup_help": {
      "n": 21,
      "min_ms": 0.81,
      "median_ms": 0.85,
      "p95_ms": 0.9,
      "mean_ms": 0.85
    }
  },
  {
    "asset": "anyr-windows-x86_64.exe",
    "kind": "native",
    "path": "D:\\a\\cli\\cli\\anyr-windows-x86_64.exe",
    "bytes": 2994688,
    "size": "2.9 MiB",
    "version": "0.1.34 (built 2026-10-27T18:38:18Z)",
    "target": "x86_64-pc-windows-msvc",
    "os": "windows-latest",
    "startup_version": {
      "n": 21,
      "min_ms": 9.88,
      "median_ms": 11.0,
      "p95_ms": 13.69,
      "mean_ms": 11.37
    },
    "startup_help": {
      "n": 21,
      "min_ms": 10.05,
      "median_ms": 11.02,
      "p95_ms": 13.06,
      "mean_ms": 11.26
    }
  },
  {
    "asset": "anyr.wasm",
    "kind": "wasm",
    "path": "/home/runner/work/cli/cli/target/wasm-pkg/anyr_cli_bg.wasm",
    "bytes": 258065,
    "size": "252.0 KiB",
    "version": "wasm",
    "target": "wasm32-unknown-unknown",
    "os": ""
  }
]

@duyet
duyet merged commit 9a9d3f5 into main Oct 27, 2026
10 checks passed
@duyet
duyet deleted the fix/tui-render-plain branch October 27, 2026 18:40
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