Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
64 changes: 64 additions & 0 deletions .github/workflows/windows.yml
Original file line number Diff line number Diff line change
@@ -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
12 changes: 8 additions & 4 deletions crates/app/src/update.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<PathBuf, String> {
Expand All @@ -167,7 +168,10 @@ pub fn stage(release: &Release, install: &Install) -> Result<PathBuf, String> {
}

/// 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<PathBuf, String> {
let url = release.asset(".dmg").ok_or("release has no .dmg asset")?;
Expand Down Expand Up @@ -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"))]
Expand Down
37 changes: 29 additions & 8 deletions crates/app/src/view/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -365,6 +383,7 @@ impl TerminalView {
bell: false,
attention: false,
focused: false,
pane_active: false,
sync_pending: false,
search: None,
hints: None,
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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(),
Expand Down
14 changes: 12 additions & 2 deletions crates/config/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<PathBuf> {
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;
Expand Down
Loading