output: page only long read-only output, not writes or status #236
Loading…
Reference in a new issue
No description provided.
Delete branch "fix/pager-only-long-reads"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Problem
There was exactly one pager call site,
cli::run, and it matched on the top-levelCommandvariant:A
Commandvariant is a command family, not a command. MatchingCommand::Pr(_)pagespr merge,pr close,pr status, andpr list --jsonexactly as much as it pagespr list. Across those eleven families that is roughly ninety leaf subcommands routed intoless, of which about a dozen produce output long enough to want it. The single carve-out,run watch, is the shape of the bug in miniature: someone hit a concrete symptom (a live stream buffering) and patched that one leaf rather than the granularity.Most of the rest was invisible, because the default
less -FRXquits when output fits one screen, so✓ Merged pull request #12looked fine. It stopped looking fine in three places:$PAGERwithout-F. fj deliberately does not fight the user's$PAGER, which is right, but it meant every write confirmation sat behind a keypress.--json. Machine output routed through a program built for humans. The non-TTY check savesfj pr list --json | jq; it does not save reading the JSON on a terminal first, which is how anyone finds the field they are about to pipe.dispatch, so by the timerepo delete,release delete, ortag deletereaches itsConfirmprompt, stdout is alreadydup2'd into the pager child. The prompt is written intoless; the answer is read from the terminal. The user is confirming a destructive action against a prompt they cannot see.output::pager::maybe_startitself was fine. Non-TTY,FJ_NO_PAGER,--no-pager, and the-Fin the defaultless -FRXall did their jobs. The wrong thing was the call site.Fix
Decide per subcommand, in a new
cli::paging::should_page(&Command) -> bool. It pages only genuinely long, read-only, human-facing output.Still paged
repo list/view/starred/branches(listing form)repo collaborator list,repo deploy-key listissue list/view,pr list/view--commentspr diff/commits/files/checksrelease list/view,label list,milestone list,tag listrun list/view/logsearch repos/issues/prs/usersfj apiGET and HEADNo longer paged
fj status(a capped notification inbox, and--mark-readis a one-line write),fj pr status(a fixed three-section dashboard).repo create/fork/sync/edit/rename/archive/unarchive/delete/mirror/mirror-sync/watch/unwatch/star/unstar,repo branches create|delete,repo collaborator add|remove,repo deploy-key add|delete,issue create/edit/close/reopen/lock/unlock/delete/pin/unpin/comment/edit-comment/delete-comment/develop,pr create/edit/ready/comment/attach/update-branch/review/request-review/unrequest-review/merge/close/reopen/lock/unlock,release create/edit/delete/upload/delete-asset,label create/edit/delete/clone,milestone create/edit/close/reopen/delete/assign,tag create/delete,run rerun/cancel, andfj apiwith a non-GET method. Each prints a line or two, and the three that prompt were prompting into the pager.repo clone(git),pr checkout(git),release downloadandrun download(write files, report progress).tag viewandmilestone vieware fixed blocks of four to six lines.repo topicsprints one line.repo set-defaultprints one line or writes config.--json, any--web(prints nothing, opens a browser), andapi --silent(prints nothing).Two things worth calling out about the shape:
should_pageis a pure function of the parsed clap tree, so the entire table above is unit tested with no terminal, no network, and no auth:Cli::try_parse_from(argv)then assert. Seven tests, ~90 assertions, insrc/cli/paging.rs. That is the difference between a table that is right today and one that stays right when a subcommand is added, since a newRepoSubvariant now fails to compile until someone classifies it. The matches are deliberately exhaustive rather than_ => falsefor that reason.cli::api::pick_methodbecomespub(super)so the api rule asks the same question the handler will, rather than reimplementing "GET unless-Xor--input" and drifting from it.Nothing in
src/output/pager.rschanged.Test
cargo build,cargo fmt --check, andcargo clippy --all-targets --all-features -- -D warningsclean.cargo test --all: 690 passed, 0 failed, 2 ignored (the pre-existing env-mutating pair).cli::tests::cov_ignore_stays_in_sync_with_test_modulespasses withpaging.rsoutsideCOV_IGNORE, which is correct since it is a unit-test target rather than glue.Behavior was checked end to end under a real pty (
script -qec) withFJ_PAGERpointed at a marker script that announces itself on stderr and thencats, againstrasterstate/fjon rasterhub.com. Before this change, all ten of these started the pager:After, none of them do, and these still do:
The write probes ran against
rasterstate/nope-does-not-existso the API 404s before mutating anything; the pager decision is made ahead of dispatch, so the probe is unaffected by the failure.No regression in the opt-outs, checked on
pr list, a command that does page:--no-pagerdirect,FJ_NO_PAGER=1direct, stdout piped to a non-TTY direct. The defaultless -FRXkeeps its-F, so even a paged command that returns two rows does not trap.Not run locally:
make coverage-strict, which needs a nightly toolchain andcargo-llvm-cov, neither installed here. CI's gate isCOV_MIN=73and this adds a fully-covered pure module outsideCOV_IGNORE, so it should move up rather than down.Unrelated, noticed while testing
fj label createandfj label editpanic during arg parsing on currentmain:Their
--color(aStringhex value) collides with the global--color(ColorChoice) on clap's arg id.label createhasdefault_value = "ededed", so the collision fires even without the flag.label deleteandlabel listare fine. Reproduced on stockmainatc7d8e0f, so it predates this branch and is out of scope here; those two invocations are omitted from the paging tests with a comment saying why.cli::tests::command_tree_is_internally_consistentdoes not catch it becauseCommand::debug_assertchecks definitions, not downcasts. Worth its own issue.Forseti review
No blocking findings from the lead reviewer.
No inline findings.
rasterstate/fj#23632d71cff5337sha256:bef0dcc223c04daa103a4df22f634d9353ffbd160e1fa4d75299157563c3460copenai:gpt-5.5+anthropic:claude-haiku-4-5-2025100100GITHUB_TOKENcaf2f9a78989Forseti review
No blocking findings from the lead reviewer.
No inline findings.
rasterstate/fj#23632d71cff5337sha256:bef0dcc223c04daa103a4df22f634d9353ffbd160e1fa4d75299157563c3460copenai:gpt-5.5+anthropic:claude-haiku-4-5-2025100100GITHUB_TOKENcaf2f9a78989