From 8c4d09f709e63af63bb2e5abcb78cafceffd583c Mon Sep 17 00:00:00 2001 From: slhx agent Date: Sun, 21 Jun 2026 16:41:15 +0200 Subject: [PATCH] Add visible terminal control debug helpers --- src/main.zig | 5 +++++ src/terminal_debug.zig | 36 ++++++++++++++++++++++++++++++++++++ 2 files changed, 41 insertions(+) create mode 100644 src/terminal_debug.zig diff --git a/src/main.zig b/src/main.zig index c2e958c..672ba03 100644 --- a/src/main.zig +++ b/src/main.zig @@ -17,6 +17,7 @@ const session = @import("session.zig"); const socket = @import("socket.zig"); const symbol = @import("symbol.zig"); const syntax = @import("syntax.zig"); +const terminal_debug = @import("terminal_debug.zig"); const tui = @import("tui.zig"); pub const version = "0.1.0-dev"; @@ -600,6 +601,10 @@ test "regular: local editor client exposes save and dirty quit guards" { try std.testing.expect(!client.requestedQuit()); } +test "terminal debug helpers are included in main test root" { + _ = terminal_debug; +} + test "regular: raw terminal output converts newline to carriage-return newline" { const converted = try terminalCrlfAlloc(std.testing.allocator, "a\nb\n"); defer std.testing.allocator.free(converted); diff --git a/src/terminal_debug.zig b/src/terminal_debug.zig new file mode 100644 index 0000000..db090f7 --- /dev/null +++ b/src/terminal_debug.zig @@ -0,0 +1,36 @@ +const std = @import("std"); + +/// Materialize terminal control characters as visible glyphs for failure output. +/// This is test/debug support only: product rendering still writes real CR/LF/ESC. +pub fn visibleControlsAlloc(allocator: std.mem.Allocator, bytes: []const u8) ![]u8 { + var out = std.ArrayList(u8).empty; + errdefer out.deinit(allocator); + for (bytes) |byte| switch (byte) { + '\r' => try out.appendSlice(allocator, "␍"), + '\n' => try out.appendSlice(allocator, "␊\n"), + 0x1b => try out.appendSlice(allocator, "␛"), + '\t' => try out.appendSlice(allocator, "⇥"), + else => if (byte < 0x20) { + try out.appendSlice(allocator, "␀"); + } else try out.append(allocator, byte), + }; + return out.toOwnedSlice(allocator); +} + +pub fn hasBareLf(bytes: []const u8) bool { + for (bytes, 0..) |byte, i| { + if (byte == '\n' and (i == 0 or bytes[i - 1] != '\r')) return true; + } + return false; +} + +test "regular: visible terminal controls expose newline and escape bytes" { + const debug = try visibleControlsAlloc(std.testing.allocator, "a\nb\r\n\x1b[31m"); + defer std.testing.allocator.free(debug); + try std.testing.expectEqualStrings("a␊\nb␍␊\n␛[31m", debug); +} + +test "adversarial: bare LF detector catches raw-mode skew risk" { + try std.testing.expect(hasBareLf("a\nb")); + try std.testing.expect(!hasBareLf("a\r\nb")); +}