diff options
Diffstat (limited to 'src/exec.zig')
| -rw-r--r-- | src/exec.zig | 171 |
1 files changed, 129 insertions, 42 deletions
diff --git a/src/exec.zig b/src/exec.zig index 455a325e..ee7c31e9 100644 --- a/src/exec.zig +++ b/src/exec.zig @@ -17,6 +17,10 @@ const themes = colors.themes; const builtins = @import("builtins.zig"); const config = @import("config.zig"); const grammars = @import("grammar_manifest.zig").all; +const python_lang = blk: { + @setEvalBranchQuota(20_000); + break :blk @import("grammar_manifest.zig").byName("python").?; +}; const pdf_enabled = panes.Pdf.enabled; const font_picker = pardes.font_picker; const hosted = pardes.hosted; @@ -69,7 +73,8 @@ pub fn chordEachSel(p: *Pardes, pane: *Pane, cmd: Builtin) bool { var primary_active: ?usize = null; for (texts[0..made], 0..) |txt, i| { - runBuiltin(p, cmd, id, "", txt); + // Every selection is the body's, each one sent as the first is. + runBuiltinFrom(p, cmd, id, "", txt, true); if (i == got.pri) primary_active = p.active; const still = p.panes[id] orelse break; if (still.serial != serial) break; @@ -292,8 +297,8 @@ fn askWrite(p: *Pardes, id: usize, serial: u32, path: []const u8) void { /// Tty: a shell in the caller's directory, raw from the first frame, /// stacked below the caller like Alt-n's new shell. -pub fn spawnTty(p: *Pardes, from: usize) void { - spawnTtyWithMount(p, from, false); +pub fn spawnTty(p: *Pardes, from: usize) ?*Pane { + return spawnTtyWithMount(p, from, false); } pub fn spawnV9fsTty(p: *Pardes, from: usize) void { @@ -301,13 +306,19 @@ pub fn spawnV9fsTty(p: *Pardes, from: usize) void { return p.reportError(from, "Tty9p", error.LinuxRequired); if (p.fs.socket_path.len == 0) return p.reportError(from, "Tty9p", error.SessionSocketRequired); - spawnTtyWithMount(p, from, true); + _ = spawnTtyWithMount(p, from, true); } -fn spawnTtyWithMount(p: *Pardes, from: usize, v9fs: bool) void { - const src = p.panes[from] orelse return; - const free = p.freeSlot() orelse return; - const nt = p.newShell(free, Pardes.paneDir(src)) catch return; +fn spawnTtyWithMount(p: *Pardes, from: usize, v9fs: bool) ?*Pane { + const src = p.panes[from] orelse return null; + const free = p.freeSlot() orelse { + p.reportFailure(from, "Tty: no pane free"); + return null; + }; + const nt = p.newShell(free, Pardes.paneDir(src)) catch |err| { + p.reportError(from, "Tty", err); + return null; + }; nt.greet = !v9fs; nt.v9fs_on_spawn = v9fs; nt.body.mode = .tty; @@ -317,6 +328,7 @@ fn spawnTtyWithMount(p: *Pardes, from: usize, v9fs: bool) void { layout.splitBelow(p, parent, nt); p.active = free; p.stillborn_joiner = nt.serial; + return nt; } /// when a doc lands in `col`, a lone pristine shell there is clutter — drop @@ -339,7 +351,7 @@ pub fn evictLonePristineTty(p: *Pardes, col: usize, keep_id: usize) void { panes.Terminal.scrollbar(tt).total > tt.rows) return; p.deinitPane(tt) catch |err| return p.reportError(tty_id, "close", err); layout.compute(p); // a just-stacked doc has no rect yet; absorb snaps to rows - layout.absorbVWeight(p, tty_id, null); + _ = layout.absorbVWeight(p, tty_id, null); layout.removePane(p, tty_id); p.panes[tty_id] = null; if (p.active == tty_id) p.active = keep_id; @@ -364,10 +376,8 @@ pub fn replaceStillborn(p: *Pardes) void { if (out.from != .cmd or out.from.cmd != .New) return; if (file.revision != 0 or file.content.len != 0) return; if (old.tag.own) |own| if (!std.mem.eql(u8, own, tagline.defaultTail(old))) return; - const was_active = p.active == old_id; layout.compute(p); // the joiner may have no rect yet; absorb snaps to rows p.removePane(old_id, new_id) catch |err| return p.reportError(old_id, "close", err); - if (was_active) p.active = new_id; } // ---- REPLs ---- @@ -381,16 +391,18 @@ pub fn replaceStillborn(p: *Pardes) void { /// A file pane's language, as grammar_manifest names it, by its index. pub fn langOf(pane: *const Pane) ?u8 { const f = pane.file orelse return null; - for (grammars, 0..) |g, i| for (g.exts) |ext| { - if (std.mem.endsWith(u8, f.path, ext)) return @intCast(i); - }; - return null; + return @intCast(@import("grammar_manifest.zig").byPath(f.path) orelse return null); } -/// A bound REPL whose program can still take input: a command pane's is -/// gone once its command is done. -pub fn replLive(pane: *const Pane) bool { - return pane.repl != null and pane.isTerminal() and !(pane.command != null and pane.command_done); +/// A bound REPL whose program is there to take input: a command pane's +/// while its command runs; an interactive terminal's while a program, not +/// the shell, has its tty (the host's `tty_taken`) -- a python3 left with +/// Ctrl-D leaves the shell, which would run `import os` as ImageMagick's. +pub fn replLive(p: *const Pardes, id: usize) bool { + const pane = p.panes[id] orelse return false; + if (pane.repl == null) return false; + if (pane.command != null) return !pane.command_done; + return p.hostTtyTaken(id); } /// `python-a`. @@ -401,22 +413,37 @@ pub fn replId(buf: []u8, pane: *const Pane) []const u8 { const Routed = union(enum) { none, sent: usize, asked }; +/// The REPL bound here has no program running to take the text: said, +/// and nothing sent or run. +fn replGone(p: *Pardes, from: usize, repl: *const Pane) Routed { + var idbuf: [48]u8 = undefined; + var said: [128]u8 = undefined; + p.setMessage(from, std.fmt.bufPrint(&said, "Repl: {s} has no program running; nothing sent", .{replId(&idbuf, repl)}) catch "Repl: no program running"); + return .asked; +} + /// Where an exec from `from`'s body goes when a REPL is bound for it: the /// one there is, or a question when there are several. fn routeToRepl(p: *Pardes, from: usize, text: []const u8) Routed { const src = p.panes[from] orelse return .none; - if (src.repl != null and replLive(src)) { + if (src.repl != null) { + if (!replLive(p, from)) return replGone(p, from, src); sendToRepl(p, from, from, text); return .{ .sent = from }; } const lang = langOf(src) orelse return .none; var bound: [26]usize = undefined; var n: usize = 0; - for (p.panes, 0..) |slot, i| if (slot) |pane| if (pane.repl) |r| if (r.lang == lang and replLive(pane) and n < bound.len) { + var gone: ?usize = null; + for (p.panes, 0..) |slot, i| if (slot) |pane| if (pane.repl) |r| if (r.lang == lang and n < bound.len) { + if (!replLive(p, i)) { + gone = i; + continue; + } bound[n] = i; n += 1; }; - if (n == 0) return .none; + if (n == 0) return if (gone) |i| replGone(p, from, p.panes[i].?) else .none; if (n == 1) { sendToRepl(p, from, bound[0], text); return .{ .sent = bound[0] }; @@ -426,6 +453,7 @@ fn routeToRepl(p: *Pardes, from: usize, text: []const u8) Routed { const owned = p.gpa.dupe(u8, text) catch return .none; if (p.repl_ask) |old| p.gpa.free(old.text); p.repl_ask = .{ .from = src.serial, .lang = lang, .text = owned }; + for (bound[0..n]) |i| p.repl_ask.?.offered[p.panes[i].?.repl.?.letter - 'a'] = p.panes[i].?.serial; var w = std.Io.Writer.fixed(&src.question); w.print("send to {s}:", .{grammars[lang].name}) catch {}; for (bound[0..n]) |i| w.print(" {c}", .{p.panes[i].?.repl.?.letter}) catch {}; @@ -443,10 +471,28 @@ pub fn sendToRepl(p: *Pardes, from: usize, to: usize, text: []const u8) void { const src = p.panes[from] orelse return; var idbuf: [48]u8 = undefined; const id = replId(&idbuf, dst); - edit.typeToTty(p, to, dst, text); + // Lines end in \n alone (a \r\n would give Python a blank line that + // ends its block), and no ESC or other control byte but a tab goes: an + // ESC[201~ in the text would end a bracketed paste early. + const clean = p.scratch.allocator().alloc(u8, text.len) catch return; + var n: usize = 0; + for (text, 0..) |c, i| { + if (c == '\r') { + if (i + 1 < text.len and text[i + 1] == '\n') continue; + clean[n] = '\n'; + } else if (c < ' ' and c != '\t' and c != '\n' or c == 0x7f) continue else clean[n] = c; + n += 1; + } + // Sent whole or not at all: a send cut short would leave the REPL in a + // paste, or with half a block. + if (n + 16 > p.writeRoom(to)) { + var said: [128]u8 = undefined; + return p.setMessage(from, std.fmt.bufPrint(&said, "Repl: {d} bytes is more than can be sent at once; send less", .{n}) catch "Repl: too long to send"); + } + edit.typeToTty(p, to, dst, clean[0..n]); 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 std.mem.startsWith(u8, id, "python") and + if (!panes.Terminal.bracketedPaste(dst) and !dst.repl_warned and (if (dst.repl) |r| r.lang == python_lang else false) and std.mem.indexOfScalar(u8, text, '\n') != null) { dst.repl_warned = true; @@ -471,12 +517,15 @@ const max_exec_depth = 8; /// child is `$SHELL -c` the line, which shows its output and `exit N` when /// done, and which Kill signals. Answers the pane it went to. pub fn execute(p: *Pardes, id: usize, txt: []const u8) ?usize { + return executeFrom(p, id, txt, false); +} + +/// `execute` for text a gesture took from `id`'s body when `from_body`: +/// such text may be a bound REPL's. Run by name (`Exec ...`, whose line +/// comes back through `execute`) or written as a command word, never. +pub fn executeFrom(p: *Pardes, id: usize, txt: []const u8, from_body: bool) ?usize { const pane = p.panes[id] orelse return null; - // Made by a gesture on a body, it may be a bound REPL's; run by name - // (`Exec ...`, which runs through here again) or written in a command - // word, it never is. - const gesture = p.exec_from_body and config.commandWord(std.mem.trim(u8, txt, " \t\r\n")) == null; - p.exec_from_body = false; + const gesture = from_body and config.commandWord(std.mem.trim(u8, txt, " \t\r\n")) == null; const cmd = commandText(txt); if (cmd.len == 0) return null; if (p.exec_depth == 0) p.noteHaptic(.exec); @@ -507,15 +556,23 @@ pub fn execute(p: *Pardes, id: usize, txt: []const u8) ?usize { /// A pane reused keeps what it showed: the new command runs below a line /// naming it, as acme appends to +Errors and never clears it (util.c:213). fn runCommand(p: *Pardes, from: usize, line: []const u8) ?usize { + // The board and the web page start no processes: a command pane there + // would say running for ever, and fill the slots. + if (comptime !hosted) { + p.reportFailure(from, "exec: not a builtin, and this build runs no commands"); + return null; + } if (line.len > command_max) { p.reportFailure(from, "exec: a command line is at most 1024 bytes"); return null; } const src = p.panes[from] orelse return null; const dir = Pardes.paneDir(src); - const reuse: ?usize = if (src.command != null and src.command_done) from else for (p.panes, 0..) |slot, i| { + // 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| { const other = slot orelse continue; - if (other.command != null and other.command_done and std.mem.eql(u8, other.cwdSlice(), dir)) break i; + 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| { const pane = p.panes[id].?; @@ -527,16 +584,16 @@ fn runCommand(p: *Pardes, from: usize, line: []const u8) ?usize { pane.command = owned; pane.command_done = false; pane.command_status = null; + pane.command_pty = true; pane.body.mode = .tty; // What the last program left the emulator in goes first: the // alternate screen left (only if it is there: leaving restores a // saved cursor), mouse reports and bracketed paste off, the cursor - // shown, then a soft reset (DECSTR) for the rest. + // shown, colours reset. (Not DECSTR: ghostty's stream does not + // implement it.) 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[!p"); - var buf: [command_max + 8]u8 = undefined; - const lead = if (panes.Terminal.gridCursor(pane).x != 0) "\r\n" else ""; - panes.Terminal.feedOutput(p, pane, std.fmt.bufPrint(&buf, "{s}% {s}\r\n", .{ lead, line }) catch "%\r\n"); + 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()) } }); noteRun(p, pane, "run", line); return id; @@ -549,6 +606,7 @@ fn runCommand(p: *Pardes, from: usize, line: []const u8) ?usize { p.reportError(from, "exec", err); return null; }; + echoCommand(p, pane, line); const rc = if (p.ncol > 0) p.ncol - 1 else 0; if (p.col_n[rc] > 0) { const parent = layout.splitParent(p, p.col_panes[rc][p.col_n[rc] - 1]); @@ -560,12 +618,26 @@ fn runCommand(p: *Pardes, from: usize, line: []const u8) ?usize { return free; } +/// `% line` above a command's output, every command, as a shell echoes +/// what it was given; its newlines the terminal's, so a multi-line command +/// does not staircase. +fn echoCommand(p: *Pardes, pane: *Pane, line: []const u8) void { + var buf: [2 * command_max + 8]u8 = undefined; + var w = std.Io.Writer.fixed(&buf); + w.writeAll(if (panes.Terminal.gridCursor(pane).x != 0) "\r\n% " else "% ") catch {}; + for (line) |c| (if (c == '\n') w.writeAll("\r\n") else w.writeByte(c)) catch {}; + w.writeAll("\r\n") catch {}; + panes.Terminal.feedOutput(p, pane, w.buffered()); +} + /// `run <serial> <word>` or `exit <serial> <N|?>` in the log. +/// The whole line, one record as `msg` carries its text: its control +/// characters, a newline too, become spaces (events.pushLog). A new pane is +/// announced first, so `new` comes before the `run` in it. fn noteRun(p: *Pardes, pane: *Pane, what: []const u8, said: []const u8) void { - var buf: [96]u8 = undefined; - const word = std.mem.trim(u8, said, " \t"); - const first = word[0 .. std.mem.indexOfAny(u8, word, " \t") orelse word.len]; - pardes.ctlfs.events.notePath(p, what, std.fmt.bufPrint(&buf, "{d} {s}", .{ pane.serial, first[0..@min(first.len, 64)] }) catch return); + pardes.ctlfs.events.announce(p); + var buf: [command_max + 16]u8 = undefined; + pardes.ctlfs.events.notePath(p, what, std.fmt.bufPrint(&buf, "{d} {s}", .{ pane.serial, std.mem.trim(u8, said, " \t\r\n") }) catch return); } /// A command pane's child is gone: the pane stays, showing what it said @@ -573,6 +645,8 @@ fn noteRun(p: *Pardes, pane: *Pane, what: []const u8, said: []const u8) void { pub fn commandDone(p: *Pardes, id: usize, status: ?u8) void { const pane = p.panes[id] orelse return; pane.command_done = true; + // A REPL it was is gone with its command; the pane's next is not one. + pane.repl = null; pane.command_status = status; pane.body.mode = .normal; var buf: [16]u8 = undefined; @@ -621,6 +695,11 @@ pub fn applyStartupConfig(p: *Pardes) void { } pub fn runBuiltin(p: *Pardes, b: Builtin, id: usize, txt: []const u8, arg: ?[]const u8) void { + runBuiltinFrom(p, b, id, txt, arg, false); +} + +/// `runBuiltin` saying whether a gesture on the pane's body made it. +pub fn runBuiltinFrom(p: *Pardes, b: Builtin, id: usize, txt: []const u8, arg: ?[]const u8, from_body: bool) void { if (!normal.multiOnce(p)) return; // a builtin is per-keystroke, never per-cursor const pane = p.panes[id] orelse return; // A builtin says its own name before it runs, so a tagline click is @@ -630,7 +709,7 @@ pub fn runBuiltin(p: *Pardes, b: Builtin, id: usize, txt: []const u8, arg: ?[]co p.setMessage(id, @tagName(b)); pane.msg_announcement = true; } - const c: builtins.Ctx = .{ .p = p, .pane = pane, .id = id, .txt = txt, .arg = arg }; + const c: builtins.Ctx = .{ .p = p, .pane = pane, .id = id, .txt = txt, .arg = arg, .from_body = from_body }; builtins.registry.dispatch(b, c); } @@ -662,7 +741,11 @@ 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; }, - else => _ = p.settings.apply(setting, arg), + 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"; + p.reportFailure(p.active, std.fmt.bufPrint(&text, "{s}: {s}", .{ setting.word, takes }) catch takes); + }, } if (p.settings.colors != previous_colors) { for (p.panes) |slot| { @@ -688,6 +771,10 @@ pub fn applySettingBuiltin(p: *Pardes, setting: config.Runtime.Setting, arg: ?[] if (p.settings.column_tags != previous_column_tags) { if (p.header_column != null) tagline.exitHeader(p); } + // Idle frames keep no frozen grid while transitions are off; the one + // just drawn is the baseline the first transition starts from. + if (transition_changed and previous_transition == .off) + p.presentation.capturePrevious(p.gpa, &p.surface); if (transition_changed or tagline_metrics_changed or p.settings.column_tags != previous_column_tags) { p.abandonPanelAnimations(); } |
