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