summaryrefslogtreecommitdiff
path: root/src/main.zig
diff options
context:
space:
mode:
Diffstat (limited to 'src/main.zig')
-rw-r--r--src/main.zig122
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