diff --git a/.github/workflows/windows.yml b/.github/workflows/windows.yml new file mode 100644 index 0000000..f75565c --- /dev/null +++ b/.github/workflows/windows.yml @@ -0,0 +1,64 @@ +name: Windows Build + +# Phase 0 Windows de-risk spike. The point of this job is information, not a +# shippable artifact: it proves (or kills) the make-or-break dependency — gpui +# at our pinned zed rev, plus the guise component library on top of it — +# compiling on Windows, and confirms the platform-independent crates build +# cleanly. A final informational step compiles the whole workspace to log the +# remaining Windows porting surface (pty/terminal/app are still Unix-only) +# without failing the gate. +# +# Every probe carries `if: always()` so one failure never hides the others: a +# single run collects gpui, guise, and full-workspace logs together. +on: + workflow_dispatch: {} + pull_request: + paths: + - "crates/**" + - "vendor/**" + - "Cargo.toml" + - "Cargo.lock" + - ".github/workflows/windows.yml" + +jobs: + build: + runs-on: windows-latest + steps: + - uses: actions/checkout@v5 + with: + submodules: recursive + + - name: Install Rust + uses: dtolnay/rust-toolchain@stable + + - name: Cache cargo + uses: Swatinem/rust-cache@v2 + + # The make-or-break dependency: does gpui at our zed rev compile on + # Windows at all? Everything above the UI layer is wasted if this fails. + - name: Probe gpui (zed rev) + run: cargo build -p gpui + + # Our component library resolved against gpui-on-Windows (through this + # workspace's [patch.crates-io], not guise's own crates.io pin). + - name: Probe guise-ui + if: always() + run: cargo build -p guise-ui + + # Platform-independent crates: pure logic, no OS calls. These must build. + - name: Build portable crates + if: always() + run: > + cargo build + -p vt -p input -p workspace -p theme -p config + -p macros -p mcp -p cast -p plugin + + # Informational only: compile the whole workspace so the log captures the + # full remaining Windows surface (pty/terminal are #![cfg(unix)] and app + # depends on them unconditionally). `--keep-going` enumerates every + # failing crate in one run instead of stopping at the first. Expected to + # fail today; never gates. + - name: Full workspace surface (informational) + if: always() + continue-on-error: true + run: cargo build --workspace --keep-going diff --git a/crates/app/src/update.rs b/crates/app/src/update.rs index ced5379..8115a26 100644 --- a/crates/app/src/update.rs +++ b/crates/app/src/update.rs @@ -152,8 +152,9 @@ fn download_to(url: &str, dest: &std::path::Path) -> Result<(), String> { } } -/// Download the release and swap it into place, returning the binary to restart -/// into. Only for in-place installs ([`Install::MacApp`], [`Install::AppImage`]); +/// Download the release and swap it into place, returning the path to relaunch +/// (the `.app` bundle on macOS, the AppImage on Linux). Only for in-place +/// installs ([`Install::MacApp`], [`Install::AppImage`]); /// an [`Install::Unknown`] has no in-place path and opens the release page /// instead (see `updateui`). Blocking. pub fn stage(release: &Release, install: &Install) -> Result { @@ -167,7 +168,10 @@ pub fn stage(release: &Release, install: &Install) -> Result { } /// macOS: download the notarized `.dmg`, mount it, copy the new `.app` over the -/// installed one, unmount, and return the inner binary to relaunch. +/// installed one, unmount, and return the `.app` bundle to relaunch. gpui's +/// restart `open`s this path, and `open` must be handed the bundle, not the +/// inner Mach-O — `open` on a raw executable has no bundle handler and falls +/// back to launching it inside Terminal.app. #[cfg(target_os = "macos")] fn stage_mac_app(release: &Release, app: &std::path::Path, dir: &std::path::Path) -> Result { let url = release.asset(".dmg").ok_or("release has no .dmg asset")?; @@ -206,7 +210,7 @@ fn stage_mac_app(release: &Release, app: &std::path::Path, dir: &std::path::Path return Err(format!("install update: {e}")); } let _ = std::fs::remove_dir_all(&backup); - Ok(app.join("Contents/MacOS/prompt")) + Ok(app.to_path_buf()) } #[cfg(not(target_os = "macos"))] diff --git a/crates/app/src/view/mod.rs b/crates/app/src/view/mod.rs index 619b182..b19d640 100644 --- a/crates/app/src/view/mod.rs +++ b/crates/app/src/view/mod.rs @@ -266,9 +266,14 @@ pub struct TerminalView { /// Set when this pane posts a desktop notification (OSC 9/777/99) while /// unfocused; drives the tab/pane attention indicator. Cleared on focus. attention: bool, - /// Tracks pane focus (kept in sync by the focus-in/out subscriptions), so - /// a notification only raises the attention indicator on a background pane. + /// Tracks true focus — window *and* pane — kept in sync by the focus-in/out + /// subscriptions. Drives ?1004 reporting and the attention indicator (a + /// background notification), both of which react to the window losing focus. focused: bool, + /// Tracks the active *pane* only — unlike `focused` it survives the whole + /// window losing focus. Drives the unfocused-split dimming and the hidden + /// cursor, so those reflect which split is active, not window focus. + pane_active: bool, /// True while a repaint is being withheld for synchronized output /// (?2026), with a safety timer armed to release it. sync_pending: bool, @@ -326,16 +331,29 @@ impl TerminalView { let sub_in = window.on_focus_in(&focus, cx, move |_window, cx| { let _ = on_in.update(cx, |this, cx| { this.focused = true; + this.pane_active = true; this.report_focus(true); this.clear_attention(cx); cx.emit(ViewEvent::Focused); }); }); let on_out = cx.weak_entity(); - let sub_out = window.on_focus_out(&focus, cx, move |_event, _window, cx| { - let _ = on_out.update(cx, |this, _| { + let sub_out = window.on_focus_out(&focus, cx, move |_event, window, cx| { + // `focused` tracks true focus (window + pane) — it drives ?1004 + // reporting and the background-notification attention dot, both of + // which should react to the window losing focus. `pane_active` + // tracks the active *pane* only and drives the split dimming + + // hidden cursor, so it stays set when the whole window merely + // deactivates (which also fires focus-out); only a real pane switch, + // where the window is still active, clears it. + let pane_switch = window.is_window_active(); + let _ = on_out.update(cx, |this, cx| { this.focused = false; this.report_focus(false); + if pane_switch { + this.pane_active = false; + } + cx.notify(); }); }); Self { @@ -365,6 +383,7 @@ impl TerminalView { bell: false, attention: false, focused: false, + pane_active: false, sync_pending: false, search: None, hints: None, @@ -598,9 +617,11 @@ impl Render for TerminalView { let menu = self .context_menu .map(|pos| self.context_menu_overlay(pos, cx)); - // Dim a pane while it is not focused so the active split is obvious; - // `1.0` (the opt default when unset) leaves it untouched. - let dim = if self.focused { + // Dim a pane while it is not the active split so the active one is + // obvious; `1.0` (the opt default when unset) leaves it untouched. Keyed + // off `pane_active`, not `focused`, so backgrounding the window doesn't + // dim every pane. + let dim = if self.pane_active { 1.0 } else { self.unfocused_split_opacity.clamp(0.0, 1.0) @@ -642,7 +663,7 @@ impl Render for TerminalView { self.copy_on_select, self.smart_select, self.middle_click_paste, - self.focused, + self.pane_active, query, self.suggestion_ghost(), self.image_cache.clone(), diff --git a/crates/config/src/lib.rs b/crates/config/src/lib.rs index 9ab1c04..c7bec0f 100644 --- a/crates/config/src/lib.rs +++ b/crates/config/src/lib.rs @@ -20,14 +20,24 @@ pub use watch::{watch, WatchHandle}; use std::path::PathBuf; -/// Default config file path: -/// `$XDG_CONFIG_HOME/prompt/config`, else `~/.config/prompt/config`. +/// Default config file path: `$XDG_CONFIG_HOME/prompt/config`, else on Windows +/// `%APPDATA%\prompt\config`, else `~/.config/prompt/config`. pub fn default_path() -> Option { if let Some(xdg) = std::env::var_os("XDG_CONFIG_HOME") { if !xdg.is_empty() { return Some(PathBuf::from(xdg).join("prompt").join("config")); } } + #[cfg(windows)] + { + // Windows has no HOME by default; the roaming app-data dir is the home + // for per-user config. + if let Some(appdata) = std::env::var_os("APPDATA") { + if !appdata.is_empty() { + return Some(PathBuf::from(appdata).join("prompt").join("config")); + } + } + } let home = std::env::var_os("HOME")?; if home.is_empty() { return None;