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
19 changes: 16 additions & 3 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,8 @@ The project is an experimental terminal multiplexer with:
- **Resizable window** - window can be resized dynamically with automatic terminal and PTY resizing
- **Terminal switching with panning** - use Cmd+Shift+[ / Cmd+Shift+] to switch between terminals in full-screen mode with smooth horizontal panning animation
- **Keyboard support** - ESC key collapses expanded sessions back to grid view
- **Font size adjustment** - Cmd+Plus/Minus to adjust font size (8-32px range, saved automatically)
- **Persistent configuration** - automatically saves and restores font size, window dimensions, and window position to `~/.config/architect/config.json`
- **Real-time I/O** - non-blocking PTY reading with live terminal updates
- **Scrollback in place** - mouse wheel scrolls per-terminal history; typing or new input snaps back to live output and a yellow strip in grid view indicates a scrolled session

Expand All @@ -132,6 +134,7 @@ Key dependency details:
- `src/shell.zig` - Shell process spawning and management
- `src/pty.zig` - PTY (pseudo-terminal) abstractions and utilities
- `src/font.zig` - Font rendering with SDL_ttf and glyph caching
- `src/config.zig` - Configuration persistence (font size, window dimensions, and position)
- `src/c.zig` - C library bindings for SDL3 and SDL_ttf
- `build.zig` - Zig build configuration with ghostty-vt module and SDL3 dependencies
- `build.zig.zon` - Package manifest with ghostty path dependency
Expand Down Expand Up @@ -189,17 +192,27 @@ The application implements a 3×3 grid with the following components:
- Mouse clicks detect grid cell selection and trigger expansion
- ESC key collapses expanded sessions back to grid
- Cmd+Shift+[ / Cmd+Shift+] switches between terminals in full-screen mode with panning animation
- Cmd+Plus/Minus adjusts font size (8-32px range)
- Mouse wheel scrolls per-session history; typing or new output snaps the viewport back to live
- Keyboard input encoded to ANSI sequences and written to active PTY
- Non-blocking PTY reads avoid blocking the event loop
- Window resize events trigger automatic terminal and PTY resizing
- Window move events update internal position tracking
- Window resize events trigger automatic config saving and terminal/PTY resizing

**Configuration System:**
- Settings stored in `~/.config/architect/config.json` as JSON
- Automatically saves on: window resize, font size change (window position is tracked and saved with these events)
- Automatically loads on startup with fallback to defaults
- Configuration includes: `font_size`, `window_width`, `window_height`, `window_x`, `window_y`
- Config directory created automatically if it doesn't exist
- Window position uses -1 as sentinel for "no saved position" (allows restoring position at 0,0 or with negative coordinates on multi-monitor setups)

### Known Limitations

The following features are not yet implemented:
- **No emoji support**: Unicode emojis may not render correctly
- **No font selection**: Hardcoded to SF Mono font
- **No configurability**: Grid size, colors, and keybindings are hardcoded
- **No font selection**: Hardcoded to SF Mono font (though size is adjustable via Cmd+Plus/Minus)
- **Limited configurability**: Grid size, colors, and keybindings are hardcoded (font size, window size, and position are configurable)
- **Limited AI tool compatibility**: Works with Claude and Gemini models, but not with Codex

## Zig Version
Expand Down
35 changes: 33 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,8 @@ See [Setup](#setup) section below for building from source.
- Type in the focused terminal
- **Scrollback in Place**: Hover any terminal and use the mouse wheel to scroll history; typing snaps back to live output and a yellow strip in grid view shows when you're scrolled
- **High-Quality Rendering**: SDL_ttf font rendering with glyph caching, vsynced presentation, and cached grid tiles to reduce redraw work
- **Persistent Configuration**: Automatically saves and restores font size, window dimensions, and window position
- **Font Size Adjustment**: Use Cmd+Plus/Minus to adjust font size (saved automatically)
- **Claude-friendly hooks**: Unix domain socket for notifying Architect when a session is waiting for approval or finished; grid tiles highlight with a fat yellow border

## Prerequisites
Expand Down Expand Up @@ -110,6 +112,32 @@ Run the application:
zig build run
```

## Configuration

Architect automatically saves your preferences to `~/.config/architect/config.json`. The configuration includes:

- **Font size**: Adjusted via Cmd+Plus/Minus shortcuts (range: 8-32px, default: 14px)
- **Window dimensions**: Automatically saved when you resize the window
- **Window position**: Saved along with window dimensions when you resize or adjust font size

The configuration file is created automatically on first use and updated whenever settings change. No manual editing required.

**Example configuration:**
```json
{
"font_size": 16,
"window_width": 1920,
"window_height": 1080,
"window_x": 150,
"window_y": 100
}
```

To reset to defaults, simply delete the configuration file:
```bash
rm ~/.config/architect/config.json
```

## Development

Run tests:
Expand Down Expand Up @@ -228,6 +256,7 @@ Download the latest release from the [releases page](https://github.com/forketyf
- `src/shell.zig` - Shell process spawning and management
- `src/pty.zig` - PTY abstractions and utilities
- `src/font.zig` - Font rendering with SDL_ttf and glyph caching
- `src/config.zig` - Configuration persistence (saves font size, window size, and position)
- `src/c.zig` - C library bindings for SDL3
- `build.zig` - Zig build configuration with SDL3 dependencies
- `build.zig.zon` - Zig package dependencies
Expand Down Expand Up @@ -273,15 +302,17 @@ The application uses cubic ease-in-out interpolation to smoothly transition betw
- Keyboard input handling
- Full-window terminal scaling
- Dynamic terminal and PTY resizing on window resize
- Persistent configuration (font size, window size, and position)
- Font size adjustment via keyboard shortcuts (Cmd+Plus/Minus)
- Claude Code integration via Unix domain sockets
- Scrolling back through terminal history (mouse wheel) with a grid indicator when a pane is scrolled

## Known Limitations

The following features are not yet implemented:
- **No emoji support**: Unicode emojis may not render correctly
- **No font selection**: Hardcoded to SF Mono font
- **No configurability**: Grid size, colors, and keybindings are hardcoded
- **No font selection**: Hardcoded to SF Mono font (though size is adjustable)
- **Limited configurability**: Grid size, colors, and keybindings are hardcoded
- **Limited AI tool compatibility**: Works with Claude and Gemini models, but not with Codex

## License
Expand Down
2 changes: 2 additions & 0 deletions src/c.zig
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ pub const SDL_Init = c_import.SDL_Init;
pub const SDL_Quit = c_import.SDL_Quit;
pub const SDL_CreateWindow = c_import.SDL_CreateWindow;
pub const SDL_DestroyWindow = c_import.SDL_DestroyWindow;
pub const SDL_SetWindowPosition = c_import.SDL_SetWindowPosition;
pub const SDL_CreateRenderer = c_import.SDL_CreateRenderer;
pub const SDL_DestroyRenderer = c_import.SDL_DestroyRenderer;
pub const SDL_SetRenderDrawColor = c_import.SDL_SetRenderDrawColor;
Expand Down Expand Up @@ -44,6 +45,7 @@ pub const SDL_EVENT_TEXT_INPUT = c_import.SDL_EVENT_TEXT_INPUT;
pub const SDL_EVENT_MOUSE_BUTTON_DOWN = c_import.SDL_EVENT_MOUSE_BUTTON_DOWN;
pub const SDL_EVENT_MOUSE_WHEEL = c_import.SDL_EVENT_MOUSE_WHEEL;
pub const SDL_EVENT_WINDOW_RESIZED = c_import.SDL_EVENT_WINDOW_RESIZED;
pub const SDL_EVENT_WINDOW_MOVED = c_import.SDL_EVENT_WINDOW_MOVED;

pub const SDL_SetTextureScaleMode = c_import.SDL_SetTextureScaleMode;
pub const SDL_SCALEMODE_LINEAR = c_import.SDL_SCALEMODE_LINEAR;
Expand Down
161 changes: 161 additions & 0 deletions src/config.zig
Original file line number Diff line number Diff line change
@@ -0,0 +1,161 @@
const std = @import("std");
const fs = std.fs;
const json = std.json;

pub const Config = struct {
font_size: i32,
window_width: i32,
window_height: i32,
window_x: i32,
window_y: i32,

pub fn load(allocator: std.mem.Allocator) LoadError!Config {
const config_path = try getConfigPath(allocator);
defer allocator.free(config_path);

const file = fs.openFileAbsolute(config_path, .{}) catch |err| switch (err) {
error.FileNotFound => return error.ConfigNotFound,
else => return err,
};
defer file.close();

const content = try file.readToEndAlloc(allocator, 1024 * 1024);
defer allocator.free(content);

const parsed = try json.parseFromSlice(json.Value, allocator, content, .{});
defer parsed.deinit();

const root = parsed.value;
if (root != .object) return error.InvalidConfig;
const obj = root.object;

const font_size_val = obj.get("font_size") orelse return error.InvalidConfig;
const window_width_val = obj.get("window_width") orelse return error.InvalidConfig;
const window_height_val = obj.get("window_height") orelse return error.InvalidConfig;
const window_x_val = obj.get("window_x") orelse return error.InvalidConfig;
const window_y_val = obj.get("window_y") orelse return error.InvalidConfig;

if (font_size_val != .integer or window_width_val != .integer or window_height_val != .integer or
window_x_val != .integer or window_y_val != .integer)
{
return error.InvalidConfig;
}

return Config{
.font_size = @intCast(font_size_val.integer),
.window_width = @intCast(window_width_val.integer),
.window_height = @intCast(window_height_val.integer),
.window_x = @intCast(window_x_val.integer),
.window_y = @intCast(window_y_val.integer),
};
}

pub fn save(self: Config, allocator: std.mem.Allocator) SaveError!void {
const config_path = try getConfigPath(allocator);
defer allocator.free(config_path);

const config_dir = fs.path.dirname(config_path) orelse return error.InvalidPath;
fs.makeDirAbsolute(config_dir) catch |err| switch (err) {
error.PathAlreadyExists => {},
else => return err,
};

const content = try std.fmt.allocPrint(
allocator,
\\{{
\\ "font_size": {d},
\\ "window_width": {d},
\\ "window_height": {d},
\\ "window_x": {d},
\\ "window_y": {d}
\\}}
\\
,
.{ self.font_size, self.window_width, self.window_height, self.window_x, self.window_y },
);
defer allocator.free(content);

const file = try fs.createFileAbsolute(config_path, .{ .truncate = true });
defer file.close();
try file.writeAll(content);
}

fn getConfigPath(allocator: std.mem.Allocator) ![]u8 {
const home = std.posix.getenv("HOME") orelse return error.HomeNotFound;
return try fs.path.join(allocator, &[_][]const u8{ home, ".config", "architect", "config.json" });
}
};

pub const LoadError = error{
ConfigNotFound,
InvalidConfig,
HomeNotFound,
InvalidPath,
OutOfMemory,
} || fs.File.OpenError || fs.File.ReadError || json.ParseError(json.Scanner);

pub const SaveError = error{
HomeNotFound,
InvalidPath,
OutOfMemory,
} || fs.File.OpenError || fs.File.WriteError || fs.Dir.MakeError;

test "Config - save and load" {
const allocator = std.testing.allocator;

const test_config = Config{
.font_size = 16,
.window_width = 1920,
.window_height = 1080,
.window_x = 100,
.window_y = 100,
};

const test_dir = try std.fs.cwd().makeOpenPath("test_config", .{});
defer std.fs.cwd().deleteTree("test_config") catch {};

const test_path = try fs.path.join(allocator, &[_][]const u8{ "test_config", "config.json" });
defer allocator.free(test_path);

const content = try std.fmt.allocPrint(
allocator,
\\{{
\\ "font_size": {d},
\\ "window_width": {d},
\\ "window_height": {d},
\\ "window_x": {d},
\\ "window_y": {d}
\\}}
\\
,
.{ test_config.font_size, test_config.window_width, test_config.window_height, test_config.window_x, test_config.window_y },
);
defer allocator.free(content);

try test_dir.writeFile(.{ .sub_path = "config.json", .data = content });

const file = try fs.cwd().openFile(test_path, .{});
defer file.close();

const read_content = try file.readToEndAlloc(allocator, 1024 * 1024);
defer allocator.free(read_content);

const parsed = try json.parseFromSlice(json.Value, allocator, read_content, .{});
defer parsed.deinit();

const root = parsed.value;
try std.testing.expect(root == .object);
const obj = root.object;

const font_size_val = obj.get("font_size").?;
const window_width_val = obj.get("window_width").?;
const window_height_val = obj.get("window_height").?;
const window_x_val = obj.get("window_x").?;
const window_y_val = obj.get("window_y").?;

try std.testing.expectEqual(@as(i64, 16), font_size_val.integer);
try std.testing.expectEqual(@as(i64, 1920), window_width_val.integer);
try std.testing.expectEqual(@as(i64, 1080), window_height_val.integer);
try std.testing.expectEqual(@as(i64, 100), window_x_val.integer);
try std.testing.expectEqual(@as(i64, 100), window_y_val.integer);
}
Loading