Skip to content

fix: support Windows by selecting the correct default shell#66

Merged
orhun merged 2 commits into
orhun:mainfrom
ahostbr:feat/windows-support
May 18, 2026
Merged

fix: support Windows by selecting the correct default shell#66
orhun merged 2 commits into
orhun:mainfrom
ahostbr:feat/windows-support

Conversation

@ahostbr

@ahostbr ahostbr commented May 17, 2026

Copy link
Copy Markdown
Contributor

Summary

Fixes #44 — Ratty fails to start on Windows because runtime.rs unconditionally falls back to /bin/sh when no shell is configured and $SHELL is unset:

Error: failed to spawn shell

Caused by:
    CreateProcessW `"/bin/sh\0"` in cwd `Some("C:\Users\Cortez\0")` failed:
    The system cannot find the path specified. (os error 3)

This makes the fallback platform-aware:

  • Windows — prefer %COMSPEC% (the resolved command processor — typically C:\Windows\System32\cmd.exe), fall back to cmd.exe.
  • Other platforms — unchanged, still falls back to /bin/sh.

User-set [shell] program = "..." in ratty.toml and an inherited $SHELL env var (e.g. when launching from Git Bash) still take priority — those paths are untouched.

The change

A single 16-line default_shell() helper in src/runtime.rs, gated with #[cfg(windows)] / #[cfg(not(windows))]. No new dependencies, no public API changes, no behavior change on Linux/macOS.

fn default_shell() -> String {
    #[cfg(windows)]
    { env::var("COMSPEC").unwrap_or_else(|_| "cmd.exe".to_string()) }
    #[cfg(not(windows))]
    { "/bin/sh".to_string() }
}

Verification on Windows 11

Tested on Windows 11 Pro 26200.6457, RTX 5090 (Vulkan backend), Rust 1.x stable.

Before this PR (reproduced cleanly with $env:SHELL=$null in PowerShell):

Error: failed to spawn shell
Caused by:
    CreateProcessW `"/bin/sh\0"` ... (os error 3)

After this PR (same conditions, no config file, no $SHELL):

INFO bevy_render::renderer: AdapterInfo { name: "NVIDIA GeForce RTX 5090", ... backend: Vulkan }
INFO bevy_render::batching::gpu_preprocessing: GPU preprocessing is fully supported on this device.
INFO bevy_winit::system: Creating new window Ratty (0v0)
INFO vello::wgpu_engine: Initialising shader modules in parallel using 22 threads
INFO ratty::model: loaded cursor model from embedded:CairoSpinyMouse.obj (1 mesh parts)

Verified end-to-end:

  • ratty.exe launches without errors with no $SHELL and no config file.
  • cmd.exe is spawned as the PTY child (confirmed via Win32_Process parent-child lookup).
  • ConPTY backend initializes at the configured grid size (104×32).
  • The 3D rat cursor renders correctly via Bevy/Vulkan — RGP cursor model loads from the embedded CairoSpinyMouse.obj.

Checks

Run on Windows from the project root:

  • cargo fmt --all -- --check — passes
  • cargo check — passes
  • cargo clippy --all-targets --all-features -- -D warnings — passes
  • cargo build --release — passes

Ratty unconditionally fell back to `/bin/sh` when no shell was configured
and `$SHELL` was unset, which always fails on Windows:

    Error: failed to spawn shell
    Caused by:
        CreateProcessW `"/bin/sh\0"` ... (os error 3)

Make the fallback platform-aware: on Windows prefer `%COMSPEC%` (the
resolved command processor) and fall back to `cmd.exe`. Other platforms
keep the existing `/bin/sh` fallback.

Verified on Windows 11 (RTX 5090, Vulkan): ratty.exe now launches
out-of-the-box, spawns `cmd.exe` as the PTY child via ConPTY, and the
3D rat cursor renders normally.

Fixes orhun#44
@orhun

orhun commented May 17, 2026

Copy link
Copy Markdown
Owner

Hey, thanks for the PR!

Maybe it also makes sense to default to git-bash exe if it exists on the system? see #44

ahostbr added a commit to ahostbr/ratty that referenced this pull request May 17, 2026
Per @orhun's review on orhun#66: most users running terminal apps on Windows
want a POSIX shell so the Ratatui demos behave the same as on Linux/macOS.

The default-shell lookup now walks, in order:

1. `$SHELL` (already supported — useful when launching from Git Bash so
   the inherited shell is honoured).
2. Git for Windows `bash.exe` in well-known install locations:
   - `%ProgramW6432%\Git\{bin,usr/bin}\bash.exe`
   - `%ProgramFiles%\Git\{bin,usr/bin}\bash.exe`
   - `%ProgramFiles(x86)%\Git\{bin,usr/bin}\bash.exe`
   - `%LOCALAPPDATA%\Programs\Git\{bin,usr/bin}\bash.exe`
3. `bash.exe` anywhere on `PATH` (covers scoop / chocolatey installs).
4. `%COMSPEC%` (typically resolves to `cmd.exe`).
5. Literal `cmd.exe` as the final fallback.

Verified on Windows 11 with Git for Windows installed at the default
location — `bash.exe` is now the spawned PTY child:

    ProcessId   : 204736
    Name        : bash.exe
    CommandLine : "C:\Program Files\Git/bin/bash.exe"

Removing the Git install reverts to the `%COMSPEC%` / `cmd.exe` fallback
verified by the previous commit.
@ahostbr

ahostbr commented May 17, 2026

Copy link
Copy Markdown
Contributor Author

Thanks! Just pushed b521537 on top — default_shell() now walks: $SHELL → Git for Windows bash.exe (env-resolved %ProgramFiles%, %ProgramFiles(x86)%, %ProgramW6432%, %LOCALAPPDATA%\Programs, then PATH) → %COMSPEC%cmd.exe.

Verified on Windows 11 — with Git installed it now spawns "C:\Program Files\Git/bin/bash.exe" as the PTY child; without Git it falls back to cmd.exe (from the earlier commit). Lets me know if you'd prefer a tighter detection (e.g. only %ProgramFiles%\Git\bin\bash.exe) or a different fallback order.

@ahostbr

ahostbr commented May 17, 2026

Copy link
Copy Markdown
Contributor Author

Investigated the RGP examples on Windows next — wanted to flag what I found before scope-creeping this PR.

TL;DR: the path-/payload-handling code in inline.rs / model.rs is fine on Windows. The 3D rat cursor renders correctly via the same code path the examples use. The actual blocker for big_rat / draw / document is ConPTY stripping APC sequences before they reach the host, which means inline::consume_pty_output never sees the \x1b_ratty;…\x1b\ register/place sequences that the widget writes to stdout.

How I confirmed it: wrote a tiny portable-pty-backed probe that spawns a child running printf '\x1b_PROBE;hello=world\x1b\', then dumps every byte received from the PTY master:

=== PROBE RESULT (default ConPTY flags) ===
Total bytes read: 16
0000: 1b 5b 3f 39 30 30 31 68 1b 5b 3f 31 30 30 34 68    .[?9001h.[?1004h

Contains \x1b_ (APC start)?  false
Contains 'PROBE;' marker?    false

Only the bash-init CSIs survive — the APC body and ST terminator are dropped by conhost. (Same outcome when running big_rat inside Ratty with RUST_LOG=warn: the cursor model loads from the embedded GLB, but no registered RGP object / failed to load RGP object line ever appears, because the register APC never makes it to inline::handle_rgp_sequence.)

The fix lives upstream in portable-pty. Microsoft added PSEUDOCONSOLE_PASSTHROUGH_MODE (0x8) for exactly this use case (Windows 11 22H2+ / Windows Server 2025). The flag is even already declared in pty/src/win/psuedocon.rs as #[allow(dead_code)] — it just isn't OR'd into the CreatePseudoConsole call. Until portable-pty exposes it (env-var override or a builder option), Ratty (and any other terminal built on portable-pty) can't see APC traffic on Windows.

Proposed path forward:

  1. Merge this PR as-is — it unblocks Windows for everyone hitting Wont Open on Windows #44 and matches your suggestion to default to Git Bash. RGP support is independent of the shell-default work and shouldn't gate this one.
  2. I'll open a separate portable-pty PR upstream (env-var-gated PSEUDOCONSOLE_PASSTHROUGH_MODE opt-in, default off, no behavior change for existing callers).
  3. Once that's merged + released, a follow-up Ratty PR can bump portable-pty and either default the flag on for Ratty or expose it via ratty.toml.

Happy to file a tracking issue against Ratty in the meantime so this doesn't get lost.

Per @orhun's review on orhun#66: most users running terminal apps on Windows
want a POSIX shell so the Ratatui demos behave the same as on Linux/macOS.

The default-shell lookup now walks, in order:

1. `$SHELL` (already supported — useful when launching from Git Bash so
   the inherited shell is honoured).
2. Git for Windows `bash.exe` in well-known install locations:
   - `%ProgramW6432%\Git\{bin,usr/bin}\bash.exe`
   - `%ProgramFiles%\Git\{bin,usr/bin}\bash.exe`
   - `%ProgramFiles(x86)%\Git\{bin,usr/bin}\bash.exe`
   - `%LOCALAPPDATA%\Programs\Git\{bin,usr/bin}\bash.exe`
3. `bash.exe` anywhere on `PATH` (covers scoop / chocolatey installs).
4. `%COMSPEC%` (typically resolves to `cmd.exe`).
5. Literal `cmd.exe` as the final fallback.

Verified on Windows 11 with Git for Windows installed at the default
location — `bash.exe` is now the spawned PTY child:

    ProcessId   : 204736
    Name        : bash.exe
    CommandLine : "C:\Program Files\Git/bin/bash.exe"

Removing the Git install reverts to the `%COMSPEC%` / `cmd.exe` fallback
verified by the previous commit.
@ahostbr ahostbr force-pushed the feat/windows-support branch from b521537 to fbc8e2d Compare May 17, 2026 23:09
@ahostbr

ahostbr commented May 17, 2026

Copy link
Copy Markdown
Contributor Author

Pushed fbc8e2d (force-update of b521537) — refactored find_git_bash per a code-taste review: the %LOCALAPPDATA%\Programs per-user install was special-cased after the main env-var loop, which is exactly the kind of structural smell that's a one-row-table-extension waiting to happen. Folded everything into a single const CANDIDATES: &[(&str, &str)] table walked once, PATH fallback after. Same behavior, ~10 fewer lines, easier to extend (Scoop / Chocolatey / custom installers = add a row). cargo fmt --check, cargo check, cargo clippy -D warnings all green.


Heads-up on the RGP follow-up — my earlier comment was wrong.

I pulled a vendored portable-pty 0.8.1 source from crates.io, gated the PSEUDOCONSOLE_PASSTHROUGH_MODE flag behind an env var, [patch.crates-io]'d it into a probe, and verified the env var fires, CreatePseudoConsole is called with flags=0xe (0x6 default | 0x8 passthrough), and bash launches correctly. Same probe writes a known APC out of bash (printf '\x1b_PROBE;hello=world\x1b\').

Result: ConPTY still strips APC sequences even with PSEUDOCONSOLE_PASSTHROUGH_MODE enabled on Windows 11 26200.6457.

=== WITH PASSTHROUGH (flags=0xe) ===
PTY master receives 100 bytes:
  ConPTY init + BEGIN-MARK + OSC 0 (title) + END-MARK
  (no \x1b_, no PROBE;)

Bash's own xxd of the exact bytes it printed:
  BEGIN-MARK 1b 5f P R O B E ; h e l l o = w o r l d 1b 5c END-MARK

So bash emits the APC correctly, and ConPTY drops it regardless of the passthrough flag. The #[allow(dead_code)] on the flag in portable-pty is starting to look intentional — the upstream maintainer may already know this. The earlier "submit an upstream PR exposing the flag" plan is moot; the flag doesn't actually solve it.

I'm going to take this back to the drawing board (OSC-wrapped RGP / side-loaded openconsole conpty.dll / how Microsoft Terminal handles inline graphics) and update #67 with the empirical evidence. The shell-default work in this PR is fully independent and ready for your call.

@ahostbr

ahostbr commented May 17, 2026

Copy link
Copy Markdown
Contributor Author

Side-load test: latest microsoft/terminal ConPTY also doesn't pass APC through.

Pulled Microsoft.Windows.Console.ConPTY.1.24.260512001.nupkg from microsoft/terminal release v1.24.11321.0, extracted conpty.dll + OpenConsole.exe, dropped them next to the probe binary. Confirmed portable-pty::load_conpty() picks up the sideloaded conpty.dll (init bytes change from in-box's \x1b[?9001h\x1b[?1004h to sideloaded's \x1b[1t\x1b[c\x1b[?1004h\x1b[?9001h — different ConPTY version observed at the wire). Process tree confirms OpenConsole.exe is spawned by the sideloaded DLL as expected.

Same APC-emitting bash script, both with and without PORTABLE_PTY_CONPTY_PASSTHROUGH=1 set:

=== sideloaded conpty.dll, flags=0x6 ===
Total bytes read: 23
0000: 1b 5b 31 74 1b 5b 63 1b 5b 3f 31 30 30 34 68 1b 5b 3f 39 30 30 31 68    .[1t.[c.[?1004h.[?9001h

=== sideloaded conpty.dll, flags=0xe ===
Total bytes read: 23      (identical bytes)
Contains \x1b_ (APC start)?  false

Cross-checked against an upstream research dive (Microsoft Terminal PR #17510, issue #17313, Ecosystem Roadmap):

  • PSEUDOCONSOLE_PASSTHROUGH_MODE isn't a real public flag. 0x8 in the current winconpty.h is PSEUDOCONSOLE_GLYPH_WIDTH_GRAPHEMES, not passthrough. The #[allow(dead_code)] constant in portable-pty traces back to an old proposal that never shipped.
  • The 1.22 "ConPTY passthrough" work was a synchronisation refactor, not a sequence-forwarding feature. It enabled unrecognised sequences to pass through into conhost's own renderer (which is how Sixel got rendered inside Windows Terminal). It does not forward APC to third-party hosts.
  • APC is consumed by ConPTY's VT parser unconditionally in every shipping version, including the latest sideloadable NuGet. OSC survives (with caveats — Warp tracks an ordering bug in issue #17313). Recognised DCS survives. Everything else is dropped.

So both ConPTY hypotheses are dead:

  1. In-box kernel32.dll ConPTY + flag — flag isn't real.
  2. Side-loaded conpty.dll from latest microsoft/terminal — empirically still strips APC.

This makes OSC-wrapped RGP the only viable path on Windows. It's not pretty, but it's the path that works without waiting for Microsoft to add APC to the passthrough roadmap (which it isn't on). I'll update #67 with the full evidence and start sketching what an OSC wrapping might look like that survives ConPTY's ordering quirk.

The shell-default work in this PR remains fully independent of this rabbit-hole.

@orhun orhun left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM, thanks!

Let's discuss the proper windows support in another issue

@orhun orhun changed the title fix: default to cmd.exe / %COMSPEC% on Windows fix: support Windows by selecting the correct default shell May 18, 2026
@orhun orhun merged commit 551db6a into orhun:main May 18, 2026
11 checks passed
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.

Wont Open on Windows

3 participants