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