Skip to content

Ask a person what they want instead of making them know - #60

Merged
op-q merged 2 commits into
feat/no-hosted-relayfrom
feat/terminal-ui
Sep 10, 2026
Merged

op-q merged 2 commits into
feat/no-hosted-relayfrom
feat/terminal-ui

Conversation

@op-q

@op-q op-q commented Sep 10, 2026

Copy link
Copy Markdown
Owner

Stacked on #59. Retargets as the stack merges.

drop, drop send and drop recv, typed on their own in a terminal, open an interface: a chooser, a file browser, a code field, a destination picker, and a few checkboxes. Everything else is unchanged — the flags are the program-facing surface, and every harness that drives them keeps working untouched.

The activation rule

Stricter than "no arguments on a terminal", deliberately:

  • All three streams must be terminals, stdout included, though the interface never writes there. The transfer code goes to stdout so it survives a pipe, so a redirected stdout is somebody collecting that code — they should get it, not a screen.
  • DROP_STATUS counts even with a bare command line. A harness exports it once and every drop it spawns inherits it.
  • Any flag means the command. Not a curated list of flags that imply non-interactive use; that list would be wrong the first time somebody added a flag.

Being wrong toward the command costs a person one extra word. Being wrong the other way hangs a CI job on a screen nobody can see.

Terminal lifecycle came first

The worst failure here is not a bad screen, it is a shell that no longer echoes. send answers a signal with std::process::exit(130) and no unwinding, so the spool file can be deleted by hand — but no destructor runs on that path. restore() therefore lives over an atomic and the signal handler calls it, before deleting the spool file, since the terminal is the one that cannot be recovered from by hand.

recv had no termination handler at all and now has one. And Ctrl-C had to become an ordinary key as well as a signal — in raw mode the driver stops turning it into SIGINT.

Three things found by using it

Each a design error rather than a discoverability one:

  • enter opened a folder while s selected one, so the gesture everybody makes first did the wrong thing. enter now picks whatever is highlighted, → opens, and choosing the folder you are standing in is the first row of the listing.
  • That was only safe once esc meant back. It meant "give up entirely", so one mistaken keystroke was unrecoverable. The flow is now a loop whose next screen is whichever answer is still missing; stepping back clears the most recent one.
  • The code field took any non-empty string and let TransferCode::parse reject it two screens later, from a program that had already exited. It is checked where it is typed, by the same parser.

Cost

ratatui and crossterm, both pure Rust — the bar flate2 is already held to for four prebuilt targets. Release binary 26,988,848 → 27,482,824 bytes, +1.8%.

Not in this PR

Progress still prints below the closed interface. Putting it on screen, closing both sides on cancel, and the receiver's missing cancel message are phase 3 of the plan.

Verification

Full workspace suite, clippy --all-targets, fmt, check-secrets.sh — all pass. The interface itself is driven through a pty, since a shell cannot test it.

🤖 Generated with Claude Code

`drop`, `drop send` and `drop recv`, typed on their own in a terminal, open
an interface: a chooser, a file browser, a code field, a destination picker,
and a few checkboxes. Everything else is unchanged, which is the point — the
flags are the program-facing surface and every harness that drives them keeps
working untouched.

The activation rule is deliberately conservative, and stricter than "no
arguments on a terminal":

- All three streams must be terminals, **stdout included**, though the
  interface never writes there. The transfer code goes to stdout so it
  survives a pipe, so a redirected stdout is somebody collecting that code
  and they should get it rather than a screen.
- `DROP_STATUS` counts even with a bare command line. A harness exports it
  once and every `drop` it spawns inherits it.
- **Any** flag means the command, not a curated list of flags that imply
  non-interactive use — that list would be wrong the first time somebody
  added a flag.

Terminal lifecycle lands before any screen is drawn, because the worst
failure here is not a bad screen but a shell that no longer echoes. `send`
answers a signal with `std::process::exit(130)` and no unwinding, so the
spool file can be deleted by hand; no destructor runs on that path, so
`restore()` lives over an atomic and the handler calls it — before deleting
the spool file, since that one cannot be recovered from by hand. `recv` had
no termination handler at all and now has one. Ctrl-C also had to become an
ordinary key: in raw mode the driver stops turning it into SIGINT.

Three things the first person to use it found, each a design error rather
than a discoverability one:

- `enter` opened a folder while `s` selected one, so the gesture everybody
  makes first did the wrong thing. `enter` now picks whatever is
  highlighted, `→` opens, and choosing the folder you are standing in is the
  first row of the listing rather than a key to know about.
- That was only safe once `esc` meant back. It meant "give up entirely", so
  one mistaken keystroke was unrecoverable. The flow is now a loop whose
  next screen is whichever answer is still missing.
- The code field took any non-empty string and let `TransferCode::parse`
  reject it two screens later, from a program that had already exited. It
  is checked where it is typed, by the same parser.

The sender's announcement says who the code is for and that `drop recv` on
its own now works.

ratatui and crossterm, both pure Rust, which is the bar `flate2` is already
held to for four prebuilt targets. Release binary 26,988,848 → 27,482,824
bytes, +1.8%.

Progress still prints below the closed interface; putting it on screen, and
closing both sides on cancel, is phase 3 of the plan.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.

@op-q
op-q merged commit 75ccddd into feat/no-hosted-relay Sep 10, 2026
4 checks passed
@op-q
op-q deleted the feat/terminal-ui branch September 10, 2026 14:24
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