Skip to content

Commit 42092e7

Browse files
committed
Stream CRLF normalization directly into libghostty
The old path allocated up to 131 KB of stack scratch (plus heap fallback with a silent-truncation failure mode) and did two passes over the input: one to count bare \n, one to copy-and-insert \r. Replace with a single streaming pass that writes maximal raw segments directly to libghostty's VT parser via vtWrite and emits "\r\n" inline at each bare \n. libghostty's state machine already handles arbitrary chunking (it's how the filter feeds data anyway), so no scratch buffer is needed. Zero allocation, no truncation risk, no behaviour change.
1 parent 819098f commit 42092e7

1 file changed

Lines changed: 18 additions & 39 deletions

File tree

src/module.zig

Lines changed: 18 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -141,9 +141,6 @@ fn fnWriteInput(raw_env: ?*c.emacs_env, _: isize, args: [*c]c.emacs_value, _: ?*
141141
defer vt_log_env = null;
142142
}
143143

144-
// Normalize CRLF: Emacs PTYs lack ONLCR, so bare \n arrives
145-
// without \r. Insert \r before every bare \n.
146-
// Done here in Zig to avoid Elisp unibyte→multibyte corruption.
147144
const raw = data.?;
148145

149146
// Respond to OSC 4/10/11 color queries BEFORE feeding libghostty.
@@ -154,44 +151,26 @@ fn fnWriteInput(raw_env: ?*c.emacs_env, _: isize, args: [*c]c.emacs_value, _: ?*
154151
// the program discards our reply as noise.
155152
extractOscColorQueries(env, term, raw);
156153

157-
// Count bare \n to determine output size.
158-
var extra_cr: usize = 0;
159-
for (0..raw.len) |i| {
160-
if (raw[i] == '\n' and (i == 0 or raw[i - 1] != '\r')) {
161-
extra_cr += 1;
154+
// Normalize CRLF by streaming directly into libghostty's parser.
155+
// Emacs PTYs lack ONLCR, so bare \n arrives without \r — insert
156+
// one before each bare \n by feeding the preceding segment verbatim
157+
// and then "\r\n". libghostty's VT state machine handles arbitrary
158+
// chunking (that's how the process filter already works), so no
159+
// scratch buffer, no allocation, no truncation fallback.
160+
var seg_start: usize = 0;
161+
var prev_was_cr: bool = false;
162+
for (raw, 0..) |ch, i| {
163+
if (ch == '\n' and !prev_was_cr) {
164+
if (i > seg_start) term.vtWrite(raw[seg_start..i]);
165+
term.vtWrite("\r\n");
166+
seg_start = i + 1;
167+
prev_was_cr = false;
168+
} else {
169+
prev_was_cr = (ch == '\r');
162170
}
163171
}
164-
165-
if (extra_cr == 0) {
166-
// No normalization needed — feed raw data directly.
167-
term.vtWrite(raw);
168-
} else {
169-
// Need to insert \r before bare \n.
170-
const out_len = raw.len + extra_cr;
171-
var norm_stack: [131072]u8 = undefined;
172-
var norm_heap: ?[]u8 = null;
173-
defer if (norm_heap) |nh| std.heap.c_allocator.free(nh);
174-
175-
const norm_buf: []u8 = if (out_len <= norm_stack.len)
176-
&norm_stack
177-
else blk: {
178-
norm_heap = std.heap.c_allocator.alloc(u8, out_len) catch {
179-
// Fall back to stack buffer, truncating if needed.
180-
break :blk &norm_stack;
181-
};
182-
break :blk norm_heap.?;
183-
};
184-
185-
var npos: usize = 0;
186-
for (0..raw.len) |i| {
187-
if (raw[i] == '\n' and (i == 0 or raw[i - 1] != '\r')) {
188-
norm_buf[npos] = '\r';
189-
npos += 1;
190-
}
191-
norm_buf[npos] = raw[i];
192-
npos += 1;
193-
}
194-
term.vtWrite(norm_buf[0..npos]);
172+
if (seg_start < raw.len) {
173+
term.vtWrite(raw[seg_start..]);
195174
}
196175

197176
// Scan for OSC sequences that libghostty-vt discards (7, 51, 52, 133).

0 commit comments

Comments
 (0)