Skip to content

Commit 4dc765f

Browse files
authored
Merge pull request #74 from forketyfork/fix/nvim-exit-delay-smear-cursor
Fix nvim exit delay and smear-cursor plugin rendering
2 parents 8b646b5 + af886ce commit 4dc765f

File tree

6 files changed

+139
-14
lines changed

6 files changed

+139
-14
lines changed

build.zig

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,13 @@ pub fn build(b: *std.Build) void {
2828
);
2929
}
3030

31+
if (b.lazyDependency("libxev", .{
32+
.target = target,
33+
.optimize = optimize,
34+
})) |dep| {
35+
exe_mod.addImport("xev", dep.module("xev"));
36+
}
37+
3138
const exe = b.addExecutable(.{
3239
.name = "architect",
3340
.root_module = exe_mod,

build.zig.zon

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,10 @@
88
.url = "https://github.com/ghostty-org/ghostty/archive/f705b9f46a4083d8053cfa254898c164af46ff34.tar.gz",
99
.hash = "122022d77cfd6d901de978a2667797a18d82f7ce2fd6c40d4028d6db603499dc9679",
1010
},
11+
.libxev = .{
12+
.url = "https://deps.files.ghostty.org/libxev-34fa50878aec6e5fa8f532867001ab3c36fae23e.tar.gz",
13+
.hash = "libxev-0.0.0-86vtc4IcEwCqEYxEYoN_3KXmc6A9VLcm22aVImfvecYs",
14+
},
1115
},
1216
.paths = .{
1317
"build.zig",

src/font.zig

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,18 @@ pub const Font = struct {
4747
/// Limit cached glyph textures to avoid unbounded GPU/heap growth.
4848
const MAX_GLYPH_CACHE_ENTRIES: usize = 4096;
4949

50+
/// Maximum byte length for a single glyph string to prevent abuse from
51+
/// malicious or malformed terminal output. 256 bytes allows for reasonable
52+
/// grapheme clusters including emoji sequences and combining characters
53+
/// while protecting against memory exhaustion attacks.
54+
const MAX_GLYPH_BYTE_LENGTH: usize = 256;
55+
56+
/// Maximum codepoints in a single grapheme cluster before chunking.
57+
/// Chosen to balance rendering performance with memory usage. Values above
58+
/// this threshold are split into smaller segments to avoid creating
59+
/// excessively large textures (e.g., cursor trail effects with 120+ chars).
60+
const MAX_CLUSTER_SIZE: usize = 32;
61+
5062
const CacheEntry = struct {
5163
texture: *c.SDL_Texture,
5264
seq: u64,
@@ -197,6 +209,22 @@ pub const Font = struct {
197209
if (codepoints.len == 0) return;
198210
if (codepoints.len == 1 and codepoints[0] == 0) return;
199211

212+
if (codepoints.len > MAX_CLUSTER_SIZE) {
213+
const chars_per_chunk = MAX_CLUSTER_SIZE;
214+
const cell_width = @max(1, @divTrunc(target_width, @as(c_int, @intCast(codepoints.len))));
215+
216+
var offset: usize = 0;
217+
while (offset < codepoints.len) {
218+
const chunk_end = @min(offset + chars_per_chunk, codepoints.len);
219+
const chunk = codepoints[offset..chunk_end];
220+
const chunk_x = x + @as(c_int, @intCast(offset)) * cell_width;
221+
const chunk_width = @as(c_int, @intCast(chunk.len)) * cell_width;
222+
try self.renderCluster(chunk, chunk_x, y, chunk_width, target_height, fg_color, variant);
223+
offset = chunk_end;
224+
}
225+
return;
226+
}
227+
200228
const effective_variant = self.effectiveVariant(variant, codepoints);
201229

202230
var total_bytes: usize = 0;
@@ -265,6 +293,22 @@ pub const Font = struct {
265293
if (codepoints.len == 0) return;
266294
if (codepoints.len == 1 and codepoints[0] == 0) return;
267295

296+
if (codepoints.len > MAX_CLUSTER_SIZE) {
297+
const chars_per_chunk = MAX_CLUSTER_SIZE;
298+
const cell_width = @max(1, @divTrunc(target_width, @as(c_int, @intCast(codepoints.len))));
299+
300+
var offset: usize = 0;
301+
while (offset < codepoints.len) {
302+
const chunk_end = @min(offset + chars_per_chunk, codepoints.len);
303+
const chunk = codepoints[offset..chunk_end];
304+
const chunk_x = x + @as(c_int, @intCast(offset)) * cell_width;
305+
const chunk_width = @as(c_int, @intCast(chunk.len)) * cell_width;
306+
try self.renderClusterFill(chunk, chunk_x, y, chunk_width, target_height, fg_color, variant);
307+
offset = chunk_end;
308+
}
309+
return;
310+
}
311+
268312
const effective_variant = self.effectiveVariant(variant, codepoints);
269313

270314
var total_bytes: usize = 0;
@@ -350,6 +394,11 @@ pub const Font = struct {
350394
}
351395

352396
fn getGlyphTexture(self: *Font, utf8: []const u8, fg_color: c.SDL_Color, fallback: Fallback, variant: Variant) RenderGlyphError!*c.SDL_Texture {
397+
if (utf8.len > MAX_GLYPH_BYTE_LENGTH) {
398+
log.warn("Refusing to render excessively long glyph string: {d} bytes", .{utf8.len});
399+
return error.GlyphRenderFailed;
400+
}
401+
353402
const key = GlyphKey{
354403
.hash = std.hash.Wyhash.hash(0, utf8),
355404
.color = packColor(if (fallback == .emoji) WHITE else fg_color),

src/main.zig

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
const std = @import("std");
44
const builtin = @import("builtin");
55
const posix = std.posix;
6+
const xev = @import("xev");
67
const app_state = @import("app/app_state.zig");
78
const notify = @import("session/notify.zig");
89
const session_state = @import("session/state.zig");
@@ -241,14 +242,17 @@ pub fn main() !void {
241242
}
242243
}
243244

245+
var loop = try xev.Loop.init(.{});
246+
defer loop.deinit();
247+
244248
for (0..GRID_ROWS * GRID_COLS) |i| {
245249
var session_buf: [16]u8 = undefined;
246250
const session_z = try std.fmt.bufPrintZ(&session_buf, "{d}", .{i});
247251
sessions[i] = try SessionState.init(allocator, i, shell_path, size, session_z, notify_sock);
248252
init_count += 1;
249253
}
250254

251-
try sessions[0].ensureSpawned();
255+
try sessions[0].ensureSpawnedWithLoop(&loop);
252256

253257
defer {
254258
for (&sessions) |*session| {
@@ -417,7 +421,7 @@ pub fn main() !void {
417421
) orelse continue;
418422

419423
var session = &sessions[hovered_session];
420-
try session.ensureSpawned();
424+
try session.ensureSpawnedWithLoop(&loop);
421425

422426
const escaped = shellQuotePath(allocator, drop_path) catch |err| {
423427
std.debug.print("Failed to escape dropped path: {}\n", .{err});
@@ -514,7 +518,7 @@ pub fn main() !void {
514518

515519
defer if (cwd_buf) |buf| allocator.free(buf);
516520

517-
try sessions[next_free_idx].ensureSpawnedWithDir(cwd_z);
521+
try sessions[next_free_idx].ensureSpawnedWithDir(cwd_z, &loop);
518522
sessions[next_free_idx].status = .running;
519523
sessions[next_free_idx].attention = false;
520524

@@ -761,6 +765,8 @@ pub fn main() !void {
761765
}
762766
}
763767

768+
try loop.run(.no_wait);
769+
764770
var any_session_dirty = false;
765771
var has_scroll_inertia = false;
766772
for (&sessions) |*session| {

src/render/renderer.zig

Lines changed: 15 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -168,8 +168,18 @@ fn renderSessionContent(
168168
term_cols: u16,
169169
term_rows: u16,
170170
) RenderError!void {
171+
if (!session.spawned) return;
172+
173+
const terminal = session.terminal orelse {
174+
log.err("session {d} is spawned but terminal is null!", .{session.id});
175+
return;
176+
};
177+
171178
const base_bg = c.SDL_Color{ .r = 14, .g = 17, .b = 22, .a = 255 };
172-
const session_bg_color = base_bg;
179+
const session_bg_color = if (terminal.colors.background.get()) |rgb|
180+
c.SDL_Color{ .r = rgb.r, .g = rgb.g, .b = rgb.b, .a = 255 }
181+
else
182+
base_bg;
173183

174184
_ = c.SDL_SetRenderDrawColor(renderer, session_bg_color.r, session_bg_color.g, session_bg_color.b, session_bg_color.a);
175185
const bg_rect = c.SDL_FRect{
@@ -179,13 +189,6 @@ fn renderSessionContent(
179189
.h = @floatFromInt(rect.h),
180190
};
181191
_ = c.SDL_RenderFillRect(renderer, &bg_rect);
182-
183-
if (!session.spawned) return;
184-
185-
const terminal = session.terminal orelse {
186-
log.err("session {d} is spawned but terminal is null!", .{session.id});
187-
return;
188-
};
189192
const screen = terminal.screens.active;
190193
const cursor_visible = terminal.modes.get(.cursor_visible);
191194
const pages = screen.pages;
@@ -248,7 +251,10 @@ fn renderSessionContent(
248251

249252
const style = list_cell.style();
250253
var fg_color = getCellColor(style.fg_color, default_fg);
251-
var bg_color = getCellColor(style.bg_color, session_bg_color);
254+
var bg_color = if (style.bg(list_cell.cell, &terminal.colors.palette.current)) |rgb|
255+
c.SDL_Color{ .r = rgb.r, .g = rgb.g, .b = rgb.b, .a = 255 }
256+
else
257+
session_bg_color;
252258
const variant = chooseVariant(style);
253259

254260
if (style.flags.inverse) {

src/session/state.zig

Lines changed: 55 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
const std = @import("std");
22
const posix = std.posix;
33
const builtin = @import("builtin");
4+
const xev = @import("xev");
45
const ghostty_vt = @import("ghostty-vt");
56
const shell_mod = @import("../shell.zig");
67
const pty_mod = @import("../pty.zig");
@@ -72,6 +73,10 @@ pub const SessionState = struct {
7273
hovered_link_start: ?ghostty_vt.Pin = null,
7374
hovered_link_end: ?ghostty_vt.Pin = null,
7475
pending_write: std.ArrayListUnmanaged(u8) = .empty,
76+
/// Process watcher for event-driven exit detection.
77+
process_watcher: ?xev.Process = null,
78+
/// Completion structure for process wait callback.
79+
process_completion: xev.Completion = .{},
7580

7681
pub const InitError = shell_mod.Shell.SpawnError || MakeNonBlockingError || error{
7782
DivisionByZero,
@@ -85,6 +90,9 @@ pub const SessionState = struct {
8590
StringAllocOutOfMemory,
8691
StyleSetNeedsRehash,
8792
StyleSetOutOfMemory,
93+
SystemResources,
94+
SystemFdQuotaExceeded,
95+
InvalidArgument,
8896
};
8997

9098
pub fn init(
@@ -115,10 +123,14 @@ pub const SessionState = struct {
115123
}
116124

117125
pub fn ensureSpawned(self: *SessionState) InitError!void {
118-
return self.ensureSpawnedWithDir(null);
126+
return self.ensureSpawnedWithDir(null, null);
119127
}
120128

121-
pub fn ensureSpawnedWithDir(self: *SessionState, working_dir: ?[:0]const u8) InitError!void {
129+
pub fn ensureSpawnedWithLoop(self: *SessionState, loop: *xev.Loop) InitError!void {
130+
return self.ensureSpawnedWithDir(null, loop);
131+
}
132+
133+
pub fn ensureSpawnedWithDir(self: *SessionState, working_dir: ?[:0]const u8, loop_opt: ?*xev.Loop) InitError!void {
122134
if (self.spawned) return;
123135

124136
const shell = try shell_mod.Shell.spawn(
@@ -153,6 +165,21 @@ pub const SessionState = struct {
153165
self.cwd_dirty = true;
154166
self.dirty = true;
155167

168+
if (loop_opt) |loop| {
169+
var process = try xev.Process.init(shell.child_pid);
170+
errdefer process.deinit();
171+
172+
process.wait(
173+
loop,
174+
&self.process_completion,
175+
SessionState,
176+
self,
177+
processExitCallback,
178+
);
179+
180+
self.process_watcher = process;
181+
}
182+
156183
log.debug("spawned session {d}", .{self.id});
157184

158185
self.processOutput() catch {};
@@ -177,6 +204,9 @@ pub const SessionState = struct {
177204
if (self.cwd_path) |path| {
178205
allocator.free(path);
179206
}
207+
if (self.process_watcher) |*watcher| {
208+
watcher.deinit();
209+
}
180210
if (self.spawned) {
181211
if (self.stream) |*stream| {
182212
stream.deinit();
@@ -204,6 +234,25 @@ pub const SessionState = struct {
204234
StyleSetOutOfMemory,
205235
};
206236

237+
fn processExitCallback(
238+
self_opt: ?*SessionState,
239+
_: *xev.Loop,
240+
_: *xev.Completion,
241+
r: xev.Process.WaitError!u32,
242+
) xev.CallbackAction {
243+
const self = self_opt orelse return .disarm;
244+
const exit_code = r catch |err| {
245+
log.err("process wait error for session {d}: {}", .{ self.id, err });
246+
return .disarm;
247+
};
248+
249+
self.dead = true;
250+
self.dirty = true;
251+
log.info("session {d} process exited with code {d}", .{ self.id, exit_code });
252+
253+
return .disarm;
254+
}
255+
207256
pub fn checkAlive(self: *SessionState) void {
208257
if (!self.spawned or self.dead) return;
209258

@@ -223,6 +272,10 @@ pub const SessionState = struct {
223272

224273
self.clearSelection();
225274
self.pending_write.clearAndFree(self.allocator);
275+
if (self.process_watcher) |*watcher| {
276+
watcher.deinit();
277+
self.process_watcher = null;
278+
}
226279
if (self.spawned) {
227280
if (self.stream) |*stream| {
228281
stream.deinit();

0 commit comments

Comments
 (0)