Skip to content

Add a box command to the host terminal, with a host/box chooser - #236

Open
kacperpaczos wants to merge 2 commits into
Dvlv:masterfrom
kacperpaczos:feat/command-dispatcher
Open

Add a box command to the host terminal, with a host/box chooser#236
kacperpaczos wants to merge 2 commits into
Dvlv:masterfrom
kacperpaczos:feat/command-dispatcher

Conversation

@kacperpaczos

Copy link
Copy Markdown
Contributor

Fixes #235

Adds an Add Command… button to the Exported Binaries section, so a command from a box can be made available in the host terminal - and when the host already has one under that name, the two can coexist behind a chooser instead of one shadowing the other.

The flow

  1. The command is resolved in the box first (command -v inside it). Not there → a "Not Found" popup, nothing is written.
  2. Nothing on the host answers to that name → plain distrobox-export --bin. Unchanged distrobox behaviour, no new concepts, and the new row appears in the list straight away.
  3. Something does answer → a dialog says what was found (host paths as-is, in box <name> for an existing export) and offers Replace With Chooser. On confirm the targets are merged - an existing chooser's boxes, the box of a distrobox wrapper, and the box you are in - and one chooser is written.

The chooser

A self-contained bash script in ~/.local/bin, generated from one template so every one of them has the same shape:

Run claude with:
  1: host (/usr/bin/claude)
  2: mybox
Run claude from [1]: 
  • The menu goes to stderr and the answer is read from /dev/tty, so it never consumes the program's own stdin.
  • With no terminal (a pipe, a script, a .desktop launcher) it does not prompt - it runs the first target. A prompt there would hang every script that calls the command.
  • BOXBUDDY_DISPATCH=host|<box> picks a target outright, for scripts that want a specific one.
  • Adding the same command from a second box folds that box in rather than replacing the file.

Nothing else knows about it: the file is the entire state. No config, no database, and it keeps working after BoxBuddy is uninstalled.

Staying visible to distrobox

distrobox-export keeps no registry - the state is two comment lines in the file it writes, # distrobox_binary and # name: <container>. list_exported_binaries greps for the first and filters on the second, and --delete refuses with "is not exported" when the first is absent. A chooser overwriting an exported wrapper would therefore make distrobox lose sight of the command silently.

So the chooser carries those two markers (one # name: line per target box) next to BoxBuddy's own. --list-binaries from any target box still finds it, --delete still removes it, and the two tools agree. This is the one thing in the PR I would flag for a veto - it is BoxBuddy writing distrobox's markers into a file distrobox did not generate. The alternative is that the command quietly disappears from --list-binaries; I thought agreement was the lesser evil, but it is your call. As a consequence the binaries list skips entries that are choosers, so a command is not listed twice.

Screenshots

Virtual display, stub distrobox, isolated $HOME - a fake claude on the host and claude in the box.

dialog conflict

The chooser row afterwards, and the same dialog when what is in the way is an earlier distrobox export:

row wrapper

Testing

cargo test: 18 pass (12 new), covering the script/marker round-trip, the command-name validation, bash -n on every shape of generated script, the real distrobox-export wrapper text parsed back to its container name, and a behavioural test that runs a generated chooser against a stub distrobox and asserts what it exec'd.

End to end on a virtual display with an isolated $HOME, a stub distrobox and a fake host binary, driving the real UI: plain export, conflict → chooser, the chooser listed after reopening the window, and re-adding a command that is already a distrobox export. The generated file was then run for real - through a pty for the menu (choice, empty input, out-of-range), through a pipe for the no-terminal path, and with BOXBUDDY_DISPATCH set to a box, to host, and to a bad value (exit 2).

Two limits I did not paper over: a box literally named host would be shadowed by the host entry, and a host path containing whitespace cannot round-trip through the marker line (runtime quoting is unaffected). Both are noted in the code.

The Flatpak path goes through the existing run_command/get_command_output helpers, so the host is reached with flatpak-spawn --host; I could only test the native path here.

BoxBuddy could export an application to the host menu, but not a command
to the host terminal, and a command that exists both on the host and in a
box could only shadow one with the other.

The Exported Binaries section gains an "Add Command…" button. The command
is resolved inside the box first; if nothing on the host answers to that
name it is a plain `distrobox-export --bin`, exactly as before. When the
host already has one - its own binary, an earlier distrobox export, or a
chooser BoxBuddy wrote before - the dialog offers to replace it with a
chooser: a small self-contained bash script in ~/.local/bin that asks, at
invocation time, which one to run.

The chooser prints a numbered menu on stderr and reads the answer from
/dev/tty, so it never eats the program's own stdin. Scripts and pipes must
not block on a prompt, so with no terminal it runs the first target
silently, and BOXBUDDY_DISPATCH=host|<box> picks one outright. Adding the
same command from another box folds that box into the existing chooser
rather than replacing it.

The file carries distrobox's own `# distrobox_binary` and `# name: <box>`
markers next to BoxBuddy's, so `distrobox-export --list-binaries` still
finds the command and `--delete` still removes it; without them distrobox
would quietly lose sight of a command it had exported. The binaries list
skips entries that are choosers so they are not listed twice.

Nothing else knows about the chooser: the file is the whole state, and it
keeps working if BoxBuddy is uninstalled.
`make lint` runs clippy, and the strings added here tripped
needless_borrows_for_generic_args - gettext takes anything that converts
into a String, so the format! result can go in as it is. Only the lines
this branch introduced are touched; the same pattern elsewhere in the file
predates it and is left alone.
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.

No way to add a command from a box to the host terminal, and no way to keep both

1 participant