Skip to content

Commit f4ac7a4

Browse files
mathix420claude
andcommitted
fix(windows): resolve diffs, stray consoles, and state persistence
Three Windows-only breakages: - Diffs never opened: the sidebar ran `sh -c '… | delta'`, but there is no POSIX `sh` on Windows. Wire delta in as git's pager instead so git drives the pipe itself — no shell dependency, identical result on every platform. - A console window flashed on every `git`/`gh` probe: a GUI-subsystem binary owns no console, so each child got its own. Add a `hide_console` helper that sets `CREATE_NO_WINDOW`, applied to all child spawns. - Projects were forgotten across restarts: `config_path` only checked `$XDG_CONFIG_HOME`/`$HOME`, both unset on Windows, so state never saved or loaded. Fall back to the roaming app-data dir there. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent 6b0001e commit f4ac7a4

7 files changed

Lines changed: 81 additions & 23 deletions

File tree

alacritree/src/app.rs

Lines changed: 20 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1146,25 +1146,30 @@ impl AlacritreeApp {
11461146
}
11471147
}
11481148

1149-
/// Pipe `git diff` for the clicked file through delta. Positional args via
1150-
/// `sh -c '…' name "$1" "$2"` keep path/branch names out of the script body so
1151-
/// no shell metacharacter in the file name can be interpreted.
1149+
/// Show the clicked file's `git diff` in delta, wired in as git's pager so git
1150+
/// drives the pipe itself. This drops the POSIX-`sh` dependency the old
1151+
/// `sh -c '… | delta'` had — which had no equivalent on Windows, so diffs never
1152+
/// opened there. Paths/branches stay in argv, so no file name is shell-parsed.
11521153
fn build_diff_command(req: &DiffRequest) -> (String, Vec<String>) {
1153-
let script: &str = match &req.source {
1154-
DiffSource::Staged => r#"git diff --cached -- "$1" | delta --paging=always"#,
1155-
DiffSource::Worktree => r#"git diff -- "$1" | delta --paging=always"#,
1156-
// `--no-index` is git's "diff arbitrary files" mode; against /dev/null it
1157-
// shows the untracked file as a pure addition. Exits non-zero by design.
1158-
DiffSource::Untracked => r#"git diff --no-index -- /dev/null "$1" | delta --paging=always"#,
1154+
let mut args =
1155+
vec!["-c".to_string(), "core.pager=delta --paging=always".to_string(), "diff".to_string()];
1156+
match &req.source {
1157+
DiffSource::Staged => args.push("--cached".to_string()),
1158+
DiffSource::Worktree => {},
1159+
// `--no-index` against /dev/null shows the untracked file as a pure
1160+
// addition; git special-cases "/dev/null" on every platform. Exits
1161+
// non-zero by design.
1162+
DiffSource::Untracked => args.push("--no-index".to_string()),
11591163
// Triple-dot diff = "from merge-base to HEAD" — matches the sidebar's
11601164
// `Changes vs <branch>` stat semantics in git_status.rs.
1161-
DiffSource::Branch { .. } => r#"git diff "$2"... -- "$1" | delta --paging=always"#,
1162-
};
1163-
let mut args = vec!["-c".to_string(), script.to_string(), "sh".to_string(), req.file.clone()];
1164-
if let DiffSource::Branch { base } = &req.source {
1165-
args.push(base.clone());
1165+
DiffSource::Branch { base } => args.push(format!("{base}...")),
1166+
}
1167+
args.push("--".to_string());
1168+
if matches!(req.source, DiffSource::Untracked) {
1169+
args.push("/dev/null".to_string());
11661170
}
1167-
("sh".to_string(), args)
1171+
args.push(req.file.clone());
1172+
("git".to_string(), args)
11681173
}
11691174

11701175
fn dirty_warning(counts: &DirtyCounts) -> Option<String> {

alacritree/src/command_ext.rs

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
//! alacritree is a GUI-subsystem binary with no console of its own, so on
2+
//! Windows each `git`/`gh`/`cmd` child gets a fresh console window unless we
3+
//! pass `CREATE_NO_WINDOW`.
4+
5+
use std::process::Command;
6+
7+
pub trait CommandExt {
8+
/// Suppress the console window Windows would spawn for this child. No-op
9+
/// elsewhere.
10+
fn hide_console(&mut self) -> &mut Self;
11+
}
12+
13+
impl CommandExt for Command {
14+
#[cfg(windows)]
15+
fn hide_console(&mut self) -> &mut Self {
16+
use std::os::windows::process::CommandExt as _;
17+
const CREATE_NO_WINDOW: u32 = 0x0800_0000;
18+
self.creation_flags(CREATE_NO_WINDOW)
19+
}
20+
21+
#[cfg(not(windows))]
22+
fn hide_console(&mut self) -> &mut Self {
23+
self
24+
}
25+
}

alacritree/src/links.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ use alacritty_terminal::index::{Direction, Point};
1212
use alacritty_terminal::term::Term;
1313
use alacritty_terminal::term::search::{Match, RegexIter, RegexSearch};
1414

15+
use crate::command_ext::CommandExt;
1516
use crate::session::EventProxy;
1617

1718
// Identical to alacritty's built-in URL hint regex so the set of recognised
@@ -161,6 +162,7 @@ pub fn open(uri: &str) {
161162
#[cfg(target_os = "macos")]
162163
fn spawn(uri: &str) -> std::io::Result<()> {
163164
Command::new("open")
165+
.hide_console()
164166
.arg(uri)
165167
.stdin(Stdio::null())
166168
.stdout(Stdio::null())
@@ -174,6 +176,7 @@ fn spawn(uri: &str) -> std::io::Result<()> {
174176
// `start` treats its first quoted argument as a window title, so pass an
175177
// empty title before the URL to keep `cmd` from eating it.
176178
Command::new("cmd")
179+
.hide_console()
177180
.args(["/c", "start", ""])
178181
.arg(uri)
179182
.stdin(Stdio::null())
@@ -186,6 +189,7 @@ fn spawn(uri: &str) -> std::io::Result<()> {
186189
#[cfg(not(any(target_os = "macos", target_os = "windows")))]
187190
fn spawn(uri: &str) -> std::io::Result<()> {
188191
Command::new("xdg-open")
192+
.hide_console()
189193
.arg(uri)
190194
.stdin(Stdio::null())
191195
.stdout(Stdio::null())

alacritree/src/main.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ mod bindings;
55
mod builtin_font;
66
mod clipboard;
77
mod colors;
8+
mod command_ext;
89
mod config;
910
mod fonts;
1011
mod git_status;

alacritree/src/pr_status.rs

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@ use std::sync::mpsc::{self, Receiver};
1616
use std::thread;
1717
use std::time::{Duration, Instant};
1818

19+
use crate::command_ext::CommandExt;
20+
1921
/// Re-query at most this often. PR base branches rarely change, and a stale
2022
/// answer just falls back to the previous diff target — not worth hammering
2123
/// `gh` on every status refresh.
@@ -120,6 +122,7 @@ fn spawn_lookup(
120122
/// to be checked out in the worktree.
121123
fn query_gh(path: &Path, branch: &str) -> Option<PrInfo> {
122124
let output = Command::new("gh")
125+
.hide_console()
123126
.current_dir(path)
124127
.args(["pr", "view", branch, "--json", "number,baseRefName,url"])
125128
.stdout(Stdio::piped())
@@ -147,7 +150,8 @@ mod tests {
147150

148151
#[test]
149152
fn parses_gh_json() {
150-
let stdout = br#"{"baseRefName":"main","number":42,"url":"https://github.com/o/r/pull/42"}"#;
153+
let stdout =
154+
br#"{"baseRefName":"main","number":42,"url":"https://github.com/o/r/pull/42"}"#;
151155
let info = parse_gh_output(stdout).unwrap();
152156
assert_eq!(info.number, 42);
153157
assert_eq!(info.base_branch, "main");

alacritree/src/state.rs

Lines changed: 20 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -26,13 +26,26 @@ fn default_true() -> bool {
2626
}
2727

2828
pub fn config_path() -> Option<PathBuf> {
29-
let base = if let Some(xdg) = std::env::var_os("XDG_CONFIG_HOME") {
30-
PathBuf::from(xdg)
31-
} else {
32-
let home = std::env::var_os("HOME")?;
33-
PathBuf::from(home).join(".config")
34-
};
35-
Some(base.join("alacritree").join("state.toml"))
29+
Some(config_dir()?.join("alacritree").join("state.toml"))
30+
}
31+
32+
/// Per-user config base: XDG on Unix, the roaming app-data dir on Windows
33+
/// (which has neither `$XDG_CONFIG_HOME` nor `$HOME`).
34+
#[cfg(not(windows))]
35+
fn config_dir() -> Option<PathBuf> {
36+
if let Some(xdg) = std::env::var_os("XDG_CONFIG_HOME") {
37+
return Some(PathBuf::from(xdg));
38+
}
39+
let home = std::env::var_os("HOME")?;
40+
Some(PathBuf::from(home).join(".config"))
41+
}
42+
43+
#[cfg(windows)]
44+
fn config_dir() -> Option<PathBuf> {
45+
std::env::var_os("APPDATA")
46+
.or_else(|| std::env::var_os("LOCALAPPDATA"))
47+
.map(PathBuf::from)
48+
.or_else(home::home_dir)
3649
}
3750

3851
pub fn load() -> PersistedState {

alacritree/src/worktree.rs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@ use std::process::{Command, Stdio};
66
use std::sync::mpsc::{self, Receiver, Sender};
77
use std::thread;
88

9+
use crate::command_ext::CommandExt;
10+
911
#[derive(Debug, Clone)]
1012
pub enum Progress {
1113
Step(String),
@@ -141,6 +143,7 @@ fn enable_claude_terminal_bell(worktree_root: &Path) -> std::io::Result<()> {
141143

142144
fn run_git(cwd: &Path, args: &[&str]) -> Result<(), String> {
143145
let output = Command::new("git")
146+
.hide_console()
144147
.arg("-C")
145148
.arg(cwd)
146149
.args(args)
@@ -159,6 +162,7 @@ fn run_git(cwd: &Path, args: &[&str]) -> Result<(), String> {
159162

160163
fn has_remote(cwd: &Path, name: &str) -> bool {
161164
Command::new("git")
165+
.hide_console()
162166
.arg("-C")
163167
.arg(cwd)
164168
.args(["remote", "get-url", name])
@@ -219,6 +223,7 @@ fn resolve_base_branch(cwd: &Path, hint: Option<&str>) -> Result<(String, String
219223

220224
fn rev_parse_verify(cwd: &Path, name: &str) -> bool {
221225
Command::new("git")
226+
.hide_console()
222227
.arg("-C")
223228
.arg(cwd)
224229
.args(["rev-parse", "--verify", "--quiet", name])
@@ -235,6 +240,7 @@ fn rev_parse_verify(cwd: &Path, name: &str) -> bool {
235240
/// We pull the `refs/heads/<name>` from the symref line.
236241
fn query_origin_head(cwd: &Path) -> Option<String> {
237242
let output = Command::new("git")
243+
.hide_console()
238244
.arg("-C")
239245
.arg(cwd)
240246
.args(["ls-remote", "--symref", "origin", "HEAD"])

0 commit comments

Comments
 (0)