Skip to content

feat(cli): show friendly amounts and relative times, and color tables - #101

Merged
thebrandonlucas merged 1 commit into
feat/cli-look-04-confirmfrom
feat/cli-look-05-tables
Oct 23, 2026
Merged

thebrandonlucas merged 1 commit into
feat/cli-look-04-confirmfrom
feat/cli-look-05-tables

Conversation

@thebrandonlucas

@thebrandonlucas thebrandonlucas commented Oct 22, 2026 •

Copy link
Copy Markdown
Contributor

Stack position: 5 of 13 in the look-and-feel stack. Base: feat/cli-look-04-confirm (#98).

Problem

Human tables showed amounts in API base units (150000 msats, 250 cents) and every timestamp as a full UTC string. Both are exact but slow to read. Tables and field views had no color, so the status of each row did not stand out.

Behavior (human output only; JSON is unchanged)

What Before After
Whole sats 150000000 msats 150,000 sats (1 sat singular)
Fractional sats 1500 msats 1.5 sats (every BTC amount in sats, up to 3 decimals, so a column never mixes units)
Cents 123456 cents $1,234.56
Other units 1000 usd 1,000 usd (grouped)
Table timestamp 2026-10-21 14:02:11 UTC 1 day ago / in 2 days / just now (within 60 s either way)
Field timestamp 2025-10-28 16:51:09 UTC 2025-10-28 16:51:09 UTC (11 months ago)

With color on:

  • Table headers and field names are bold.
  • Known statuses are colored: completed, succeeded, ready, active, paid green; failed, expired, error, rejected red; sending, receiving, generating, approved, pending, processing yellow. Any other status stays plain.
  • The outcome line is colored: accepted yellow, completed and ready green, dry_run bold yellow.

Design notes

  • Width before style. comfy-table lays out plain, scrubbed text as before. Styles are added afterwards:
    • The header line and field names are wrapped whole.
    • A status cell is found at the STATUS label's display column, using unicode-width, so wide glyphs earlier in the row do not shift it. If any label before STATUS was shortened, statuses stay plain rather than risk a misplaced color.
  • One color decision per stream. Human stdout now goes through anstream::AutoStream with the --color choice. Tables are always rendered styled, and the stream strips the styles when stdout is not a colored terminal, so the piped bytes equal the plain render. Output::new takes the choice. --output-file and JSON never pass through the renderer.
  • human::View { width, now, color } replaces the bare width argument, and tests use a fixed now. Timestamps are converted to epoch seconds with Hinnant's days_from_civil, so no time crate is added.
  • Dependency: unicode-width = "=0.2.2", which comfy-table already uses, so Cargo.lock gains one dependency edge and no crate.
  • The PR 4 preview reuses these renderers, so it now reads amount 1,000 sats.

Try it on staging

Setup: Try a pull request on staging (added by #97). Everything here is read-only. Find a wallet ID with voltage wallets list --account me.

voltage wallets list --account me
voltage payments list --account me --limit 5

Expect: bold headers, CREATED AT and UPDATED AT as 11 months ago and 8 days ago, and statuses colored (expired red, completed green).

voltage wallets get WALLET_ID --account me

Expect: bold field names, and created_at 2025-10-28 16:51:09 UTC (11 months ago).

voltage payments receive --account me --wallet WALLET_ID --currency btc --kind bolt11 --amount 1500 --unit sats

Expect, for this description (it sends nothing): amount 1,500 sats in the summary and the details, and a bold yellow dry_run outcome line.

voltage payments list --account me --limit 5 | cat
voltage payments list --account me --limit 5 --output table | cat
voltage payments list --account me --limit 5 --color never

Expect: piped JSON, unchanged; a piped table with the same layout, friendly values, and no escape codes; and the plain table on a terminal.

Validation

  • cargo fmt --check, cargo clippy --locked --all-targets -- -D warnings, cargo machete --with-metadata, cargo test --locked (152 unit and 126 integration tests), and python3 scripts/check-coverage.py all pass.
  • Unit tests cover:
    • amounts: 0, 1, 999, 1000, 1500, a 19-digit value, sats, cents (0, 5, 123456), another unit, and non-integer numbers left as sent;
    • relative times at every unit boundary, past and future;
    • epoch conversion (1970, 2024, and 2026 values checked against Python);
    • field and table placement of timestamps;
    • color that strips to the plain render, a wide glyph before the status column, an unknown status left plain, a shortened label disabling status color, and bold field names only.
  • New integration test: payments list --output table with --color always has green completed and red failed, strips to the --color auto output, and --color never equals it. The existing narrow-terminal test now expects 150 sats.
  • Checked by hand on staging: wallets list, payments list, and wallets get render as described.
  • CRAP: 0 of 702 measured functions at or above 20. The highest is 18.00 (src/cli.rs endpoint, untouched). human::table is 17.00.

Docs

docs/reference/output.md and CHANGELOG.md.

@tee8z tee8z 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.

Requesting changes on 1ff530f (base #98 00bae89). The formatting is exact:

  • Amounts are converted as strings, so nothing overflows or rounds, even at 19 digits.
  • Cents are padded correctly ($0.05, $1.00), and negative gives -$2.50.
  • Asset amounts read as base units.
  • Only Z timestamps are converted, and days_from_civil matches the reference values.
  • JSON, NDJSON and --output-file never pass through the renderer.
  • Styles are applied after layout, so alignment counts visible width.
  • Only fixed status words are painted.

P2: one amount column can mix sats and msats, which invites a 1,000x misreading. friendly (src/output/human.rs:490) shows a msat amount that is a whole number of sats as sats, and anything else as msats. In a payments list, 150,000 msats and 1,500 msats then show as 150 sats and 1,500 msats in the same column. The smaller payment shows the bigger number, and only an m tells them apart. The PR's own test asserts that mixed column (tests/cli_integration.rs:3753). It also invites a copy mistake, because --amount without --unit is in msats, so 150 sats pasted as --amount 150 asks for 150 msats. Show every BTC amount in one unit. Sats with up to three decimals (1.5 sats) stays exact.

Nonblocking:

  • relative (human.rs:425) turns a small negative difference into in under a minute, and a few minutes into in 2 minutes. So a local clock that runs behind the server shows a resource created just now as created in the future, and tables no longer show an absolute time to correct it. Treating anything within 60 seconds either way as just now avoids it.
  • The description says relative times are tested "at every unit boundary", but the cases never hit exactly 3,600, 86,400, 2,592,000 or 31,536,000 seconds. These early returns are untested: paint_table's split_first_mut else return, char_at_column's None => continue through paint_table, and the before-1970 fallback in View::plain.
  • The tests call crate::style::paint(...) inside functions (human.rs:772, :775, :776). The style checklist asks for an import at the top of the test module.

Validation on this head, in nix develop: cargo fmt --check, cargo clippy --locked --all-targets -- -D warnings, cargo machete --with-metadata, cargo test --locked (152 unit, 126 integration) and python3 scripts/check-coverage.py passed. The CRAP report did not finish: its instrumented run hit the known flaky terminal test from #75 (a_profile_saved_after_login_keeps_changes_made_while_its_prompt_was_open, at the 10-second prompt wait), which #104 addresses. I will rerun it and add the result. CI is green.

Comment thread src/output/human.rs Outdated
Comment thread src/output/human.rs Outdated
Human output shows amounts in their friendliest exact unit with grouped
digits (150,000 sats, $1,234.56), table timestamps relative to now
(3 minutes ago), and single-resource timestamps with the relative time
beside the UTC time. With color on, table headers and field names are bold
and known statuses and outcomes are colored. Styles are added after
comfy-table lays out plain text and stdout strips them when color is off, so
columns never move and piped tables keep their layout. JSON is unchanged.
@thebrandonlucas
thebrandonlucas force-pushed the feat/cli-look-05-tables branch from c9b442e to 2ac8b2c Compare October 23, 2026 12:38
@thebrandonlucas

Copy link
Copy Markdown
Contributor Author

Review-body notes, in 2ac8b2c:

  • Clock skew: within 60 seconds either way reads just now (also replied on the thread).
  • Boundaries and early returns:
    • The relative-time test now covers exactly 3,600, 86,400, 2,592,000, and 31,536,000 seconds.
    • New tests for paint_table with no lines, and with a row that ends before the status column (the char_at_column None path).
    • New test for the before-1970 fallback, now in unix_seconds.
  • Test imports: paint is imported at the top of the test module.

@tee8z tee8z 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.

Approved 2ac8b2c against feat/cli-look-04-confirm (#98 b17d0ca). My findings are fixed:

  • Every BTC amount is shown in sats, with up to three decimals for msats, so one column never mixes units. I checked the cases: 1 msat reads 0.001 sats, 1500 reads 1.5 sats, 150000 reads 150 sats, and 1234567 reads 1,234.567 sats. The integration test now asserts 150 sats beside 1.5 sats.
  • Anything within a minute either way reads just now.
  • The nonblocking notes are addressed too: tests for the empty and short-table early returns and for the before-1970 fallback, and paint is imported in the test module.

Validation on this head, in nix develop: cargo fmt --check, cargo clippy --locked --all-targets -- -D warnings, cargo machete --with-metadata, cargo test --locked (166 unit, 133 integration) and python3 scripts/check-coverage.py passed. CRAP: 735 measured functions, none at or above 20, highest wait_for_payment at 17.05. CI's RustSec audit fails on RUSTSEC-2026-0385 (ctutils 0.4.2, published today), which master has too. That needs a ctutils bump of its own, not a change here. The other CI jobs pass.

@thebrandonlucas
thebrandonlucas merged commit d158ea8 into master Oct 23, 2026
7 of 8 checks passed
@thebrandonlucas
thebrandonlucas deleted the feat/cli-look-05-tables branch October 23, 2026 15:47
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