Skip to content

Platform-aware default shell + Windows recording input fixes#199

Merged
jrozner merged 3 commits into
mainfrom
worktree-default-shell
Jun 18, 2026
Merged

Platform-aware default shell + Windows recording input fixes#199
jrozner merged 3 commits into
mainfrom
worktree-default-shell

Conversation

@jrozner

@jrozner jrozner commented Jun 18, 2026

Copy link
Copy Markdown
Member

Summary

  • Platform-aware default recording shell. $SHELL (→ /bin/sh) on Unix, PowerShell on Windows. Previously Config::with_defaults() read $SHELL on every platform, so on Windows it seeded an empty shell and fell back to /bin/sh, which doesn't exist there. Logic is centralized in config::default_shell() and shared by with_defaults() and the menu's runtime fallback; the unused per-platform default_shell() helpers in the recorder are removed.
  • Windows: forward special keys to the child. The recorder relied only on crossterm raw mode, which clears the cooked-input flags but never sets ENABLE_VIRTUAL_TERMINAL_INPUT — so arrow keys / Ctrl-R were never encoded as the VT sequences PowerShell + PSReadLine need. New TerminalModeGuard enables VT input + VT output processing for the recording and restores the exact prior console modes on drop.
  • Windows: stop the orphaned stdin thread. StdinPoller spawned a thread blocked in a console read and never stopped it, so after recording it stole the post-recording menu's first keystroke. It now cancels the blocking read (CancelSynchronousIo) and joins on drop.
  • forbid(unsafe_code)deny(unsafe_code). The Windows console APIs have no safe-wrapper equivalent (the way rustix covers the Unix syscalls), so the crate-wide invariant is relaxed to deny with a single scoped, documented #[allow(unsafe_code)] on recorder::windows alone. Each FFI call has a SAFETY comment.
  • Env forwarding into the child stays the verbatim environment — TERM/SHELL pass through when set and are absent otherwise (native Windows defines neither).

Testing

  • cargo test — 164 pass.
  • cargo clippy --all-targets and cargo clippy --target x86_64-pc-windows-gnu — clean.
  • ⚠️ The Windows code is compile-/clippy-verified via cross-check only. Runtime behavior on Windows (especially the CancelSynchronousIo teardown timing) still needs on-device validation — Windows can't be run in this environment.

Notes

  • The "Cannot load PSReadLine module" message users may see is most likely environmental (execution policy, PowerShell edition, module install) rather than something the recorder controls. If PSReadLine genuinely fails to load, Ctrl-R reverse search won't work even with correct input forwarding, since that's a PSReadLine feature.

@jrozner jrozner requested a review from jkennedyvz as a code owner June 18, 2026 16:52
@jrozner jrozner force-pushed the worktree-default-shell branch from 10d5970 to 73d93dc Compare June 18, 2026 20:29
The Windows recorder needs a handful of console FFI calls (VT mode setup and
waiting on the console input handle) that have no safe-wrapper equivalent the
way `rustix` covers the Unix syscalls. `#![forbid(unsafe_code)]` cannot be
overridden locally, so relax it to `#![deny(unsafe_code)]`: the no-unsafe
default still holds crate-wide, but a module can opt in with a scoped, documented
`#[allow(unsafe_code)]`. The Windows console module is the sole opt-in.
@jrozner jrozner force-pushed the worktree-default-shell branch from 73d93dc to 45ccc26 Compare June 18, 2026 20:38
jrozner added 2 commits June 18, 2026 13:40
`recorder::unix::default_shell` and `recorder::windows::default_shell` were
public but unused: the recorder always receives an explicit shell from the menu,
and the platform-appropriate default now lives in `config::default_shell`. Drop
them so there is a single source of truth for the default shell.
Default recording shell is now platform-aware (config::default_shell), used by
both Config::with_defaults() and the menu's runtime fallback:

  * Unix: $SHELL, falling back to /bin/sh.
  * Windows: pwsh.exe when PowerShell 7 is on PATH, else powershell.exe.
    Previously with_defaults() read $SHELL on every platform, so Windows got an
    empty default and fell back to /bin/sh, which does not exist there.

pwsh is preferred because aterm forwards its environment to the child: launched
from pwsh, the inherited PSModulePath points at PowerShell 7's modules, and
handing that to a Windows PowerShell 5.1 child makes it load PS7 module versions
incompatible with 5.1 — PSReadLine and even core modules fail to load ("Cannot
load PSReadline module"). Launching pwsh keeps the shell and its PSModulePath
consistent.

Windows console handling for the recording (no-op on Unix):

  * TerminalModeGuard enables ENABLE_VIRTUAL_TERMINAL_INPUT (and clears the
    cooked-input flags) so special keys — arrows, Home/End, function keys — are
    captured as VT escape sequences and forwarded to the child; ReadConsoleW
    otherwise returns only typed characters and drops them. It also enables
    ENABLE_VIRTUAL_TERMINAL_PROCESSING + DISABLE_NEWLINE_AUTO_RETURN so the
    child's VT output (colours, cursor motion) renders instead of printing escape
    bytes. The exact prior console modes are restored on drop.

  * StdinPoller's reader thread gates each blocking console read behind a 50ms
    WaitForSingleObject on the console input handle, so it re-checks its stop
    flag and exits on its own at teardown. This keeps the thread from outliving
    the recording and stealing the post-recording menu's first keystroke, and
    avoids an un-interruptible read that would otherwise hang the menu until a
    keypress.

The Windows console FFI opts into unsafe via a scoped, documented
#[allow(unsafe_code)] on recorder::windows (the crate is otherwise
deny(unsafe_code)); each call has a SAFETY comment. Adds windows-sys as a
cfg(windows) dependency.

Verified: 164 tests pass; clippy clean on the native and x86_64-pc-windows-gnu
targets. Windows runtime behavior confirmed by the reporter (PSReadLine loads,
special keys work, exit drops straight to the menu).
@jrozner jrozner force-pushed the worktree-default-shell branch from 45ccc26 to 76060c7 Compare June 18, 2026 20:41
@jrozner jrozner merged commit 4b974c8 into main Jun 18, 2026
10 checks passed
@jrozner jrozner deleted the worktree-default-shell branch June 18, 2026 22:39
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