diff options
Diffstat (limited to 'src/main.zig')
| -rw-r--r-- | src/main.zig | 122 |
1 files changed, 119 insertions, 3 deletions
diff --git a/src/main.zig b/src/main.zig index 9dce559f..8818645e 100644 --- a/src/main.zig +++ b/src/main.zig @@ -46,11 +46,13 @@ pub const debug = if (is_emscripten) struct {} else struct { }; const help_text = - \\Usage: pardes [options] [file|dir] + \\Usage: pardes [options] [file|dir|-] \\ \\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. + \\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. \\ \\Options: \\ --tty start as one shell pane already in tty mode @@ -98,6 +100,95 @@ const help_text = \\ ; +/// 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. +fn readStdinText(gpa: std.mem.Allocator) ![]u8 { + const limit = pardes.memory.limits.max_file_bytes; + var raw: std.ArrayList(u8) = .empty; + var chunk: [64 * 1024]u8 = undefined; + var cut = false; + while (true) { + const n = std.c.read(0, &chunk, chunk.len); + if (n < 0) { + if (std.posix.errno(n) == .INTR) continue; + return error.ReadFailed; + } + if (n == 0) break; + const got: usize = @intCast(n); + if (raw.items.len + got > limit) { + try raw.appendSlice(gpa, chunk[0 .. limit - raw.items.len]); + cut = true; + break; + } + 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; + } + 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" }, + }; + for (cases) |c| { + const got = try stripEscapes(gpa, c[0]); + defer gpa.free(got); + try std.testing.expectEqualStrings(c[1], got); + } +} + /// A forwarding failure in words, never an error's name: the ones this /// file raises said outright, any other as its name's words (`NotFound`, /// `not found`), a dial's as Messages says it. @@ -369,7 +460,8 @@ fn nativeMain(init: std.process.Init) !void { } else if (std.mem.eql(u8, a, "--version")) { try std.Io.File.stdout().writeStreamingAll(init.io, version_text); return; - } else if (a.len > 0 and a[0] != '-' and positional == null) { + } else if (a.len > 0 and (a[0] != '-' or std.mem.eql(u8, a, "-")) and positional == null) { + // `-` is stdin, as for cat and vim: a text to show. session_only = false; positional = a; } else if (a.len == 0) { @@ -440,6 +532,24 @@ fn nativeMain(init: std.process.Init) !void { // itself, which is what a right click in that pane would have been. var look_buf: [64]u8 = undefined; const look = try std.fmt.bufPrint(&look_buf, "/pane/{d}/look", .{parent.serial}); + // `pardes -`: stdin read to its end, a +Pager pane of the session + // made of it (as acme's `B` has no stdin, through pane/new, a name + // and the body), and back at once: the PAGER a pane's shell gets. + if (positional) |word| if (std.mem.eql(u8, word, "-")) { + const text = readStdinText(arena) catch |err| Refuse.with(init.io, "stdin", err); + const made = ninep_io.Client.read(arena, parent.dial, "/pane/new", "/pane/new") catch |err| Refuse.with(init.io, "-", err); + const serial = std.fmt.parseInt(u32, std.mem.trim(u8, made, " \n"), 10) catch |err| Refuse.with(init.io, "-", err); + var cwd_buf: [4096]u8 = undefined; + const cwd = if (std.c.getcwd(&cwd_buf, cwd_buf.len)) |c| std.mem.span(@as([*:0]u8, @ptrCast(c))) else "/"; + var path_buf: [64]u8 = undefined; + var line_buf: [4200]u8 = undefined; + const name_line = std.fmt.bufPrint(&line_buf, "{s}/+Pager\n", .{std.mem.trimEnd(u8, cwd, "/")}) catch "/+Pager\n"; + ninep_io.Client.write(arena, parent.dial, try std.fmt.bufPrint(&path_buf, "/pane/{d}/name", .{serial}), name_line) catch |err| Refuse.with(init.io, "-", err); + if (text.len > 0) ninep_io.Client.write(arena, parent.dial, try std.fmt.bufPrint(&path_buf, "/pane/{d}/body", .{serial}), text) catch |err| Refuse.with(init.io, "-", err); + // Shown, not edited: nothing to ask about at Exit. + ninep_io.Client.write(arena, parent.dial, try std.fmt.bufPrint(&path_buf, "/pane/{d}/ctl", .{serial}), "clean\n") catch {}; + return; + }; const typed_word = positional orelse { var tag_buf: [64]u8 = undefined; const tag = try std.fmt.bufPrint(&tag_buf, "/pane/{d}/tag", .{parent.serial}); @@ -530,6 +640,12 @@ fn nativeMain(init: std.process.Init) !void { if (wait) waitForDel(init.io, arena, parent.dial, path, made_serial); return; } + // `pardes -` outside a session: an editor whose first pane holds stdin, + // as `vim -` does; the terminal is /dev/tty, never stdin. + if (positional) |a| if (std.mem.eql(u8, a, "-")) { + opts.stdin_text = readStdinText(arena) catch |err| badArgs(init.io, "stdin: {s}", .{@errorName(err)}); + positional = null; + }; if (positional) |a| { const target = @import("look.zig").parsePathLine(a); if (std.mem.eql(u8, target.path, "/n") or std.mem.startsWith(u8, target.path, "/n/") or |
