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
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ Architect solves this with a grid view that keeps all your agents visible, with
- Wakeable idle input handling keeps typing responsive after short idle periods instead of waiting on a fixed sleep window
- Keyboard navigation: ⌘+Return to expand, ⌘1–⌘0 to switch grid slots, ⌘Arrow to move focus in grid view (plays a brief wave animation on the destination terminal), ⌘N to add, ⌘W to close a terminal (restarts if it's the only terminal), ⌘T for worktrees, ⌘O for recent folders, ⌘D for repo-wide git diff (staged + unstaged + untracked), ⌘R for reader mode, ⌘/ for shortcuts; quit with ⌘Q or the window close button
- Git diff overlay title shows the repo root folder being diffed
- Per-cell cwd bar in grid view with reserved space so terminal content stays visible
- Per-cell cwd bar in grid view reserves space, and terminal dimensions track grid/full mode so content wraps inside the visible area
- Scrollback with trackpad/wheel support and an auto-hiding draggable scrollbar in terminal views
- OSC 8 hyperlink support (Cmd+Click to open)
- Replies to OSC 4/10/11 color queries using the live terminal palette/default colors so Codex and similar CLIs do not stall on startup probes
Expand Down
2 changes: 1 addition & 1 deletion docs/ARCHITECTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,7 @@ Platform Session Rendering UI Overlay
- Runtime persistence is updated during the frame loop when runtime state changes (cwd changes, terminal spawn/despawn, window move/resize, font size changes), and finalization is explicit at the end of `app/runtime.zig`: final save and deinit `Persistence` before deferred subsystem teardown begins.
- Font reload paths are transactional: acquire both replacement fonts first, then swap and destroy old fonts, so a partial reload failure cannot leave deinit hooks pointing at already-freed font resources.
- Window-resize scale handling follows a single ordered path (`reload-if-needed`, then `resize`) to keep behavior consistent between changed-scale and unchanged-scale events.
- Terminal resizes preserve the existing screen contents. Architect temporarily disables ghostty-vt semantic-prompt redraw clearing while applying its own PTY/terminal size updates, records new terminal row/column sizes only after resize calls succeed, skips PTY resize calls when the terminal row/column count is unchanged, and keeps grid view changes render-only. Grid font scaling and CWD-bar space change how many cells are visible in a tile, but they do not resize the backing PTY/VT or reflow agent history; grid rendering follows the active cursor row so compact tiles show short sessions from the top and long sessions near the latest output.
- Terminal resizes follow Ghostty's ordering: update the PTY winsize first, then resize the ghostty-vt terminal model, allowing ghostty-vt's semantic prompt clearing when the shell opted into prompt redraws. Architect records new terminal row/column sizes only after resize calls succeed and skips PTY resize calls when the terminal row/column count is unchanged. Grid/full transitions update the backing PTY/VT size after the stable view mode is reached, and grid sizing accounts for grid font scaling plus the reserved CWD-bar space so compact tiles wrap to the visible area. Grid rendering follows the active cursor row so compact tiles show short sessions from the top and long sessions near the latest output.
- Shared Utilities (`geom`, `colors`, `dpi`, `config`, `logging`, `metrics`, etc.) may be imported by any layer but never import from layers above them.
- **Exception:** `app/*` modules may import `c.zig` directly for SDL type definitions used in input handling. This is a pragmatic shortcut for FFI constants, not a general license to depend on the Platform layer.

Expand Down
114 changes: 78 additions & 36 deletions src/app/layout.zig
Original file line number Diff line number Diff line change
Expand Up @@ -114,11 +114,15 @@ pub fn calculateGridCellTerminalSize(font: *const font_mod.Font, window_width: c
}

pub fn calculateTerminalSizeForMode(font: *const font_mod.Font, window_width: c_int, window_height: c_int, mode: app_state.ViewMode, grid_font_scale: f32, grid_cols: usize, grid_rows: usize, ui_scale: f32) TerminalSize {
_ = mode;
_ = grid_font_scale;
_ = grid_cols;
_ = grid_rows;
return calculateTerminalSize(font, window_width, window_height, 1.0, ui_scale);
return switch (mode) {
.Grid => {
const grid_dim = @max(grid_cols, grid_rows);
const base_grid_scale: f32 = 1.0 / @as(f32, @floatFromInt(grid_dim));
const effective_scale: f32 = base_grid_scale * grid_font_scale;
return calculateGridCellTerminalSize(font, window_width, window_height, effective_scale, grid_cols, grid_rows, ui_scale);
},
.Collapsing, .GridResizing, .Expanding, .Full, .PanningLeft, .PanningRight, .PanningUp, .PanningDown => calculateTerminalSize(font, window_width, window_height, 1.0, ui_scale),
};
}

pub fn scaledFontSize(points: c_int, scale: f32) c_int {
Expand All @@ -134,7 +138,7 @@ pub fn applyTerminalResize(
render_width: c_int,
render_height: c_int,
ui_scale: f32,
) void {
) bool {
const usable_width = @max(0, render_width - dpi.scale(renderer_mod.terminal_padding, ui_scale) * 2);
const usable_height = @max(0, render_height - dpi.scale(renderer_mod.terminal_padding, ui_scale) * 2);

Expand All @@ -145,66 +149,75 @@ pub fn applyTerminalResize(
.ws_ypixel = @intCast(usable_height),
};

var terminal_resized = false;
for (sessions) |session| {
const cells_changed = terminalCellSizeChanged(session.pty_size, cols, rows);
if (!session.spawned or !cells_changed) {
if (!session.spawned) {
session.pty_size = new_size;
continue;
}

const shell = &(session.shell orelse continue);
const terminal = &(session.terminal orelse continue);

shell.pty.setSize(new_size) catch |err| {
std.debug.print("Failed to resize PTY for session {d}: {}\n", .{ session.id, err });
continue;
};
const cells_changed = terminalCellSizeChanged(session.pty_size, cols, rows);
const terminal_cells_changed = terminal.cols != cols or terminal.rows != rows;

resizeTerminalPreservingPrompt(allocator, terminal, cols, rows) catch |err| {
std.debug.print("Failed to resize terminal for session {d}: {}\n", .{ session.id, err });
continue;
};
if (cells_changed) {
shell.pty.setSize(new_size) catch |err| {
std.debug.print("Failed to resize PTY for session {d}: {}\n", .{ session.id, err });
continue;
};
}

session.pty_size = new_size;
if (session.stream) |*stream| {
stream.handler.deinit();
stream.handler = vt_stream.Handler.init(terminal, shell);
} else {
session.stream = vt_stream.initStream(allocator, terminal, shell);
if (terminal_cells_changed) {
resizeTerminal(allocator, terminal, cols, rows, new_size) catch |err| {
std.debug.print("Failed to resize terminal for session {d}: {}\n", .{ session.id, err });
continue;
};

if (session.stream == null) {
session.stream = vt_stream.initStream(allocator, terminal, shell);
}
session.markDirty();
terminal_resized = true;
}

session.markDirty();
session.pty_size = new_size;
}
return terminal_resized;
}

fn terminalCellSizeChanged(current: pty_mod.winsize, cols: u16, rows: u16) bool {
return current.ws_col != cols or current.ws_row != rows;
}

fn resizeTerminalPreservingPrompt(
fn resizeTerminal(
allocator: std.mem.Allocator,
terminal: *ghostty_vt.Terminal,
cols: u16,
rows: u16,
size: pty_mod.winsize,
) !void {
const prompt_redraw = terminal.flags.shell_redraws_prompt;
terminal.flags.shell_redraws_prompt = .false;
defer terminal.flags.shell_redraws_prompt = prompt_redraw;

try terminal.resize(allocator, cols, rows);
terminal.width_px = @intCast(size.ws_xpixel);
terminal.height_px = @intCast(size.ws_ypixel);
terminal.modes.set(.synchronized_output, false);
}

test "view mode and grid font scale do not change terminal size" {
test "grid mode sizes terminals to the rendered tile area" {
var font: font_mod.Font = undefined;
font.cell_width = 10;
font.cell_height = 20;

const full = calculateTerminalSizeForMode(&font, 1200, 800, .Full, 2.0, 2, 1, 1.0);
const full = calculateTerminalSizeForMode(&font, 1200, 800, .Full, 1.0, 2, 1, 1.0);
const normal_grid = calculateTerminalSizeForMode(&font, 1200, 800, .Grid, 1.0, 2, 1, 1.0);
const enlarged_grid = calculateTerminalSizeForMode(&font, 1200, 800, .Grid, 2.0, 2, 1, 1.0);

try std.testing.expectEqual(full, normal_grid);
try std.testing.expectEqual(full, enlarged_grid);
try std.testing.expect(normal_grid.cols < full.cols);
try std.testing.expect(enlarged_grid.cols < normal_grid.cols);
try std.testing.expectEqual(full, calculateTerminalSizeForMode(&font, 1200, 800, .Expanding, 1.0, 2, 1, 1.0));
try std.testing.expectEqual(full, calculateTerminalSizeForMode(&font, 1200, 800, .Collapsing, 1.0, 2, 1, 1.0));
try std.testing.expectEqual(full, calculateTerminalSizeForMode(&font, 1200, 800, .GridResizing, 1.0, 2, 1, 1.0));
}

test "terminal cell size ignores pixel-only resize differences" {
Expand All @@ -220,7 +233,7 @@ test "terminal cell size ignores pixel-only resize differences" {
try std.testing.expect(terminalCellSizeChanged(size, 120, 41));
}

test "terminal resize preserves semantic prompt contents" {
test "terminal resize preserves prompt contents when shell does not redraw" {
const allocator = std.testing.allocator;

var terminal = try ghostty_vt.Terminal.init(allocator, .{
Expand All @@ -236,16 +249,45 @@ test "terminal resize preserves semantic prompt contents" {
try screen.testWriteString("> ");
screen.cursorSetSemanticContent(.{ .input = .clear_eol });
try screen.testWriteString("echo");
terminal.flags.shell_redraws_prompt = .false;

const before = try terminal.plainString(allocator);
defer allocator.free(before);
try std.testing.expectEqualStrings("ABCDE\n> echo", before);

const prompt_redraw = terminal.flags.shell_redraws_prompt;
try resizeTerminalPreservingPrompt(allocator, &terminal, 20, 3);
try std.testing.expectEqual(prompt_redraw, terminal.flags.shell_redraws_prompt);
const size = pty_mod.winsize{ .ws_col = 20, .ws_row = 3, .ws_xpixel = 200, .ws_ypixel = 60 };
try resizeTerminal(allocator, &terminal, 20, 3, size);

const after = try terminal.plainString(allocator);
defer allocator.free(after);
try std.testing.expectEqualStrings("ABCDE\n> echo", after);
try std.testing.expectEqual(@as(u32, 200), terminal.width_px);
try std.testing.expectEqual(@as(u32, 60), terminal.height_px);
}

test "terminal resize clears semantic prompt when shell redraws prompt" {
const allocator = std.testing.allocator;

var terminal = try ghostty_vt.Terminal.init(allocator, .{
.cols = 10,
.rows = 3,
.max_scrollback = 5,
});
defer terminal.deinit(allocator);

const screen = terminal.screens.active;
try screen.testWriteString("ABCDE\n");
screen.cursorSetSemanticContent(.{ .prompt = .initial });
try screen.testWriteString("> ");
screen.cursorSetSemanticContent(.{ .input = .clear_eol });
try screen.testWriteString("echo");

terminal.flags.shell_redraws_prompt = .true;
const size = pty_mod.winsize{ .ws_col = 20, .ws_row = 3, .ws_xpixel = 200, .ws_ypixel = 60 };
try resizeTerminal(allocator, &terminal, 20, 3, size);
try std.testing.expectEqual(.true, terminal.flags.shell_redraws_prompt);

const after = try terminal.plainString(allocator);
defer allocator.free(after);
try std.testing.expectEqualStrings("ABCDE", after);
}
Loading