diff options
| author | Gabriel Schneider <[email protected]> | 2026-10-01 10:09:54 -0300 |
|---|---|---|
| committer | Gabriel Schneider <[email protected]> | 2026-10-01 10:36:28 -0300 |
| commit | edad6d960dc0fc1165cbb3b801e0538045b2247f (patch) | |
| tree | 0bedfbb9cd371e5d77cb697438c5198c688653c5 /src/main.zig | |
| parent | 4db57bf5bd788a292fcf2cfbea3b6d8b281510f0 (diff) | |
| download | pardes-edad6d960dc0fc1165cbb3b801e0538045b2247f.tar.gz pardes-edad6d960dc0fc1165cbb3b801e0538045b2247f.zip | |
A +Pager keeps its program's colours: the session parses what /pager is written after its directory with ghostty-vt, SGR becomes spans drawn over the plain text, every other escape is dropped, and PagerColor off pages it plain
Diffstat (limited to 'src/main.zig')
| -rw-r--r-- | src/main.zig | 129 |
1 files changed, 24 insertions, 105 deletions
diff --git a/src/main.zig b/src/main.zig index e998accc..6d895307 100644 --- a/src/main.zig +++ b/src/main.zig @@ -50,9 +50,10 @@ const help_text = \\ \\With no arguments the shell pane starts in tty mode. A FILE argument \\(optionally file:LINE) opens it alone, filling the window; a DIRECTORY - \\argument becomes the cwd shells spawn in. `-` reads stdin, its terminal - \\escapes stripped, into a +Pager pane: inside a pardes pane the outer - \\session's, returning at once (a terminal's PAGER), else a new editor's. + \\argument becomes the cwd shells spawn in. `-` reads stdin into a +Pager + \\pane, its colours kept (PagerColor) and every other escape dropped: + \\inside a pardes pane the outer session's, returning at once (a + \\terminal's PAGER), else a new editor's. \\ \\Options: \\ --tty start as one shell pane already in tty mode @@ -100,11 +101,12 @@ const help_text = \\ ; -/// `pardes -` inside a session: `text` into the directory's one +Pager -/// (/pager, refilled when it is there), written into its -/// body through one open, as one undo step, and left clean. A text -/// the session would not take is never lost: it goes to stderr, with why, -/// and the exit is 1. +/// `pardes -` inside a session: `text`, stdin as it came, escapes and all, +/// into the directory's one +Pager (/pager, refilled when it is there): +/// written after the directory on that one open, the session parses it +/// (pager.zig) and puts it in as one undo step, left clean. A text the +/// session would not take is never lost: it goes to stderr, a terminal, +/// with why, and the exit is 1. fn pageInto(io: std.Io, gpa: std.mem.Allocator, dial: []const u8, text: []const u8) void { const Fail = struct { fn with(o: std.Io, t: []const u8, what: []const u8, err: anyerror) noreturn { @@ -128,22 +130,16 @@ fn pageInto(io: std.Io, gpa: std.mem.Allocator, dial: []const u8, text: []const } else !pardes.filesystem.deniedAbove(cwd); var line_buf: [4200]u8 = undefined; const dir_line = std.fmt.bufPrint(&line_buf, "{s}\n", .{if (plain) cwd else ""}) catch "\n"; - // Its serial read back on the open that asked: another client's exec - // meanwhile can never send this text into that client's pane. - const answer = ninep_io.Client.ask(gpa, dial, "/pager", dir_line) catch |err| Fail.with(io, text, "no +Pager pane", err); - const serial = std.fmt.parseInt(u32, std.mem.trim(u8, answer, " \n"), 10) catch |err| Fail.with(io, text, "no +Pager pane", err); - // The whole text through one open of the body (truncated once, then - // written at rising offsets): one undo step, and every byte of it. - var body_buf: [64]u8 = undefined; - const body = std.fmt.bufPrint(&body_buf, "/pane/{d}/body", .{serial}) catch unreachable; - ninep_io.Client.write(gpa, dial, body, text) catch |err| Fail.with(io, text, "the +Pager pane would not take it", err); - var ctl_buf: [64]u8 = undefined; - ninep_io.Client.write(gpa, dial, std.fmt.bufPrint(&ctl_buf, "/pane/{d}/ctl", .{serial}) catch unreachable, "clean\n") catch {}; + // Its serial read back on the open that asked, once the text is in: + // another client's exec meanwhile can never send it into that + // client's pane. + const answer = ninep_io.Client.ask(gpa, dial, "/pager", dir_line, text) catch |err| Fail.with(io, text, "no +Pager pane", err); + _ = std.fmt.parseInt(u32, std.mem.trim(u8, answer, " \n"), 10) catch |err| Fail.with(io, text, "no +Pager pane", err); } -/// Stdin to its end, its terminal escapes stripped (a pager's input is -/// coloured for a terminal; a text pane shows text), at most a file's -/// limit, a longer one cut there and said so at its end. +/// Stdin to its end, as it came (its escapes are the session's to parse, +/// pager.zig), at most a file's limit, a longer one cut there and said so +/// at its end. fn readStdinText(gpa: std.mem.Allocator) ![]u8 { const limit = pardes.memory.limits.max_file_bytes; var raw: std.ArrayList(u8) = .empty; @@ -164,90 +160,13 @@ fn readStdinText(gpa: std.mem.Allocator) ![]u8 { } try raw.appendSlice(gpa, chunk[0..got]); } - var text = try stripEscapes(gpa, raw.items); if (cut) { - const said = "\n[pardes: the rest of stdin is cut: a pane holds at most a file's limit]\n"; - text = try std.mem.concat(gpa, u8, &.{ text, said }); - } - return text; -} - -/// `bytes` without terminal escapes: CSI (`ESC [` ... a final byte, SGR -/// colour among them), OSC (`ESC ]` ... BEL or `ESC \`), any other -/// two-byte `ESC x`, and a man page's overstrike (`x BS x`, `_ BS x`). -pub fn stripEscapes(gpa: std.mem.Allocator, bytes: []const u8) ![]u8 { - var out = try std.ArrayList(u8).initCapacity(gpa, bytes.len); - var i: usize = 0; - while (i < bytes.len) { - const c = bytes[i]; - if (c == 0x1b and i + 1 < bytes.len) { - const kind = bytes[i + 1]; - i += 2; - if (kind == '[') { - while (i < bytes.len and !(bytes[i] >= 0x40 and bytes[i] <= 0x7e)) i += 1; - i += 1; - } else if (std.mem.indexOfScalar(u8, "()*+", kind) != null) { - i += 1; // a charset's name after it - } else if (kind == ']') { - while (i < bytes.len) : (i += 1) { - if (bytes[i] == 0x07) { - i += 1; - break; - } - if (bytes[i] == 0x1b and i + 1 < bytes.len and bytes[i + 1] == '\\') { - i += 2; - break; - } - } - } - continue; - } - // Overstrike: the byte before a backspace is struck over. - if (c == 0x08) { - if (out.items.len > 0) _ = out.pop(); - i += 1; - continue; - } - // A carriage return: CRLF is a newline; alone, the line starts - // over, as on a terminal, so a progress line keeps its last state. - if (c == '\r') { - if (i + 1 < bytes.len and bytes[i + 1] == '\n') { - i += 1; - continue; - } - const line_start = if (std.mem.lastIndexOfScalar(u8, out.items, '\n')) |nl| nl + 1 else 0; - out.shrinkRetainingCapacity(line_start); - i += 1; - continue; - } - // Bytes a terminal acts on and never shows: NUL, BEL, SO, SI. - if (c == 0 or c == 0x07 or c == 0x0e or c == 0x0f) { - i += 1; - continue; - } - out.appendAssumeCapacity(c); - i += 1; - } - return out.toOwnedSlice(gpa); -} - -test "stripEscapes takes out colour, other escapes and overstrike, and keeps the text" { - const gpa = std.testing.allocator; - const cases = [_][2][]const u8{ - .{ "\x1b[33mcommit abc\x1b[m\n", "commit abc\n" }, - .{ "a\x1b]8;;http://x\x1b\\link\x1b]8;;\x07b", "alinkb" }, - .{ "N\x08NA\x08AM\x08ME\x08E _\x08u", "NAME u" }, - .{ "plain\ttext\n", "plain\ttext\n" }, - .{ "\x1b(Bdone", "done" }, - .{ "a\r\nb\r\n", "a\nb\n" }, - .{ "10%\r50%\r100%\ndone\n", "100%\ndone\n" }, - .{ "x\x00y\x07z\x0e\x0f\n", "xyz\n" }, - }; - for (cases) |c| { - const got = try stripEscapes(gpa, c[0]); - defer gpa.free(got); - try std.testing.expectEqualStrings(c[1], got); + // CAN ends an escape the cut left half sent, and SGR 0 its colour, + // so the note is never swallowed or tinted. + const said = "\x18\x1b[m\n[pardes: the rest of stdin is cut: a pane holds at most a file's limit]\n"; + try raw.appendSlice(gpa, said); } + return raw.toOwnedSlice(gpa); } /// A forwarding failure in words, never an error's name: the ones this @@ -693,7 +612,7 @@ fn nativeMain(init: std.process.Init) !void { const command = std.fmt.bufPrint(&command_buf, "{s}{s}\n", .{ path, word[target.path.len..] }) catch |err| Refuse.with(init.io, word, err); // The pane it went to, read on the open that asked (another // client's look meanwhile is not this one's): what --wait waits on. - const went = ninep_io.Client.ask(arena, parent.dial, look, command) catch |err| Refuse.with(init.io, word, err); + const went = ninep_io.Client.ask(arena, parent.dial, look, command, "") catch |err| Refuse.with(init.io, word, err); var rows = std.mem.tokenizeAny(u8, went, " \n"); if (rows.next()) |row| made_serial = std.fmt.parseInt(u32, row, 10) catch null; } else { |
