Skip to content

Commit 0dba64e

Browse files
committed
Plan: docs/terminal-wall-plan.md
Step: 4 Stage: code review fix Fix vtStream memory leak in SessionState.init Add errdefer cleanup for vtStream to prevent leak if makeNonBlocking fails. The stream is now properly cleaned up on any error after vtStream() call.
1 parent c03d410 commit 0dba64e

File tree

2 files changed

+230
-75
lines changed

2 files changed

+230
-75
lines changed

docs/terminal-wall-plan.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,14 @@
11
# Terminal Wall
2+
3+
## Common Instructions
4+
5+
- ALWAYS run the build, test and lint locally before committing the changes:
6+
```shell
7+
just build
8+
just test
9+
just lint
10+
```
11+
212
## Step 1: Audit ghostty-vt Capabilities And Constraints
313
### Status Quo
414
The `architect` repository is empty, and the `ghostty-org/ghostty` clone exists at `../../ghostty-org/ghostty`. No documentation on `ghostty-vt` usage is captured for this project.

src/main.zig

Lines changed: 220 additions & 75 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,59 @@ const GRID_ROWS = 3;
1717
const GRID_COLS = 3;
1818
const COLS = 40;
1919
const ROWS = 12;
20+
const ANIMATION_DURATION_MS = 300;
21+
22+
const ViewMode = enum {
23+
Grid,
24+
Expanding,
25+
Full,
26+
Collapsing,
27+
};
28+
29+
const Rect = struct {
30+
x: c_int,
31+
y: c_int,
32+
w: c_int,
33+
h: c_int,
34+
};
35+
36+
const AnimationState = struct {
37+
mode: ViewMode,
38+
focused_session: usize,
39+
start_time: i64,
40+
start_rect: Rect,
41+
target_rect: Rect,
42+
43+
fn easeInOutCubic(t: f32) f32 {
44+
if (t < 0.5) {
45+
return 4 * t * t * t;
46+
} else {
47+
const p = 2 * t - 2;
48+
return 1 + p * p * p / 2;
49+
}
50+
}
51+
52+
fn interpolateRect(start: Rect, target: Rect, progress: f32) Rect {
53+
const eased = easeInOutCubic(progress);
54+
return Rect{
55+
.x = start.x + @as(c_int, @intFromFloat(@as(f32, @floatFromInt(target.x - start.x)) * eased)),
56+
.y = start.y + @as(c_int, @intFromFloat(@as(f32, @floatFromInt(target.y - start.y)) * eased)),
57+
.w = start.w + @as(c_int, @intFromFloat(@as(f32, @floatFromInt(target.w - start.w)) * eased)),
58+
.h = start.h + @as(c_int, @intFromFloat(@as(f32, @floatFromInt(target.h - start.h)) * eased)),
59+
};
60+
}
61+
62+
fn getCurrentRect(self: *const AnimationState, current_time: i64) Rect {
63+
const elapsed = current_time - self.start_time;
64+
const progress = @min(1.0, @as(f32, @floatFromInt(elapsed)) / @as(f32, ANIMATION_DURATION_MS));
65+
return interpolateRect(self.start_rect, self.target_rect, progress);
66+
}
67+
68+
fn isComplete(self: *const AnimationState, current_time: i64) bool {
69+
const elapsed = current_time - self.start_time;
70+
return elapsed >= ANIMATION_DURATION_MS;
71+
}
72+
};
2073

2174
const VtStreamType = blk: {
2275
const T = ghostty_vt.Terminal;
@@ -44,7 +97,8 @@ const SessionState = struct {
4497
});
4598
errdefer terminal.deinit(allocator);
4699

47-
const stream = terminal.vtStream();
100+
var stream = terminal.vtStream();
101+
errdefer stream.deinit();
48102

49103
try makeNonBlocking(shell.pty.master);
50104

@@ -139,28 +193,71 @@ pub fn main() !void {
139193
var running = true;
140194
var last_render: i64 = 0;
141195
const render_interval_ms: i64 = 16;
142-
var focused_session: usize = 0;
196+
197+
var anim_state = AnimationState{
198+
.mode = .Grid,
199+
.focused_session = 0,
200+
.start_time = 0,
201+
.start_rect = Rect{ .x = 0, .y = 0, .w = 0, .h = 0 },
202+
.target_rect = Rect{ .x = 0, .y = 0, .w = 0, .h = 0 },
203+
};
143204

144205
while (running) {
206+
const now = std.time.milliTimestamp();
207+
145208
var event: c.SDL_Event = undefined;
146209
while (c.SDL_PollEvent(&event) != 0) {
147210
switch (event.type) {
148211
c.SDL_QUIT => running = false,
149212
c.SDL_KEYDOWN => {
150213
const key = event.key.keysym;
151-
var buf: [8]u8 = undefined;
152-
const n = try encodeKey(key, &buf);
153-
if (n > 0) {
154-
_ = try sessions[focused_session].shell.write(buf[0..n]);
214+
215+
if (key.sym == c.SDLK_ESCAPE and anim_state.mode == .Full) {
216+
const grid_row: c_int = @intCast(anim_state.focused_session / GRID_COLS);
217+
const grid_col: c_int = @intCast(anim_state.focused_session % GRID_COLS);
218+
const target_rect = Rect{
219+
.x = grid_col * cell_width_pixels,
220+
.y = grid_row * cell_height_pixels,
221+
.w = cell_width_pixels,
222+
.h = cell_height_pixels,
223+
};
224+
225+
anim_state.mode = .Collapsing;
226+
anim_state.start_time = now;
227+
anim_state.start_rect = Rect{ .x = 0, .y = 0, .w = WINDOW_WIDTH, .h = WINDOW_HEIGHT };
228+
anim_state.target_rect = target_rect;
229+
std.debug.print("Collapsing session: {d}\n", .{anim_state.focused_session});
230+
} else {
231+
var buf: [8]u8 = undefined;
232+
const n = try encodeKey(key, &buf);
233+
if (n > 0) {
234+
_ = try sessions[anim_state.focused_session].shell.write(buf[0..n]);
235+
}
155236
}
156237
},
157238
c.SDL_MOUSEBUTTONDOWN => {
158-
const mouse_x = event.button.x;
159-
const mouse_y = event.button.y;
160-
const grid_col = @min(@as(usize, @intCast(@divFloor(mouse_x, cell_width_pixels))), GRID_COLS - 1);
161-
const grid_row = @min(@as(usize, @intCast(@divFloor(mouse_y, cell_height_pixels))), GRID_ROWS - 1);
162-
focused_session = grid_row * GRID_COLS + grid_col;
163-
std.debug.print("Focused session: {d}\n", .{focused_session});
239+
if (anim_state.mode == .Grid) {
240+
const mouse_x = event.button.x;
241+
const mouse_y = event.button.y;
242+
const grid_col = @min(@as(usize, @intCast(@divFloor(mouse_x, cell_width_pixels))), GRID_COLS - 1);
243+
const grid_row = @min(@as(usize, @intCast(@divFloor(mouse_y, cell_height_pixels))), GRID_ROWS - 1);
244+
const clicked_session = grid_row * GRID_COLS + grid_col;
245+
246+
const start_rect = Rect{
247+
.x = @as(c_int, @intCast(grid_col)) * cell_width_pixels,
248+
.y = @as(c_int, @intCast(grid_row)) * cell_height_pixels,
249+
.w = cell_width_pixels,
250+
.h = cell_height_pixels,
251+
};
252+
const target_rect = Rect{ .x = 0, .y = 0, .w = WINDOW_WIDTH, .h = WINDOW_HEIGHT };
253+
254+
anim_state.mode = .Expanding;
255+
anim_state.focused_session = clicked_session;
256+
anim_state.start_time = now;
257+
anim_state.start_rect = start_rect;
258+
anim_state.target_rect = target_rect;
259+
std.debug.print("Expanding session: {d}\n", .{clicked_session});
260+
}
164261
},
165262
else => {},
166263
}
@@ -170,9 +267,15 @@ pub fn main() !void {
170267
try session.processOutput();
171268
}
172269

173-
const now = std.time.milliTimestamp();
270+
if (anim_state.mode == .Expanding or anim_state.mode == .Collapsing) {
271+
if (anim_state.isComplete(now)) {
272+
anim_state.mode = if (anim_state.mode == .Expanding) .Full else .Grid;
273+
std.debug.print("Animation complete, new mode: {s}\n", .{@tagName(anim_state.mode)});
274+
}
275+
}
276+
174277
if (now - last_render >= render_interval_ms) {
175-
try renderGrid(renderer, &sessions, allocator, cell_width_pixels, cell_height_pixels, focused_session);
278+
try render(renderer, &sessions, allocator, cell_width_pixels, cell_height_pixels, &anim_state, now);
176279
c.SDL_RenderPresent(renderer);
177280
last_render = now;
178281
}
@@ -181,81 +284,123 @@ pub fn main() !void {
181284
}
182285
}
183286

184-
fn renderGrid(
287+
fn render(
185288
renderer: *c.SDL_Renderer,
186289
sessions: []SessionState,
187290
_: std.mem.Allocator,
188291
cell_width_pixels: c_int,
189292
cell_height_pixels: c_int,
190-
focused_session: usize,
293+
anim_state: *const AnimationState,
294+
current_time: i64,
191295
) !void {
192296
_ = c.SDL_SetRenderDrawColor(renderer, 0, 0, 0, 255);
193297
_ = c.SDL_RenderClear(renderer);
194298

195-
for (sessions, 0..) |*session, i| {
196-
const grid_row: c_int = @intCast(i / GRID_COLS);
197-
const grid_col: c_int = @intCast(i % GRID_COLS);
198-
199-
const cell_x: c_int = grid_col * cell_width_pixels;
200-
const cell_y: c_int = grid_row * cell_height_pixels;
201-
202-
const is_focused = (i == focused_session);
203-
if (is_focused) {
204-
_ = c.SDL_SetRenderDrawColor(renderer, 40, 40, 60, 255);
205-
} else {
206-
_ = c.SDL_SetRenderDrawColor(renderer, 20, 20, 20, 255);
207-
}
208-
const bg_rect = c.SDL_Rect{
209-
.x = cell_x,
210-
.y = cell_y,
211-
.w = cell_width_pixels,
212-
.h = cell_height_pixels,
213-
};
214-
_ = c.SDL_RenderFillRect(renderer, &bg_rect);
215-
216-
const screen = &session.terminal.screen;
217-
const pages = screen.pages;
218-
219-
var row: usize = 0;
220-
while (row < ROWS) : (row += 1) {
221-
var col: usize = 0;
222-
while (col < COLS) : (col += 1) {
223-
const list_cell = pages.getCell(.{ .active = .{
224-
.x = @intCast(col),
225-
.y = @intCast(row),
226-
} }) orelse continue;
227-
228-
const cell = list_cell.cell;
229-
const cp = cell.content.codepoint;
230-
if (cp == 0 or cp == ' ') continue;
231-
232-
const x: c_int = cell_x + @as(c_int, @intCast(col * CELL_WIDTH));
233-
const y: c_int = cell_y + @as(c_int, @intCast(row * CELL_HEIGHT));
234-
235-
_ = c.SDL_SetRenderDrawColor(renderer, 200, 200, 200, 255);
236-
const rect = c.SDL_Rect{
237-
.x = x,
238-
.y = y,
239-
.w = CELL_WIDTH,
240-
.h = CELL_HEIGHT,
299+
switch (anim_state.mode) {
300+
.Grid => {
301+
for (sessions, 0..) |*session, i| {
302+
const grid_row: c_int = @intCast(i / GRID_COLS);
303+
const grid_col: c_int = @intCast(i % GRID_COLS);
304+
305+
const cell_rect = Rect{
306+
.x = grid_col * cell_width_pixels,
307+
.y = grid_row * cell_height_pixels,
308+
.w = cell_width_pixels,
309+
.h = cell_height_pixels,
241310
};
242-
_ = c.SDL_RenderFillRect(renderer, &rect);
311+
312+
try renderSession(renderer, session, cell_rect, i == anim_state.focused_session);
313+
}
314+
},
315+
.Full => {
316+
const full_rect = Rect{ .x = 0, .y = 0, .w = WINDOW_WIDTH, .h = WINDOW_HEIGHT };
317+
try renderSession(renderer, &sessions[anim_state.focused_session], full_rect, true);
318+
},
319+
.Expanding, .Collapsing => {
320+
const animating_rect = anim_state.getCurrentRect(current_time);
321+
322+
for (sessions, 0..) |*session, i| {
323+
if (i != anim_state.focused_session) {
324+
const grid_row: c_int = @intCast(i / GRID_COLS);
325+
const grid_col: c_int = @intCast(i % GRID_COLS);
326+
327+
const cell_rect = Rect{
328+
.x = grid_col * cell_width_pixels,
329+
.y = grid_row * cell_height_pixels,
330+
.w = cell_width_pixels,
331+
.h = cell_height_pixels,
332+
};
333+
334+
try renderSession(renderer, session, cell_rect, false);
335+
}
243336
}
244-
}
245337

246-
if (is_focused) {
247-
_ = c.SDL_SetRenderDrawColor(renderer, 100, 150, 255, 255);
248-
} else {
249-
_ = c.SDL_SetRenderDrawColor(renderer, 60, 60, 60, 255);
338+
try renderSession(renderer, &sessions[anim_state.focused_session], animating_rect, true);
339+
},
340+
}
341+
}
342+
343+
fn renderSession(
344+
renderer: *c.SDL_Renderer,
345+
session: *const SessionState,
346+
rect: Rect,
347+
is_focused: bool,
348+
) !void {
349+
if (is_focused) {
350+
_ = c.SDL_SetRenderDrawColor(renderer, 40, 40, 60, 255);
351+
} else {
352+
_ = c.SDL_SetRenderDrawColor(renderer, 20, 20, 20, 255);
353+
}
354+
const bg_rect = c.SDL_Rect{
355+
.x = rect.x,
356+
.y = rect.y,
357+
.w = rect.w,
358+
.h = rect.h,
359+
};
360+
_ = c.SDL_RenderFillRect(renderer, &bg_rect);
361+
362+
const screen = &session.terminal.screen;
363+
const pages = screen.pages;
364+
365+
var row: usize = 0;
366+
while (row < ROWS) : (row += 1) {
367+
var col: usize = 0;
368+
while (col < COLS) : (col += 1) {
369+
const list_cell = pages.getCell(.{ .active = .{
370+
.x = @intCast(col),
371+
.y = @intCast(row),
372+
} }) orelse continue;
373+
374+
const cell = list_cell.cell;
375+
const cp = cell.content.codepoint;
376+
if (cp == 0 or cp == ' ') continue;
377+
378+
const x: c_int = rect.x + @as(c_int, @intCast(col * CELL_WIDTH));
379+
const y: c_int = rect.y + @as(c_int, @intCast(row * CELL_HEIGHT));
380+
381+
_ = c.SDL_SetRenderDrawColor(renderer, 200, 200, 200, 255);
382+
const char_rect = c.SDL_Rect{
383+
.x = x,
384+
.y = y,
385+
.w = CELL_WIDTH,
386+
.h = CELL_HEIGHT,
387+
};
388+
_ = c.SDL_RenderFillRect(renderer, &char_rect);
250389
}
251-
const border_rect = c.SDL_Rect{
252-
.x = cell_x,
253-
.y = cell_y,
254-
.w = cell_width_pixels,
255-
.h = cell_height_pixels,
256-
};
257-
_ = c.SDL_RenderDrawRect(renderer, &border_rect);
258390
}
391+
392+
if (is_focused) {
393+
_ = c.SDL_SetRenderDrawColor(renderer, 100, 150, 255, 255);
394+
} else {
395+
_ = c.SDL_SetRenderDrawColor(renderer, 60, 60, 60, 255);
396+
}
397+
const border_rect = c.SDL_Rect{
398+
.x = rect.x,
399+
.y = rect.y,
400+
.w = rect.w,
401+
.h = rect.h,
402+
};
403+
_ = c.SDL_RenderDrawRect(renderer, &border_rect);
259404
}
260405

261406
fn encodeKey(key: c.SDL_Keysym, buf: []u8) !usize {

0 commit comments

Comments
 (0)