feat(windows): make Windows a first-class target through platform seams - #12
Merged
Merged
Conversation
Windows supports AF_UNIX SOCK_STREAM but std does not expose it. Route the six daemon files through a single transport module that re-exports std::os::unix::net on Unix and uds_windows on Windows. See docs/internal/windows-port/pr-01-transport.md.
Replace libc::flock with std::fs::File::try_lock/unlock, which maps to the same syscalls on both platforms. Update the libc dependency comment and the lock_tests contract tests in the same commit. See docs/internal/windows-port/pr-02-instance-lock-std.md.
Add Windows creation flags (DETACHED_PROCESS | CREATE_NEW_PROCESS_GROUP) and split the shutdown watch per platform: signal_hook on Unix, ctrlc with sync_channel on Windows. Move signal-hook to Unix-only deps and add ctrlc for Windows. Wrap signal delivery tests in #[cfg(unix)]. See docs/internal/windows-port/pr-03-detach-signals.md.
Extract restrict_to_owner helper with #[cfg(unix)] using from_mode(0o600) and #[cfg(windows)] as a documented no-op depending on the profile directory's inherited ACL. Decision C requires the comment to name what it depends on and when that dependency breaks. See docs/internal/windows-port/pr-04-permissions.md.
Route recovery plugin socket types through a platform seam, fix the HOME environment variable to use dirs::home_dir(), restrict ipc paths per platform capability, and name the AF_UNIX 107-byte path length limit when bind refuses. See docs/internal/windows-port/pr-05-recovery-plugin.md.
…-integration # Conflicts: # Cargo.toml
…ntegration # Conflicts: # src/daemon/socket.rs
Reorder imports across daemon files to satisfy rustfmt after the transport seam merge.
Windows has no SIGTERM equivalent; the variant is only constructed on Unix. Annotate it rather than removing it — the nightcrow stop protocol path may use it on both platforms.
Route socket types through the platform seam in test files, gate permission-mode assertions and Unix-only fixtures with #[cfg(unix)], add junction-based equivalents for symlink tests on Windows, and split the executable fixture per platform. Watcher filter verified stable across 5 runs on Windows. See docs/internal/windows-port/pr-06-tests.md.
Promote shell to a config section. Unix defaults to \ or /bin/sh with -lc; Windows defaults to %ComSpec% or cmd.exe with /C. The command text stays a single argv item so the shell handles quoting. No Git Bash auto-detection (decision B). See docs/internal/windows-port/pr-07-shell.md.
Add a Stop subcommand that sends a Shutdown message over the daemon socket. The signal path and socket path converge on one channel so both run the same graceful shutdown. No force kill — the point is to reap child shells (decision A). See docs/internal/windows-port/pr-08-stop.md.
Start without bracketed paste when the console lacks it, read the local time zone on Windows via windows-sys, strip verbatim \\?\ path prefixes for display, and resolve .exe/.cmd/.bat extensions in the plugin registry. See docs/internal/windows-port/pr-09-display-defects.md.
Add windows-latest to the OS matrix and set fail-fast: false so one OS failing does not hide another's results. This is the first time the MSVC toolchain is exercised — git2 vendored OpenSSL may need a follow-up if it breaks. See docs/internal/windows-port/pr-10-ci.md.
PTY and terminal tests that spawn real shells and assert on Unix sh -lc output are meaningless on Windows where the default shell is cmd.exe. Gate them with #[cfg(unix)]. Fix path_complete and path_tree to handle Windows path separators.
Fix is_executable PATHEXT comparison (extension has no dot, PATHEXT entries do), normalize backslashes to forward slashes in for_display on Windows, normalize home directory paths before strip_prefix in display_path and home_relative_path, strip verbatim prefix in path test fixtures, and document missing leader commands in README.
…omparison Gate RELAUNCH_MARKER and PTY_TEST_DEADLINE with #[cfg(unix)] since the tests using them are Unix-only. Replace manual char comparison closures with array patterns per clippy::manual_pattern_char_comparison.
The path tree normalizes separators to forward slashes for display, so the assertions must expect forward slashes regardless of platform.
The status_line and helper tests spawn real shells (echo, printf, cat, sleep) and assert on their output. On Windows the default shell is cmd.exe, so these tests are meaningless. Gate them with #[cfg(unix)]. Gate the ENOUGH/BRIEF/Instant constants they use as well.
The signal_delivery module is nested under the tests module, so super::ShutdownWatch does not resolve. Import from crate::platform::signals instead. Also add .opencode/ to .gitignore.
docker-compose.yml runs the four AGENTS.md gates (fmt, clippy, build, test) inside a rust:latest container with named-volume caches for cargo registry and target. Windows developers can catch Unix regressions locally before pushing, without waiting for CI. Document the workflow in AGENTS.md and verification.md. Note the known flaky reattach test that fails in Docker but passes in CI.
…ose.yml Move the Docker unix-gate instructions and build/test prerequisites from AGENTS.md and verification.md into docs/getting-started.md under a 'Building and testing' section. Rename docker-compose.yml to compose.yml per the modern Compose convention. AGENTS.md and verification.md now point to the getting-started section instead of duplicating the content.
std::fs::canonicalize on Windows prepends \\?\ to the result. cmd.exe treats that as a UNC path and falls back to C:\Windows, so the terminal pane opened in the wrong directory. Add canonicalize_clean() that strips the prefix while preserving native separators, and use it in resolve_repo_path and spawn_pane.
The directory browser now prepends . and .. rows at the root level so the user can pick the current directory or step up without leaving the browser, matching every file manager. They are depth-0 only to avoid clutter and infinite recursion; expand skips them, and selected_path skips the . component so root/.// collapses to root/. Tab no longer escalates to the browser on a second press — it only completes the path. The browser opens with the down arrow alone, so repeated Tab presses never leave the field.
On Windows the reader thread never saw EOF, so a pane whose shell exited stayed open. portable-pty keeps the pseudoconsole alive for as long as the master is held, and ClosePseudoConsole — which releases the output pipe's write end — only runs when that master drops. The wait thread already knew the child was gone but threw the result away. It now reports ChildExited, and an ExitPhase state machine holds the exit back until the channel is dry and a short grace has passed, so output the host is still copying is not cut off by the caller destroying the pane. Both new tests answer the pseudoconsole's cursor query first: it is created with INHERIT_CURSOR, so the child does not run at all until a DSR reply arrives.
The caret sat still. Driven by our own clock rather than SLOW_BLINK, which Windows conhost and others ignore outright — the event loop already redraws every 16 ms whether or not an event arrived, so a phase read per frame is all the animation needs. The dark half draws a blank so the row does not shift as it blinks.
`-d` was ignored next to `attach`, so the one-command way in did not exist: the session had to be started and attached to separately. It now probes the socket, starts a background session if nothing answers, waits for it to accept, and attaches. Probing by connecting rather than by testing the path, because a daemon that died leaves the socket file behind on Unix. The background copy also drops `attach` from its arguments — it is always the daemon, never a client.
The browser mixed two metaphors: `→` expanded a tree in place while `.` and `..` offered file-manager navigation as list rows. Enter could not serve both — on `..` it answered with `root/../` and closed the browser, which is the opposite of what reaching for `..` means. Rows are answers, keys are verbs. `←` on a collapsed depth-0 row already re-roots to the parent and selects the directory just left, so the way out was never missing, only duplicated as a row. Enter now means the same thing everywhere. Adds the workflow test the moving root exists for: browsing from one checkout to a sibling without leaving the browser.
…st key Typing a letter over the prefilled repo path cleared the whole buffer. The prefill exists to supply a shared prefix — a sibling checkout is the common case — so throwing it away on the first keystroke discards exactly what it was there for, with nothing on screen to undo it. Typing now extends. That leaves nothing reading `RepoInput::prefilled`, so the flag goes, and with it `repo_input_accept_prefill` and its →/End binding, which existed only to leave prefill mode. Discarding a path is Esc or Backspace.
nightcrow already builds and tests on macOS, Linux, and Windows in CI, but nothing said so as a requirement — getting-started only said "Unix and Windows", and no rule told an author where a platform branch belongs. A cfg gate added in the wrong place is cheap to write and expensive to find. Names the three platforms, points at the existing seams (`src/platform/`, `src/daemon/transport.rs`), and says that gating a test off a platform costs its coverage there and needs a reason.
The one-command way in is now backgrounding the session and attaching to it, so the quick start opens with that rather than with the foreground run — the separate pieces (`-d`, `attach`, foreground, `stop`) follow it for the cases that want them apart. Also says out loud what the code has targeted for a while: macOS, Linux, and Windows, one binary and the same surfaces on all three.
Every hint-bar string already reads `q: detach` and the attach loop logs "nightcrow detached" when it returns, but the keybinding tables still said "Quit" — from before the TUI became a client of a session it does not own. There is no path where the chord ends the daemon; `nightcrow stop` is that. Left `q`=Quit in architecture/ui.md alone: that one names the Action variant, not the label.
`Prefix` holds a root twice — as given and as the filesystem reports it — so a path that arrives spelled differently can still be placed against the tree. On Windows the second spelling was `canonicalize`'s verbatim (`\?\`) form, which nothing in the process ever produces: not libgit2, not the watcher. It was a prefix of nothing, leaving the literal spelling to carry the comparison alone. Where the two disagree — an 8.3 short name in the path against the long name the filesystem reports — nothing places, and a repository whose `.git` sits inside its own tree reads as having an external git dir. `canonicalize_clean` is the form the rest of the tree already compares against.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Makes nightcrow run on Windows as a first-class target, alongside macOS and
Linux, and CI now gates all three.
The port is done through platform seams rather than scattered
cfgblocks:one module owns the platform difference and the rest of the tree calls the
seam. Where a capability simply does not exist on one platform, the code says
so at the seam instead of pretending.
Platform seams
transportseam (UnixStream/UnixListenershims) so the daemon,attach, andstopall speak one API.Socket restriction is applied per platform capability rather than assuming
Unix file modes.
setsidon Unix,DETACHED_PROCESS | CREATE_NEW_PROCESS_GROUPon Windows.
std::fs::Fileinstead of a Unix-onlyprimitive.
ShutdownWatchcovers both;Shutdown::Terminateisdead-code-allowed on Windows rather than removed, so the enum stays whole.
Unix-only where they need a real POSIX shell.
What came with it
[shell]config section with per-platform defaults, so the pane shell is nothardcoded to a Unix one.
nightcrow stop— asks a running daemon to shut down over its socket,running the same graceful sequence as SIGINT/SIGTERM.
nightcrow -d attach— starts a session if none is running, then attaches.An already-running session is attached to as-is.
browser drops
./..for a moving root.\\?\) prefixes stripped from canonicalized paths;path-separator assumptions removed from tests.
Verification
windows-latest, so all four gates (build,test,clippy -D warnings,fmt --check) run on macOS, Linux, and Windows.compose.ymladds aunix-gateservice to run the Linux gates from aWindows machine locally, before the push-and-wait cycle.
pre-commitrunsfmt --checkonly,pre-pushruns the fullCI-equivalent gate scoped to what changed vs
upstream/dev.One known flaky test in containers —
a_reattaching_client_makes_an_alternate_screen_program_draw_againcan fail onPTY timing under Docker load. It passes on
devand onubuntu-latest.Docs
docs/getting-started.mdgains a "Verifying on the other platform" section(the
cfggates mean a green local build is not proof the others are green),and the README states the three-platform target and leads with
nightcrow -d attach.