Skip to content

Commit 8b07eea

Browse files
committed
feat: Windows portability — cross-platform RNG, HOME lookup, browser open (#11)
- ident.rs: mint Stable-Commit-Ids from `getrandom` (OS entropy on every platform — getrandom syscall / BCryptGenRandom) instead of reading `/dev/urandom`; the time/pid/counter fallback stays as a last resort. - commands.rs: `home_dir()` now falls back to `USERPROFILE` when `HOME` is unset (Windows); all HOME lookups go through it. The man-page setup step is gated to non-Windows (`man` doesn't exist there); completion already no-ops when the shell can't be detected. - view.rs: `open_url` gains a Windows arm (`cmd /C start`) alongside macOS (`open`) and other-unix (`xdg-open`). - CI: a `windows-latest` job builds and runs the portable unit tests (ident/render/engine; the git-driven integration + view tests assume a POSIX shell and are skipped there). Verified `cargo check --target x86_64-pc-windows-msvc --all-targets` compiles cleanly (lib, bin, and tests). `make_executable` was already cfg(unix)-gated.
1 parent d938010 commit 8b07eea

6 files changed

Lines changed: 95 additions & 25 deletions

File tree

.github/workflows/ci.yml

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,3 +44,19 @@ jobs:
4444
# they aren't covered here. The rest run against throwaway local repos.
4545
- name: Test
4646
run: cargo test --verbose
47+
48+
# Guard Windows portability: the code must compile and the pure unit tests
49+
# (ident / render / engine) must pass. The git-driven integration and view
50+
# tests assume a POSIX shell and aren't run here yet.
51+
windows:
52+
name: Windows
53+
runs-on: windows-latest
54+
steps:
55+
- uses: actions/checkout@v4
56+
- name: Install Rust
57+
uses: dtolnay/rust-toolchain@stable
58+
- uses: Swatinem/rust-cache@v2
59+
- name: Build
60+
run: cargo build --verbose
61+
- name: Test (portable unit tests)
62+
run: cargo test --lib -- --skip view

Cargo.lock

Lines changed: 36 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,9 @@ anyhow = "1"
2222
clap = { version = "4", features = ["derive"] }
2323
clap_complete = "4"
2424
clap_mangen = "0.2"
25+
# Cross-platform OS entropy for minting Stable-Commit-Ids (a thin wrapper over
26+
# each platform's RNG — getrandom syscall / BCryptGenRandom — no C deps).
27+
getrandom = "0.3"
2528
# The interactive `git queue tui` editor (ADR-0001). By far the project's
2629
# largest dependency; the domain logic lives in the headless `engine`, so the
2730
# TUI stack is confined to the thin `view` shell. crossterm is re-exported via

src/commands.rs

Lines changed: 15 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -77,8 +77,11 @@ so reviewers see which PR merges next)",
7777
}
7878
// The next three steps are user-scoped (not repo-local), so they only run
7979
// interactively — never under a non-interactive `--yes`.
80-
// 3. Man page.
81-
if tty && confirm("Install the man page? (`man git-queue`, and `git queue --help`)", true) {
80+
// 3. Man page (Unix only — Windows has no `man`).
81+
if tty
82+
&& !cfg!(windows)
83+
&& confirm("Install the man page? (`man git-queue`, and `git queue --help`)", true)
84+
{
8285
if let Err(e) = install_man_pages() {
8386
eprintln!("note: could not install the man page: {e:#}");
8487
}
@@ -114,8 +117,8 @@ so reviewers see which PR merges next)",
114117
}
115118
}
116119
// 6. Claude Code skill (only when Claude Code is present; interactive only).
117-
let home = std::env::var("HOME").unwrap_or_default();
118-
let claude_dir = std::path::Path::new(&home).join(".claude");
120+
let home = home_dir();
121+
let claude_dir = home.join(".claude");
119122
if tty
120123
&& claude_dir.exists()
121124
&& confirm(
@@ -138,7 +141,7 @@ git-queue correctly?",
138141
("copilot", ".config/github-copilot"),
139142
]
140143
.iter()
141-
.filter(|(_, d)| std::path::Path::new(&home).join(d).exists())
144+
.filter(|(_, d)| home.join(d).exists())
142145
.map(|(n, _)| *n)
143146
.collect();
144147
if tty
@@ -176,8 +179,7 @@ fn setup_undo() -> Result<()> {
176179
let _ = git::ok(&["config", "--global", "--unset", "alias.q"]);
177180
println!("Removed the `git q` alias.");
178181
}
179-
let home = std::env::var("HOME").unwrap_or_default();
180-
let skill = std::path::Path::new(&home).join(".claude/skills/using-git-queue/SKILL.md");
182+
let skill = home_dir().join(".claude/skills/using-git-queue/SKILL.md");
181183
if skill.exists() {
182184
std::fs::remove_file(&skill).ok();
183185
println!("Removed {}.", skill.display());
@@ -186,8 +188,13 @@ fn setup_undo() -> Result<()> {
186188
Ok(())
187189
}
188190

191+
/// The user's home directory, portably: `HOME` on unix, `USERPROFILE` on
192+
/// Windows.
189193
fn home_dir() -> std::path::PathBuf {
190-
std::path::PathBuf::from(std::env::var("HOME").unwrap_or_default())
194+
std::env::var_os("HOME")
195+
.or_else(|| std::env::var_os("USERPROFILE"))
196+
.map(std::path::PathBuf::from)
197+
.unwrap_or_default()
191198
}
192199

193200
/// The candidate `man1` directories, preferring a system-wide location.

src/ident.rs

Lines changed: 5 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -23,17 +23,14 @@ pub fn new_id() -> String {
2323
encode(&random_bytes())
2424
}
2525

26-
/// 128 bits of randomness: the OS pool when available, else a hash of
27-
/// high-resolution time, pid and a counter (uniqueness, not secrecy).
26+
/// 128 bits of randomness: the OS pool (cross-platform via `getrandom`), else a
27+
/// hash of high-resolution time, pid and a counter (uniqueness, not secrecy).
2828
fn random_bytes() -> [u8; 16] {
29-
use std::io::Read;
3029
let mut out = [0u8; 16];
31-
if let Ok(mut f) = std::fs::File::open("/dev/urandom") {
32-
if f.read_exact(&mut out).is_ok() {
33-
return out;
34-
}
30+
match getrandom::fill(&mut out) {
31+
Ok(()) => out,
32+
Err(_) => fallback_random(),
3533
}
36-
fallback_random()
3734
}
3835

3936
fn fallback_random() -> [u8; 16] {

src/view.rs

Lines changed: 20 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -573,15 +573,28 @@ impl App {
573573
}
574574
}
575575

576-
/// Open `url` in the user's default browser (best-effort).
576+
/// Open `url` in the user's default browser (best-effort, cross-platform).
577577
fn open_url(url: &str) {
578-
let opener = if cfg!(target_os = "macos") {
579-
"open"
580-
} else {
581-
"xdg-open"
578+
#[cfg(target_os = "windows")]
579+
let mut cmd = {
580+
// `start` is a cmd builtin; the empty "" is its (window-title) argument.
581+
let mut c = std::process::Command::new("cmd");
582+
c.args(["/C", "start", "", url]);
583+
c
584+
};
585+
#[cfg(target_os = "macos")]
586+
let mut cmd = {
587+
let mut c = std::process::Command::new("open");
588+
c.arg(url);
589+
c
590+
};
591+
#[cfg(not(any(target_os = "windows", target_os = "macos")))]
592+
let mut cmd = {
593+
let mut c = std::process::Command::new("xdg-open");
594+
c.arg(url);
595+
c
582596
};
583-
let _ = std::process::Command::new(opener)
584-
.arg(url)
597+
let _ = cmd
585598
.stdout(std::process::Stdio::null())
586599
.stderr(std::process::Stdio::null())
587600
.spawn();

0 commit comments

Comments
 (0)