client: run a keymap command in the background without a transient session #224

Closed
opened 2026-10-21 20:03:21 +09:00 by natsukium · 0 comments
Owner

Problem

Opening a new felis window, or a second window on the current session, from a chord today takes a binding like:

[keymap."super+n"]
command = ["/usr/bin/open", "-n", "/nix/store/<hash>-felis-0.3.0/Applications/felis.app"]
kind = "run"
  • Blocking. run always spawns its argv as a transient session over the live grid (start_run → enter_transient in crates/felis-client/src/app_methods.rs), and the window comes back only when the command exits. A window launch (felis) execs felis-client and returns only when that new window closes, so ["felis"] would cover the origin window for the new window's whole lifetime. open -n is the workaround because it returns at once.
  • cwd lost. open goes through LaunchServices, so the new window lands in $HOME. Yet run already starts in the origin session's OSC 7 directory, and a window launch already starts its first session in the launch cwd (launch_cwd in crates/felis-client-core/src/dial.rs).
  • Upgrade-fragile path. The store path changes on every upgrade. (PATH resolution is out of scope here; see below.)

A dedicated new_window / duplicate_window action is not on the table (docs/explanation/non-goals.md "Window-management chords").

Proposal: background = true on run

docs/explanation/input.md "Why run earns a keybind rather than a WM hotkey" justifies run by either (a) the grid-sized transient a redraw-style TUI needs, or (b) the focused session's context (id, cwd) a WM hotkey does not know. Today every run pays for (a) even when it needs only (b).

Add an optional background field to run (default false; the current behavior is unchanged).

  • No PTY and no transient session. The client spawns the argv directly, detached (its own process group/session, stdio to null), and only reaps it. A spawn failure or non-zero exit is logged. "A daemon spawns, not the client" (input.md "Which daemon runs the command") rests on "the client is a wgpu window, not a PTY host", which a PTY-less spawn does not need.
  • Same context as a foreground run: cwd is the origin session's OSC 7 directory when it names a local one, with FELIS_ORIGIN_SESSION_ID, FELIS_HOST, FELIS_CWD exported. The environment is scrubbed of inherited origin variables first (#223).
  • cwd fallback differs, and is documented. A foreground transient with no usable OSC 7 inherits the local daemon's cwd. A background spawn has no daemon, so it uses the home directory, the fallback resolve_launch_cwd applies to an unusable launch cwd. The client's own cwd is not used: a Dock-launched client sits in /.
  • Gating is identical to a foreground run. It is refused while is_busy() (a session landing or a pending switch fetch), because the current session id and the shadow screen's OSC 7 may still describe the departing session. It is also refused under refuse_nested_visit.
  • No argv placeholder expansion. sh -c plus the env vars is the escape hatch, staying clear of a mini expression language (principle 1).

Resulting bindings (default-local recipes):

"super+n"       = { kind = "run", background = true, command = ["/run/current-system/sw/bin/felis"] }
"super+shift+n" = { kind = "run", background = true, command = ["sh", "-c", "exec /run/current-system/sw/bin/felis attach \"$FELIS_ORIGIN_SESSION_ID\""] }

In a --host window, felis attach "$FELIS_ORIGIN_SESSION_ID" dials the local daemon, not the remote one; a script can branch on FELIS_HOST. Bare felis inherits no --socket / --config. The reference states both limits.

Out of scope

  • PATH. felis does not change how argv[0] resolves. A stable profile path (/run/current-system/sw/bin/felis, an absolute ~/.nix-profile/bin/felis, /opt/homebrew/bin/felis) avoids the store hash, and the home-manager module can write it. A general command_path key with client-side argv[0] resolution was rejected: felis would re-implement the OS executable lookup (execvp semantics including executable checks and relative PATH entries; CreateProcessW searching the parent's PATH on Windows). Login-shell environment capture was rejected because it is not portable and runs shell startup code, against REQ-012 on reload.
  • Resolving the felis CLI itself without PATH: #225.
  • Inheriting the window's carrier and config (--host / --socket / --config).

Cascade

add-config-key flow (the BindingValue::Run field, just schema, the home-manager module), docs/reference/keybindings.md (run row, the background semantics, the cwd fallback, the default-local caveat), docs/explanation/input.md (the (a)/(b) split, the client-spawn exception to "Which daemon runs the command", a note under the run Revisit if: background mode makes unrelated commands easier to bind, while bindings that use the session's id/cwd still pass (b)), skills/felis if it documents run, CHANGELOG (Added).

Depends on #223.

## Problem Opening a new felis window, or a second window on the current session, from a chord today takes a binding like: ```toml [keymap."super+n"] command = ["/usr/bin/open", "-n", "/nix/store/<hash>-felis-0.3.0/Applications/felis.app"] kind = "run" ``` - **Blocking.** `run` always spawns its argv as a transient session over the live grid (`start_run` → `enter_transient` in `crates/felis-client/src/app_methods.rs`), and the window comes back only when the command exits. A window launch (`felis`) execs `felis-client` and returns only when that new window closes, so `["felis"]` would cover the origin window for the new window's whole lifetime. `open -n` is the workaround because it returns at once. - **cwd lost.** `open` goes through LaunchServices, so the new window lands in `$HOME`. Yet `run` already starts in the origin session's `OSC 7` directory, and a window launch already starts its first session in the launch cwd (`launch_cwd` in `crates/felis-client-core/src/dial.rs`). - **Upgrade-fragile path.** The store path changes on every upgrade. (PATH resolution is out of scope here; see below.) A dedicated `new_window` / `duplicate_window` action is not on the table (`docs/explanation/non-goals.md` "Window-management chords"). ## Proposal: `background = true` on `run` `docs/explanation/input.md` "Why `run` earns a keybind rather than a WM hotkey" justifies `run` by either (a) the grid-sized transient a redraw-style TUI needs, or (b) the focused session's context (id, cwd) a WM hotkey does not know. Today every `run` pays for (a) even when it needs only (b). Add an optional `background` field to `run` (default `false`; the current behavior is unchanged). - No PTY and no transient session. The **client** spawns the argv directly, detached (its own process group/session, stdio to null), and only reaps it. A spawn failure or non-zero exit is logged. "A daemon spawns, not the client" (`input.md` "Which daemon runs the command") rests on "the client is a wgpu window, not a PTY host", which a PTY-less spawn does not need. - Same context as a foreground `run`: cwd is the origin session's `OSC 7` directory when it names a local one, with `FELIS_ORIGIN_SESSION_ID`, `FELIS_HOST`, `FELIS_CWD` exported. The environment is scrubbed of inherited origin variables first (#223). - **cwd fallback differs, and is documented.** A foreground transient with no usable `OSC 7` inherits the local daemon's cwd. A background spawn has no daemon, so it uses the home directory, the fallback `resolve_launch_cwd` applies to an unusable launch cwd. The client's own cwd is not used: a Dock-launched client sits in `/`. - **Gating is identical to a foreground `run`.** It is refused while `is_busy()` (a session landing or a pending switch fetch), because the current session id and the shadow screen's `OSC 7` may still describe the departing session. It is also refused under `refuse_nested_visit`. - No argv placeholder expansion. `sh -c` plus the env vars is the escape hatch, staying clear of a mini expression language (principle 1). Resulting bindings (**default-local recipes**): ```toml "super+n" = { kind = "run", background = true, command = ["/run/current-system/sw/bin/felis"] } "super+shift+n" = { kind = "run", background = true, command = ["sh", "-c", "exec /run/current-system/sw/bin/felis attach \"$FELIS_ORIGIN_SESSION_ID\""] } ``` In a `--host` window, `felis attach "$FELIS_ORIGIN_SESSION_ID"` dials the local daemon, not the remote one; a script can branch on `FELIS_HOST`. Bare `felis` inherits no `--socket` / `--config`. The reference states both limits. ## Out of scope - **PATH.** felis does not change how argv[0] resolves. A stable profile path (`/run/current-system/sw/bin/felis`, an absolute `~/.nix-profile/bin/felis`, `/opt/homebrew/bin/felis`) avoids the store hash, and the home-manager module can write it. A general `command_path` key with client-side argv[0] resolution was rejected: felis would re-implement the OS executable lookup (`execvp` semantics including executable checks and relative PATH entries; `CreateProcessW` searching the parent's PATH on Windows). Login-shell environment capture was rejected because it is not portable and runs shell startup code, against REQ-012 on reload. - Resolving the felis CLI itself without PATH: #225. - Inheriting the window's carrier and config (`--host` / `--socket` / `--config`). ## Cascade `add-config-key` flow (the `BindingValue::Run` field, `just schema`, the home-manager module), `docs/reference/keybindings.md` (`run` row, the background semantics, the cwd fallback, the default-local caveat), `docs/explanation/input.md` (the (a)/(b) split, the client-spawn exception to "Which daemon runs the command", a note under the `run` _Revisit if_: background mode makes unrelated commands easier to bind, while bindings that use the session's id/cwd still pass (b)), `skills/felis` if it documents `run`, CHANGELOG (Added). Depends on #223.
Sign in to join this conversation.
No milestone
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
felis-terminal/felis#224
No description provided.