Merge pull request #129 from l0ng-ai/fix/windows-git-probe-console-flash

fix(windows): stop console windows flashing on every git status probe
This commit is contained in:
l0ng-ai
2026-07-19 18:47:02 +08:00
committed by GitHub
8 changed files with 67 additions and 27 deletions
+3 -4
View File
@@ -762,10 +762,9 @@ fn enable_codex_hooks_feature() -> Result<(), String> {
.chain(home_dir().map(|h| h.join(".local/bin/codex")))
.find(|p| p.exists());
let program = candidates.unwrap_or_else(|| PathBuf::from("codex"));
match std::process::Command::new(&program)
.args(["features", "enable", "hooks"])
.output()
{
let mut cmd = std::process::Command::new(&program);
cmd.args(["features", "enable", "hooks"]);
match crate::core::proc::hide_console(&mut cmd).output() {
Ok(out) if out.status.success() => Ok(()),
Ok(out) => Err(format!(
"codex exited with {}: {}",
+1
View File
@@ -18,6 +18,7 @@ pub mod config;
#[allow(dead_code)]
pub mod keychain;
pub mod osc;
pub mod proc;
pub mod session;
pub mod shells;
pub mod ssh_config;
+40
View File
@@ -0,0 +1,40 @@
//! One place for the Windows subprocess flag every helper shell-out needs.
//!
//! tty7 is a GUI process with no console of its own, so launching a console
//! subsystem program (`git.exe`, `wsl.exe`, …) makes Windows allocate a fresh
//! console for it — a black window that pops up and vanishes. That is invisible
//! on a one-off invocation and very visible on the git-status probe, which runs
//! four `git` calls every time a pane's cwd changes or a command ends.
//!
//! `CREATE_NO_WINDOW` suppresses the console entirely; stdout/stderr pipes are
//! unaffected, so output capture keeps working. Every non-PTY `Command` in the
//! app should go through [`hide_console`] (or [`hide_console_tokio`] for the
//! async flavor) before it runs. PTY children are not in scope — the daemon
//! owns those and passes its own flags (see [`crate::daemon::spawn`]).
use std::process::Command;
#[cfg(windows)]
const CREATE_NO_WINDOW: u32 = 0x0800_0000;
/// Suppress the console window Windows would otherwise allocate for a console
/// subsystem child. No-op on Unix, so callers stay `cfg`-free.
pub fn hide_console(cmd: &mut Command) -> &mut Command {
#[cfg(windows)]
{
use std::os::windows::process::CommandExt as _;
cmd.creation_flags(CREATE_NO_WINDOW);
}
cmd
}
/// [`hide_console`] for `tokio::process::Command`. Separate because tokio's
/// builder is a distinct type with its own `creation_flags`, not a `Deref` to
/// the std one.
pub fn hide_console_tokio(cmd: &mut tokio::process::Command) -> &mut tokio::process::Command {
#[cfg(windows)]
{
cmd.creation_flags(CREATE_NO_WINDOW);
}
cmd
}
+5 -9
View File
@@ -319,17 +319,13 @@ fn find_git_bash() -> Option<PathBuf> {
}
/// Installed WSL distribution names via `wsl.exe -l -q`, or empty when WSL is
/// absent. `CREATE_NO_WINDOW` keeps the probe from flashing a console window
/// (we're a GUI process).
/// absent. [`hide_console`](crate::core::proc::hide_console) keeps the probe
/// from flashing a console window (we're a GUI process).
#[cfg(windows)]
fn list_wsl_distros() -> Vec<String> {
use std::os::windows::process::CommandExt as _;
const CREATE_NO_WINDOW: u32 = 0x0800_0000;
let Ok(output) = std::process::Command::new("wsl.exe")
.args(["-l", "-q"])
.creation_flags(CREATE_NO_WINDOW)
.output()
else {
let mut cmd = std::process::Command::new("wsl.exe");
cmd.args(["-l", "-q"]);
let Ok(output) = crate::core::proc::hide_console(&mut cmd).output() else {
return Vec::new();
};
if !output.status.success() {
+3 -4
View File
@@ -62,10 +62,9 @@ pub fn is_inside_repo(cwd: &Path) -> bool {
/// Run `git -C <dir> <args>`, returning trimmed stdout on success and trimmed
/// stderr as the error otherwise.
fn git(dir: &Path, args: &[&str]) -> Result<String, String> {
let out = std::process::Command::new("git")
.arg("-C")
.arg(dir)
.args(args)
let mut cmd = std::process::Command::new("git");
cmd.arg("-C").arg(dir).args(args);
let out = crate::core::proc::hide_console(&mut cmd)
.output()
.map_err(|e| format!("failed to run git: {e}"))?;
if out.status.success() {
+4
View File
@@ -173,6 +173,10 @@ fn spawn_proxy_command(
.stdout(std::process::Stdio::piped())
.stderr(std::process::Stdio::inherit())
.kill_on_drop(true);
// The daemon is detached and has no console to lend this child, so without
// the flag a `ProxyCommand` (`ssh -W`, `connect.exe`, `cloudflared`) gets a
// console of its own that stays up for the whole session.
crate::core::proc::hide_console_tokio(&mut cmd);
let mut child = cmd
.spawn()
.map_err(|e| anyhow::anyhow!("spawn ProxyCommand failed: {e}"))?;
+8 -7
View File
@@ -251,18 +251,19 @@ fn diff_numstat(cwd: &Path) -> Option<(u32, u32)> {
/// Run `git -C <cwd> <args>` and return stdout on success, `None` on a
/// non-zero exit or a missing `git`. `GIT_OPTIONAL_LOCKS=0` makes the read
/// truly read-only; stdin is nulled so a misconfigured git can't block on a
/// prompt. Shared with [`git_diff`](crate::terminal::git_diff) so every git
/// read in the app goes through the same lock-free, prompt-proof invocation.
/// prompt; `hide_console` keeps this GUI process from flashing a console window
/// on Windows for every probe. Shared with [`git_diff`](crate::terminal::git_diff)
/// so every git read in the app goes through the same lock-free, prompt-proof
/// invocation.
pub(crate) fn git(cwd: &Path, args: &[&str]) -> Option<String> {
let out = Command::new("git")
.arg("-C")
let mut cmd = Command::new("git");
cmd.arg("-C")
.arg(cwd)
.args(args)
.env("GIT_OPTIONAL_LOCKS", "0")
.stdin(Stdio::null())
.stderr(Stdio::null())
.output()
.ok()?;
.stderr(Stdio::null());
let out = crate::core::proc::hide_console(&mut cmd).output().ok()?;
if !out.status.success() {
return None;
}
+3 -3
View File
@@ -3048,9 +3048,9 @@ impl Tty7App {
// which is what a review pass wants. Both invocations are quick; the
// prompt builder caps runaway diffs.
let run = |args: &[&str]| {
std::process::Command::new("git")
.args(args)
.current_dir(&cwd)
let mut cmd = std::process::Command::new("git");
cmd.args(args).current_dir(&cwd);
crate::core::proc::hide_console(&mut cmd)
.output()
.ok()
.filter(|o| o.status.success())