diff options
Diffstat (limited to 'src/exec.zig')
| -rw-r--r-- | src/exec.zig | 168 |
1 files changed, 156 insertions, 12 deletions
diff --git a/src/exec.zig b/src/exec.zig index d4480e00..2b0c90cb 100644 --- a/src/exec.zig +++ b/src/exec.zig @@ -217,6 +217,28 @@ pub fn heldSelection(p: *Pardes, first: usize) ?[]const u8 { return null; } +/// Where the selection `heldSelection` would give is, as acme's argument +/// location (exec.c:162, getbytearg): `path:#q0,#q1` of a file pane's dot. +pub fn heldSelectionPlace(p: *Pardes, first: usize, buf: []u8) ?[]const u8 { + var k: usize = 0; + while (k < p.panes.len + 2) : (k += 1) { + const i = switch (k) { + 0 => first, + 1 => p.active, + else => k - 2, + }; + if (k > 0 and (i == first or (k > 1 and i == p.active))) continue; + const t = p.panes[i] orelse continue; + if (t.sel[sel_slot].state == .done) return null; // a pointer selection: no byte range kept + if ((t.body.vsel.active and t.body.vsel.explicit) or t.body.msel.active) { + const f = t.file orelse return null; + const d = @import("ninep/pane.zig").dotOf(t); + return std.fmt.bufPrint(buf, "{s}:#{d},#{d}", .{ f.path, d.q0, d.q1 }) catch null; + } + } + return null; +} + /// A clicked word and the selection held for it, as one command line. pub fn withArg(p: *Pardes, txt: []const u8, arg: ?[]const u8) []const u8 { const a = std.mem.trim(u8, arg orelse return txt, " \t\r\n"); @@ -249,6 +271,16 @@ pub fn saveFile(p: *Pardes, id: usize) void { const pane = p.panes[id] orelse return; const f = if (pane.file) |*file| file else return; if (f.output != null) return; // nothing behind it yet: saveTo, with a path + // Its file changed on disk under these edits: once, as acme's Put says + // "modified since last read" (exec.c:577), and the next Save writes. + if (f.disk_newer != null) { + if (!f.disk_newer_warned) { + f.disk_newer_warned = true; + var said: [Pardes.SavePath.cap + 64]u8 = undefined; + return p.reportFailure(id, std.fmt.bufPrint(&said, "{s} modified on disk since read (Save again to overwrite)", .{f.path}) catch "modified on disk since read (Save again to overwrite)"); + } + f.disk_newer = null; + } p.emit(.{ .save_file = .{ .pane = @intCast(id) } }); } @@ -310,12 +342,16 @@ pub fn spawnV9fsTty(p: *Pardes, from: usize) void { } fn spawnTtyWithMount(p: *Pardes, from: usize, v9fs: bool) ?*Pane { - const src = p.panes[from] orelse return null; + if (p.panes[from] == null) return null; const free = p.freeSlot() orelse { p.reportError(from, "Tty", error.NoPaneSlots); return null; }; - const nt = p.newShell(free, Pardes.paneDir(src)) catch |err| { + const nt = p.newShell(free, p.execDir(from)) catch |err| { + if (err == error.PathTooLong) { + p.reportFailure(from, std.fmt.comptimePrint("Tty: its directory is longer than a path may be ({d} bytes)", .{pardes.memory.limits.host_path_cap})); + return null; + } p.reportError(from, "Tty", err); return null; }; @@ -444,13 +480,29 @@ fn routeToRepl(p: *Pardes, from: usize, text: []const u8) Routed { } const repl_enter_wait_ns: u64 = 250 * std.time.ns_per_ms; +/// Python echoes a long paste over several reads: its Enter waits for the +/// echo to go quiet this long, not for the first of it. +const repl_quiet_ns: u64 = 100 * std.time.ns_per_ms; + +/// The host's clock now; core time (`now_ns`, `stepped_ns`) is as old as +/// the last frame, seconds in an idle session. +fn clockNow(p: *Pardes) u64 { + return if (p.host.vtable.now) |clock| clock(p.host.ctx) else p.now_ns; +} /// Sends the Enters a paste into pane `id`'s REPL is owed: when the REPL -/// has answered it (`answered`), or once the wait is up. +/// has answered it (`answered`), or once the wait is up. Python's answer +/// only restarts a short wait, so the Enter goes once its echo of the +/// whole paste is in (a multi-line paste read in part would leave it at +/// `...`). pub fn flushReplEnter(p: *Pardes, id: usize, answered: bool) void { const pane = p.panes[id] orelse return; if (pane.repl_enter == 0) return; - if (!answered and p.stepped_ns < pane.repl_enter_due_ns) return; + if (answered and (if (pane.repl) |r| r.lang == python_lang else false)) { + pane.repl_enter_due_ns = clockNow(p) + repl_quiet_ns; + return; + } + if (!answered and p.now_ns < pane.repl_enter_due_ns) return; for (0..pane.repl_enter) |_| p.emitWrite(id, "\r"); pane.repl_enter = 0; } @@ -492,14 +544,22 @@ pub fn sendToRepl(p: *Pardes, from: usize, to: usize, text: []const u8) void { // A Python block whose last line is indented, or a bare header, is not // over at one Enter, pasted into 3.13's REPL or typed into the old one: // the blank line a second Enter gives ends it. - const enters: u8 = if ((if (dst.repl) |r| r.lang == python_lang else false) and pythonBlockOpen(clean[0..n])) 2 else 1; + // So is a paste of more than one line: 3.13's REPL takes it as one + // input, which one Enter leaves at `...`. + const python = if (dst.repl) |r| r.lang == python_lang else false; + const lines = std.mem.count(u8, std.mem.trim(u8, clean[0..n], " \t\n"), "\n") + 1; + const enters: u8 = if (python and (pythonBlockOpen(clean[0..n]) or (pasted and lines > 1))) 2 else 1; if (pasted) { // An Enter read with the paste is part of it: 3.13's REPL takes it // as a newline in the pasted text, even a one-line one, and waits. // It goes once the REPL has answered the paste (its next output), // or after a moment if nothing comes. dst.repl_enter = enters; - dst.repl_enter_due_ns = p.stepped_ns + repl_enter_wait_ns; + // From the clock now, not core time: idle (a detached session + // nobody watches), `stepped_ns` and `now_ns` are as old as the last + // frame, and a wait counted from there would be over already, the + // Enter going out with the paste. + dst.repl_enter_due_ns = clockNow(p) + repl_enter_wait_ns; } else for (0..enters) |_| p.emitWrite(to, "\r"); // Line by line, a blank line inside a Python block ends the block. if (!panes.Terminal.bracketedPaste(dst) and !dst.repl_warned and (if (dst.repl) |r| r.lang == python_lang else false) and @@ -577,11 +637,14 @@ fn runCommand(p: *Pardes, from: usize, line: []const u8) ?usize { return null; } const src = p.panes[from] orelse return null; - const dir = Pardes.paneDir(src); + const dir = p.execDir(from); // Not one whose pty a job it left behind still prints to: reusing it // would hang that job up. - const reuse: ?usize = if (src.command != null and src.command_done and !src.command_pty) from else for (p.panes, 0..) |slot, i| { + // From a column's tag, only a finished pane in that column is reused, + // as the new one goes in that column (placeNew). + const reuse: ?usize = if (p.exec_column == null and src.command != null and src.command_done and !src.command_pty) from else for (p.panes, 0..) |slot, i| { const other = slot orelse continue; + if (p.exec_column) |c| if (if (layout.findPane(p, i)) |f| f.col != c else true) continue; if (other.command != null and other.command_done and !other.command_pty and std.mem.eql(u8, other.cwdSlice(), dir)) break i; } else null; if (reuse) |id| { @@ -604,7 +667,7 @@ fn runCommand(p: *Pardes, from: usize, line: []const u8) ?usize { if (panes.Terminal.onAlternateScreen(pane)) panes.Terminal.feedOutput(p, pane, "\x1b[?1049l"); panes.Terminal.feedOutput(p, pane, "\x1b[?1000l\x1b[?1002l\x1b[?1003l\x1b[?1006l\x1b[?2004l\x1b[?25h\x1b[0m"); echoCommand(p, pane, line); - p.emit(.{ .spawn = .{ .pane = @intCast(id), .serial = pane.serial, .cwd = .from(pane.cwdSlice()) } }); + p.emitSpawn(id, pane.serial, pane.cwdSlice()); noteRun(p, pane, "run", line); return id; } @@ -670,6 +733,15 @@ pub fn commandText(txt: []const u8) []const u8 { return cmd; } +/// The builtin a command line's first word names, as acme's lookup reads +/// only the first word (editors/acme/exec.c, lookup): what an event's +/// flag 1 says. +pub fn namesBuiltin(txt: []const u8) bool { + const cmd = commandText(txt); + const end = std.mem.indexOfAny(u8, cmd, " \t") orelse cmd.len; + return std.meta.stringToEnum(Builtin, cmd[0..end]) != null; +} + pub fn executeBuiltinLine(p: *Pardes, id: usize, txt: []const u8) bool { const cmd = commandText(txt); if (cmd.len == 0 or p.exec_depth >= max_exec_depth) return false; @@ -726,12 +798,22 @@ pub fn runBuiltinFrom(p: *Pardes, b: Builtin, id: usize, txt: []const u8, arg: ? // A builtin says its own name before it runs, so a tagline click is // legible even when what it did is invisible. Whatever the builtin // then says for itself replaces the announcement. + // Its log record stands only if it does not fail (events.dropFailedAnnouncement). + var announced: ?u64 = null; if (p.announce and p.settings.verbose and builtins.registry.announces(b)) { p.setMessage(id, @tagName(b)); pane.msg_announcement = true; + announced = pardes.ctlfs.events.newestSaying(p, @tagName(b)); + p.fs.announced_seq = announced; + const word = @tagName(b)[0..@min(@tagName(b).len, p.fs.announced_word.len)]; + @memcpy(p.fs.announced_word[0..word.len], word); + p.fs.announced_word_len = @intCast(word.len); } + const failures = p.fs.failures; const c: builtins.Ctx = .{ .p = p, .pane = pane, .id = id, .txt = txt, .arg = arg, .from_body = from_body }; builtins.registry.dispatch(b, c); + if (announced != null and p.fs.failures != failures and p.fs.announced_seq == announced) + pardes.ctlfs.events.dropFailedAnnouncement(p, "", true); } pub fn applySettingBuiltin(p: *Pardes, setting: config.Runtime.Setting, arg: ?[]const u8) void { @@ -761,6 +843,26 @@ pub fn applySettingBuiltin(p: *Pardes, setting: config.Runtime.Setting, arg: ?[] if (!p.settings.requestFont(matches[0].path, matches[0].name, spec.size_hundredths)) return; p.font_request_taken = false; }, + // A shell that is no executable is refused, not taken and quietly + // replaced at the next spawn; bare, it goes back to the default. + .shell => { + const want = std.mem.trim(u8, arg orelse "", " \t\r\n"); + if (want.len == 0) { + p.settings.shell.requested.clear(); + p.settings.shell.pending = true; + if (!p.announce) return; + var said: [320]u8 = undefined; + return p.setMessage(p.active, std.fmt.bufPrint(&said, "Shell: the default again, {s} ($SHELL, else /bin/sh)", .{config.defaultShell()}) catch "Shell: the default again"); + } + if (comptime pardes.hosted) { + var why: [320]u8 = undefined; + if (@import("host_io.zig").Shell.refusal(want[0..@min(want.len, 200)], &why)) |refused| { + var text: [340]u8 = undefined; + return p.reportFailure(p.active, std.fmt.bufPrint(&text, "Shell: {s}", .{refused}) catch "Shell: no such shell"); + } + } + if (!p.settings.apply(setting, want) and p.announce) p.reportFailure(p.active, "Shell: does not take that value"); + }, else => if (!p.settings.apply(setting, arg) and p.announce) { var text: [96]u8 = undefined; const takes = if (setting.action == .toggle) "takes on or off" else "does not take that value"; @@ -808,19 +910,61 @@ pub const NewPane = enum { scratch, fs, shell, doc, command, errors }; /// t->w == nil). pub fn placeNew(p: *Pardes, from: usize, origin: ?usize, id: usize, kind: NewPane) void { const asked = if (p.exec_column != null) null else origin; + layout.compute(p); + const before = p.rects; switch (p.settings.placement) { .acme => switch (kind) { // New: the bottom half of the last pane in its own column // (look.c:921-923, et->col). .scratch => layout.acmePlace(p, p.exec_column orelse columnOf(p, from), null, id), - // +Errors, a command's output: the last column (util.c:94-98). - .command, .errors => layout.acmePlace(p, p.ncol - 1, null, id), + // +Errors, a command's output: the last column (util.c:94-98); + // a command run from a column's tag, that column. + .command => layout.acmePlace(p, p.exec_column orelse p.ncol - 1, null, id), + .errors => layout.acmePlace(p, p.ncol - 1, null, id), // makenewwindow(nil) for 9P's new (acme.c:877), else (t). .fs => layout.acmePlace(p, acmeColumn(p, from), null, id), .shell, .doc => layout.acmePlace(p, acmeColumn(p, from), asked, id), }, .pardes => pardesPlace(p, from, id, kind), } + // No pane is made shorter than its tag and two rows: where the place + // chosen has not the room, the column's tallest pane gives it, and + // where none can, the pane is refused, and closed at the step's end + // (Pardes.unplaced) once whatever made it is done with it. + if (!layout.placedRoomy(p, id, &before)) if (layout.findPane(p, id)) |f| { + layout.removePane(p, id); + for (p.col_panes[f.col][0..p.col_n[f.col]]) |k| p.rects[k] = before[k]; + layout.snapColWeights(p, f.col); + if (!layout.splitTallest(p, f.col, id) or !layout.placedRoomy(p, id, &before)) { + if (layout.findPane(p, id) == null) layout.insert(p, f.col, p.col_n[f.col], id); + // +Errors goes wherever there is room, last column first; with + // none, its maker says the text some other way (appendErrors). + if (kind == .errors) { + layout.removePane(p, id); + var c = p.ncol; + while (c > 0) { + c -= 1; + if (c == f.col) continue; + for (p.col_panes[c][0..p.col_n[c]]) |k| p.rects[k] = before[k]; + layout.snapColWeights(p, c); + if (p.col_n[c] == 0) { + layout.insert(p, c, 0, id); + break; + } + if (layout.splitTallest(p, c, id) and layout.placedRoomy(p, id, &before)) break; + if (layout.findPane(p, id) != null) layout.removePane(p, id); + } else { + layout.insert(p, f.col, p.col_n[f.col], id); + p.unplaced.set(id); + return; + } + } else { + p.unplaced.set(id); + p.reportError(from, "placement", error.NoPaneRoom); + return; + } + } + }; if (kind == .command) return; tagline.exitHeader(p); p.active = id; @@ -865,7 +1009,7 @@ fn pardesPlace(p: *Pardes, from: usize, id: usize, kind: NewPane) void { .shell => belowParent(p, from, id), .doc, .errors => pardesDoc(p, from, id), .command => { - const rc = p.ncol - 1; + const rc = p.exec_column orelse p.ncol - 1; if (p.col_n[rc] > 0) belowParent(p, p.col_panes[rc][p.col_n[rc] - 1], id) else layout.insert(p, rc, 0, id); }, } |
