diff options
Diffstat (limited to 'src')
| -rw-r--r-- | src/File.zig | 2 | ||||
| -rw-r--r-- | src/Messages.zig | 1 | ||||
| -rw-r--r-- | src/body_layer.zig | 6 | ||||
| -rw-r--r-- | src/builtins.zig | 55 | ||||
| -rw-r--r-- | src/config.zig | 1 | ||||
| -rw-r--r-- | src/edit.zig | 2 | ||||
| -rw-r--r-- | src/exec.zig | 101 | ||||
| -rw-r--r-- | src/fs-help.txt | 4 | ||||
| -rw-r--r-- | src/mouse.zig | 5 | ||||
| -rw-r--r-- | src/panes.zig | 17 | ||||
| -rw-r--r-- | src/pardes.zig | 127 | ||||
| -rw-r--r-- | src/tagline.zig | 15 |
12 files changed, 322 insertions, 14 deletions
diff --git a/src/File.zig b/src/File.zig index af538385..8ba58fbc 100644 --- a/src/File.zig +++ b/src/File.zig @@ -1130,7 +1130,7 @@ pub fn drawGutter(p: *Pardes, pane: *Pane, r: pardes.Rect, tx: u16, tw: u16, bod const goff = pane.scroll(); const gcur = Terminal.gridCursor(pane); const gcrow = if (pane.body.cur_pinned) pane.body.cur_row else @as(i32, gcur.y) + goff; - const typing_body = pane.focus == .body and (pane.prompt == .none or pane.prompt == .del_side); + const typing_body = pane.focus == .body and (pane.prompt == .none or pane.prompt == .del_side or pane.prompt == .repl_choice); const cur_line: i32 = if (active and typing_body) gcrow else std.math.minInt(i32); // the body's first row, the way renderPane derives it (Tagbottom) const body_y = p.bodyTop(pane, r); diff --git a/src/Messages.zig b/src/Messages.zig index 00352afe..f8178629 100644 --- a/src/Messages.zig +++ b/src/Messages.zig @@ -180,6 +180,7 @@ fn showMessage(p: *Pardes, id: usize, text: []const u8, stack: bool) void { pane.pushOlderMessage(); if (pane.msg_len == 0) pane.msg_slot = pane.freeMessageSlot(); pane.msg_announcement = false; + pane.msg_sent = false; pane.msg_status = !stack; const arrives = pane.msg_len == 0 or pane.msg_life.phase == .leaving; for (text[0..n], pane.msg[0..n]) |c, *cell| cell.* = if (c < 0x20 or c == 0x7f) ' ' else c; diff --git a/src/body_layer.zig b/src/body_layer.zig index c468bd5f..c8e229c4 100644 --- a/src/body_layer.zig +++ b/src/body_layer.zig @@ -479,7 +479,7 @@ pub fn renderBody(p: *Pardes, arena: std.mem.Allocator, pane: *Pane, r: Rect, id // cursor: tracks the shell cursor until pinned by a click or a key // (the tag's or a prompt's cursor wins while that is being typed into) - if (active and pane.focus == .body and (pane.prompt == .none or pane.prompt == .del_side)) { + if (active and pane.focus == .body and (pane.prompt == .none or pane.prompt == .del_side or pane.prompt == .repl_choice)) { if (pane.body.mode != .tty) { const cur = panes.Terminal.gridCursor(pane); const goff = panes.Terminal.gridOffset(pane); @@ -797,12 +797,12 @@ pub fn renderTagLayers(p: *Pardes, arena: std.mem.Allocator) !void { .y = first + @as(u16, pane.notices.row[i]), .w = chip, .h = BOX_H, - }, line, Messages.blendRgb(chrome.tag_bg, page, fade), Messages.blendRgb(chrome.tag_fg, page, fade)); + }, line, Messages.blendRgb(chrome.tag_bg, page, fade), Messages.blendRgb(if (kind == .message and pane.msg_sent) chrome.tag_name_fg else chrome.tag_fg, page, fade)); p.surface.tag_layers[layer_index].slide = motion.slide; p.surface.tag_layers[layer_index].fade = fade; // The caret rides the band, where the prompt's glyphs are, // at the same place in the text the grid pass puts it. - if (kind != .prompt or id != p.active or pane.prompt == .del_side) continue; + if (kind != .prompt or id != p.active or pane.prompt == .del_side or pane.prompt == .repl_choice) continue; const typed = panes.File.displayWidth(text[0..@min(@as(usize, @intCast(@max(0, pane.input.cur_col))), text.len)]); const dropped = shown - panes.File.displayWidth(kept); if (typed < dropped) continue; diff --git a/src/builtins.zig b/src/builtins.zig index 5273cf37..be0956e0 100644 --- a/src/builtins.zig +++ b/src/builtins.zig @@ -852,6 +852,61 @@ pub const Tty9p = struct { /// Fold the active pane's column into the one on its right, keeping its panes. /// The horizontal mirror of the vertical stacking `New` does. +/// Binds this terminal as a language's REPL, by the language's name as the +/// syntax table has it: an exec made on the body of a file in that +/// language -- a middle click, the execute key, a single word like `ls` +/// as much as a selection -- and one made on this terminal's own body, is +/// typed into it (bracketed paste if its program asked for that, else line +/// by line, then Enter) instead of run. `Repl python` binds (again, it is +/// already); `Repl -` unbinds; `Repl` bare says what it is bound as. With +/// several bound for a language, the exec asks which. +/// Still commands, whatever is bound: a word in a tag, `Exec <text>` run by +/// name (typed, a 2-1 chord onto Exec, a ctl line), and a command word +/// @`cmd` in the text, looked at or clicked. +pub const Repl = struct { + pub const takes_arg = true; + pub fn run(c: Ctx) void { + const pane = c.pane; + if (!pane.isTerminal()) return c.p.reportFailure(c.id, "Repl: only a terminal is a REPL"); + const arg = std.mem.trim(u8, c.arg orelse "", " \t\r\n"); + var buf: [320]u8 = undefined; + var idbuf: [48]u8 = undefined; + if (arg.len == 0) { + if (pane.repl == null) return c.p.setMessage(c.id, "Repl: not bound"); + return c.p.setMessage(c.id, std.fmt.bufPrint(&buf, "Repl: {s}", .{exec.replId(&idbuf, pane)}) catch "Repl"); + } + if (std.mem.eql(u8, arg, "-")) { + pane.repl = null; + return c.p.setMessage(c.id, "Repl: unbound"); + } + const grammars = @import("grammar_manifest.zig").all; + const lang: u8 = for (grammars, 0..) |g, i| { + if (g.exts.len > 0 and std.mem.eql(u8, g.name, arg)) break @intCast(i); + } else { + var w = std.Io.Writer.fixed(&buf); + w.print("Repl: no language \"{s}\"; one of", .{arg[0..@min(arg.len, 32)]}) catch {}; + for (grammars) |g| if (g.exts.len > 0) w.print(" {s}", .{g.name}) catch break; + return c.p.reportFailure(c.id, w.buffered()); + }; + if (pane.repl) |r| if (r.lang == lang) + return c.p.setMessage(c.id, std.fmt.bufPrint(&buf, "Repl: {s}", .{exec.replId(&idbuf, pane)}) catch "Repl"); + // The lowest letter no other REPL of the language has: freed ones + // come back. + var letter: u8 = 'a'; + while (letter <= 'z') : (letter += 1) { + const taken = for (c.p.panes) |slot| { + const other = slot orelse continue; + if (other != pane) if (other.repl) |r| if (r.lang == lang and r.letter == letter) break true; + } else false; + if (!taken) break; + } + if (letter > 'z') return c.p.reportFailure(c.id, "Repl: every letter of the language is taken"); + pane.repl = .{ .lang = lang, .letter = letter }; + pane.repl_warned = false; + c.p.setMessage(c.id, std.fmt.bufPrint(&buf, "Repl: {s}", .{exec.replId(&idbuf, pane)}) catch "Repl"); + } +}; + pub const Joincol = struct { pub const scope: Scope = .session; pub fn run(c: Ctx) void { diff --git a/src/config.zig b/src/config.zig index 1a505334..9ad9cc7e 100644 --- a/src/config.zig +++ b/src/config.zig @@ -48,6 +48,7 @@ pub const leader_path = paths: { .Mode = null, .Togglettymode = null, .Collapse = null, + .Repl = null, .Kill = null, .Exit = null, .ClipYank = "y", diff --git a/src/edit.zig b/src/edit.zig index b2d71ed2..3cbbc28c 100644 --- a/src/edit.zig +++ b/src/edit.zig @@ -87,7 +87,7 @@ pub fn applyPaste(p: *Pardes, bytes: []const u8) void { const pane = p.panes[id] orelse return; if (req) |r| if (pane.serial != r.serial) return; p.active = id; - if (pane.prompt != .none and pane.prompt != .del_side) { + if (pane.prompt != .none and pane.prompt != .del_side and pane.prompt != .repl_choice) { // an answer is one line: a newline in it would only be submitting it const line = p.scratch.allocator().alloc(u8, bytes.len) catch return; var n: usize = 0; diff --git a/src/exec.zig b/src/exec.zig index 8b058a12..1b82d2a8 100644 --- a/src/exec.zig +++ b/src/exec.zig @@ -16,6 +16,7 @@ const colors = @import("colors.zig"); const themes = colors.themes; const builtins = @import("builtins.zig"); const config = @import("config.zig"); +const grammars = @import("grammar_manifest.zig").all; const pdf_enabled = panes.Pdf.enabled; const font_picker = pardes.font_picker; const hosted = pardes.hosted; @@ -369,6 +370,96 @@ pub fn replaceStillborn(p: *Pardes) void { if (was_active) p.active = new_id; } +// ---- REPLs ---- +// +// A terminal is bound as a language's REPL by `Repl python` (builtins.zig). +// An exec made by a gesture on the body of a file in that language -- or on +// the REPL's own body -- is sent to it as input rather than run: all of it, +// a word like `ls` too. The tag's words, `Exec` by name and a command word +// `@`...`` still run as commands. + +/// 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; +} + +/// 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); +} + +/// `python-a`. +pub fn replId(buf: []u8, pane: *const Pane) []const u8 { + const r = pane.repl orelse return ""; + return std.fmt.bufPrint(buf, "{s}-{c}", .{ grammars[r.lang].name, r.letter }) catch ""; +} + +const Routed = union(enum) { none, sent: usize, 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)) { + 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) { + bound[n] = i; + n += 1; + }; + if (n == 0) return .none; + if (n == 1) { + sendToRepl(p, from, bound[0], text); + return .{ .sent = bound[0] }; + } + // Several: asked on the pane's notice band, as Del asks, one key + // answering; nothing is remembered of the answer. + 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 }; + 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 {}; + w.writeAll(" (Esc: none)") catch {}; + src.question_len = @intCast(w.buffered().len); + src.prompt = .repl_choice; + p.active = from; + return .asked; +} + +/// Types `text` into the REPL in `to`, bracketed if its program asked for +/// that, then Enter; says so on `from`'s message row and in the log. +pub fn sendToRepl(p: *Pardes, from: usize, to: usize, text: []const u8) void { + const dst = p.panes[to] orelse return; + const src = p.panes[from] orelse return; + var idbuf: [48]u8 = undefined; + const id = replId(&idbuf, dst); + edit.typeToTty(p, to, dst, text); + 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 + std.mem.indexOfScalar(u8, text, '\n') != null) + { + dst.repl_warned = true; + var warn: [128]u8 = undefined; + p.setMessage(from, std.fmt.bufPrint(&warn, "Repl: {s} takes no bracketed paste; a blank line in a block ends it", .{id}) catch "Repl: no bracketed paste"); + } + var said: [64]u8 = undefined; + p.setMessage(from, std.fmt.bufPrint(&said, "\u{2192} {s}", .{id}) catch "sent"); + src.msg_sent = true; + var rec: [96]u8 = undefined; + pardes.ctlfs.events.notePath(p, "send", std.fmt.bufPrint(&rec, "{d} {d} {s}", .{ src.serial, dst.serial, id }) catch return); +} + /// The longest line a command pane runs. pub const command_max = 1024; @@ -381,6 +472,11 @@ const max_exec_depth = 8; /// done, and which Kill signals. Answers the pane it went to. pub fn execute(p: *Pardes, id: usize, txt: []const u8) ?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 cmd = commandText(txt); if (cmd.len == 0) return null; if (p.exec_depth == 0) p.noteHaptic(.exec); @@ -388,6 +484,11 @@ pub fn execute(p: *Pardes, id: usize, txt: []const u8) ?usize { if (p.exec_depth >= max_exec_depth) return null; p.exec_depth += 1; defer p.exec_depth -= 1; + if (gesture) switch (routeToRepl(p, id, cmd)) { + .none => {}, + .sent => |to| return to, + .asked => return null, + }; if (!takesCommandLine(p, id)) return runCommand(p, id, cmd); panes.Terminal.padOutputBelowEdits(p, id); panes.Terminal.noteCommand(pane, cmd); diff --git a/src/fs-help.txt b/src/fs-help.txt index 0e87f40e..79701628 100644 --- a/src/fs-help.txt +++ b/src/fs-help.txt @@ -41,5 +41,5 @@ Pitfalls, one each: A terminal's body is a history snapshot frozen per open; pty/data is the live stream. exec: a non-builtin line runs as a command pane ($SHELL -c; exit N shown, logged), a shell's typed in. Through a mount a malformed write is EINVAL, a well-formed one that fails EIO: log's err says why. - lock/unlock on a pane's ctl need a held fd (exec 3>ctl); a lock someone holds fails: retry. - Closing a column's last pane leaves a +New in its place; Delcol closes the column. + lock/unlock need a held ctl fd (exec 3>ctl), a held lock fails: retry. A column's last pane -> +New. + Repl python on a terminal's ctl: a .py body's clicks go to it; tag words, Exec x, @`x` stay commands. diff --git a/src/mouse.zig b/src/mouse.zig index 0ec20df2..f222b5da 100644 --- a/src/mouse.zig +++ b/src/mouse.zig @@ -190,7 +190,7 @@ pub fn handleMouse(p: *Pardes, m: Mouse) void { else => false, }) for (p.panes) |slot| { const pane = slot orelse continue; - if (pane.prompt == .del_side) pane.prompt = .none; + if (pane.prompt == .del_side or pane.prompt == .repl_choice) pane.prompt = .none; }; if (m.button == .back or m.button == .forward) { if (m.kind == .press) p.jumpBy(if (m.button == .back) -1 else 1); @@ -719,6 +719,9 @@ fn dispatchPointerBuiltin( // nothing, as acme's look3 and execute return on an empty expansion. const operand = text orelse return; if (gesture) |g| if (reportGesture(p, id, cmd, operand, g.on_tag, g.operand, arg != null)) return; + // A middle click on a body is the exec a bound REPL may take. + p.exec_from_body = cmd == config.exec_cmd and gesture != null and !gesture.?.on_tag; + defer p.exec_from_body = false; exec.runBuiltin(p, cmd, id, "", exec.withArg(p, operand, arg)); } diff --git a/src/panes.zig b/src/panes.zig index 2f8b6b56..9234912f 100644 --- a/src/panes.zig +++ b/src/panes.zig @@ -72,6 +72,10 @@ pub const Pane = struct { /// Del from the keyboard with open panes both above and below: which /// one takes the rows. Nothing is typed; one key answers. del_side, + /// An exec from a file's body with several REPLs bound for its + /// language: which one takes it (exec.zig routeToRepl). One key + /// answers, the question being in `question`. + repl_choice, /// A new name for the file, drafted in place of the path on the /// tagline after a click there; Enter commits it, as a buffer name /// and never a disk rename. @@ -116,6 +120,18 @@ pub const Pane = struct { /// (Pardes.acknowledgeShell); empty until the host says which it ran. shell_tail: [96]u8 = undefined, shell_tail_len: u8 = 0, + /// A terminal bound as a language's REPL (`Repl python`): an exec from + /// the body of a file in that language is sent to it (exec.zig + /// routeToRepl). `letter` makes its id, `python-a`, with the language's + /// name from grammar_manifest. + repl: ?struct { lang: u8, letter: u8 } = null, + /// It has been told that its program takes no bracketed paste. + repl_warned: bool = false, + /// A question with its answers in it (`repl_choice`). + question: [96]u8 = undefined, + question_len: u8 = 0, + /// The newest message says where an exec was sent: it is tinted. + msg_sent: bool = false, /// The revision an Exit or a Restore last warned this pane was modified /// at, and which of them: only that word goes ahead on the warning /// (builtins.zig warnModified). @@ -360,6 +376,7 @@ pub const Pane = struct { return switch (p.prompt) { .none, .name => null, .del_side => config.del_question, + .repl_choice => p.question[0..p.question_len], else => p.input.own orelse "", }; } diff --git a/src/pardes.zig b/src/pardes.zig index 5ba95be3..9821839f 100644 --- a/src/pardes.zig +++ b/src/pardes.zig @@ -1391,6 +1391,100 @@ test "a spawn is for the pane it was made for, not whatever took the slot since" try std.testing.expectEqualStrings("second", Count.command[0.."second".len]); } +test "a REPL bound for a language takes the exec made on a body in it; tags, Exec by name and command words still run" { + const gpa = std.testing.allocator; + const p = try Pardes.init(gpa, .{ .shells = 3, .cols = 100, .rows = 30 }); + defer p.deinit(); + while (p.nextEffect()) |_| {} + const file = try p.setTestFile("print(1)\nls\n"); + gpa.free(file.file.?.path); + file.file.?.path = try gpa.dupe(u8, "/x.py"); + const Sent = struct { + var to: [MAX_PANES][128]u8 = undefined; + var len: [MAX_PANES]usize = @splat(0); + var spawned: bool = false; + fn drain(pp: *Pardes) void { + len = @splat(0); + spawned = false; + while (pp.nextEffect()) |effect| switch (effect) { + .write => |w| { + @memcpy(to[w.pane][len[w.pane]..][0..w.bytes.slice().len], w.bytes.slice()); + len[w.pane] += w.bytes.slice().len; + }, + .spawn => spawned = true, + else => {}, + }; + } + fn said(pane: usize) []const u8 { + return to[pane][0..len[pane]]; + } + }; + // Only a terminal is a REPL; a language's name is the syntax table's. + try std.testing.expect(p.executeBuiltinLine(0, "Repl python")); + try std.testing.expect(file.repl == null); + try std.testing.expect(p.executeBuiltinLine(1, "Repl pithon")); + try std.testing.expect(p.panes[1].?.repl == null); + try std.testing.expect(std.mem.indexOf(u8, p.panes[1].?.msg[0..p.panes[1].?.msg_len], "python") != null); + try std.testing.expect(p.executeBuiltinLine(1, "Repl python")); + try std.testing.expect(p.executeBuiltinLine(1, "Repl python")); // again: no change + var idbuf: [48]u8 = undefined; + try std.testing.expectEqualStrings("python-a", exec.replId(&idbuf, p.panes[1].?)); + try std.testing.expect(std.mem.endsWith(u8, try tagline.tagPrefix(p, p.panes[1].?), " python-a")); + Sent.drain(p); + + // A click on the file's body, a single word: typed into the REPL. + p.exec_from_body = true; + try std.testing.expectEqual(@as(?usize, 1), exec.execute(p, 0, "ls")); + Sent.drain(p); + try std.testing.expectEqualStrings("ls\r", Sent.said(1)); + try std.testing.expect(!Sent.spawned); + try std.testing.expect(file.msg_sent); + try std.testing.expectEqualStrings("\u{2192} python-a", file.msg[0..file.msg_len]); + // Its own body too. + p.exec_from_body = true; + try std.testing.expectEqual(@as(?usize, 1), exec.execute(p, 1, "x = 1")); + Sent.drain(p); + try std.testing.expectEqualStrings("x = 1\r", Sent.said(1)); + + // Escapes: a tag's word, Exec by name, a command word. Each a command. + _ = exec.execute(p, 0, "ls"); + Sent.drain(p); + try std.testing.expect(Sent.spawned and Sent.len[1] == 0); + try std.testing.expect(p.executeBuiltinLine(0, "Exec ls")); + Sent.drain(p); + try std.testing.expect(Sent.spawned and Sent.len[1] == 0); + p.exec_from_body = true; + _ = exec.execute(p, 0, "@`ls`"); + Sent.drain(p); + try std.testing.expect(Sent.spawned and Sent.len[1] == 0); + + // Two bound: it asks, one key answers, Esc sends nowhere. + try std.testing.expect(p.executeBuiltinLine(2, "Repl python")); + try std.testing.expectEqualStrings("python-b", exec.replId(&idbuf, p.panes[2].?)); + Sent.drain(p); + p.exec_from_body = true; + try std.testing.expectEqual(@as(?usize, null), exec.execute(p, 0, "print(2)")); + try std.testing.expectEqual(Pane.Prompt.repl_choice, file.prompt); + try std.testing.expect(std.mem.indexOf(u8, file.promptText().?, "a b") != null); + p.update(.{ .key = .{ .cp = 'b' } }); + Sent.drain(p); + try std.testing.expectEqualStrings("print(2)\r", Sent.said(2)); + try std.testing.expect(Sent.len[1] == 0 and file.prompt == .none); + p.exec_from_body = true; + _ = exec.execute(p, 0, "print(3)"); + p.update(.{ .key = .{ .cp = Key.escape } }); + Sent.drain(p); + try std.testing.expect(Sent.len[1] == 0 and Sent.len[2] == 0); + + // Unbound, its letter is free again; bare, it says what it is. + try std.testing.expect(p.executeBuiltinLine(1, "Repl -")); + try std.testing.expect(p.panes[1].?.repl == null); + try std.testing.expect(p.executeBuiltinLine(1, "Repl python")); + try std.testing.expectEqualStrings("python-a", exec.replId(&idbuf, p.panes[1].?)); + try std.testing.expect(p.executeBuiltinLine(2, "Repl")); + try std.testing.expectEqualStrings("Repl: python-b", p.panes[2].?.msg[0..p.panes[2].?.msg_len]); +} + test "Tty+fish, one word a tag can hold, opens a terminal on that shell" { const p = try Pardes.init(std.testing.allocator, .{ .tty_only = true }); defer p.deinit(); @@ -3929,6 +4023,13 @@ pub const Pardes = struct { chord_arg: ?[]u8 = null, /// how many `execute` calls are on the stack — see max_exec_depth exec_depth: u8 = 0, + /// The exec under way was made by a gesture on a pane's body -- a + /// middle click, the execute key -- and so may go to a REPL bound for + /// the text's language; one run by name, `Exec ...`, never does. + exec_from_body: bool = false, + /// An exec waiting on the REPL question (`Pane.Prompt.repl_choice`): + /// the pane it came from, the language, and the text, owned. + repl_ask: ?struct { from: u32, lang: u8, text: []u8 } = null, /// A key press is being handled: someone is at the keyboard to answer, /// so a builtin may ask rather than guess. Nothing else sets it -- not /// a click, a 9P write, a startup line or a pane's shell exiting. @@ -4138,6 +4239,7 @@ pub const Pardes = struct { pub fn deinit(p: *Pardes) void { look.cancelLookHover(p); + if (p.repl_ask) |ask| p.gpa.free(ask.text); for (0..MAX_PANES) |id| p.dropPendingWrite(id); for (&p.panes) |*slot| if (slot.*) |pane| { p.teardownPane(pane); @@ -5015,6 +5117,18 @@ pub const Pardes = struct { // pane or a header edit is in: that key is its answer. for (p.panes, 0..) |slot, id| { const asked = slot orelse continue; + if (asked.prompt == .repl_choice) { + asked.prompt = .none; + const ask = p.repl_ask orelse return; + p.repl_ask = null; + defer p.gpa.free(ask.text); + // Its letter sends it there; anything else takes it back. + if (key.ctrl or key.alt or key.cp > 'z' or key.cp < 'a') return; + for (p.panes, 0..) |target, to| if (target) |repl| if (repl.repl) |r| + if (r.lang == ask.lang and r.letter == key.cp and exec.replLive(repl)) + return exec.sendToRepl(p, id, to, ask.text); + return; + } if (asked.prompt != .del_side) continue; asked.prompt = .none; if (hit(key, config.del_above)) exec.runBuiltin(p, .Del, id, "", "k"); @@ -5126,7 +5240,7 @@ pub const Pardes = struct { look.submitSearch(p, p.active) else if (normal.selRegexArmed(pane)) |_| if (target) |answered| normal.applySelRegex(p, pane, answered, "", false), - .none, .del_side, .name => unreachable, + .none, .del_side, .repl_choice, .name => unreachable, } pane.closePrompt(); if (target) |answered| { @@ -5164,6 +5278,9 @@ pub const Pardes = struct { return; } edit.pinPaneCursor(p, &pane.body); + // The execute key on a body is the exec a bound REPL may take. + p.exec_from_body = cmd == config.exec_cmd; + defer p.exec_from_body = false; const explicit = (p.native_images and pane.hasPdfSelection()) or (pane.body.vsel.active and pane.body.vsel.explicit) or pane.body.msel.active; if (explicit) { @@ -5999,12 +6116,16 @@ pub const Pardes = struct { // A message coming or going blends toward the page it sits // on; a grid cannot move it by less than a row. var style = msg_style; + // Where an exec was sent to a REPL reads in the tag's name + // tint, set apart from what the editor says. + const ink = if (kind == .message and pane.msg_sent) chrome.tag_name_fg else chrome.tag_fg; + if (kind == .message and pane.msg_sent) style.fg = .{ .rgb = ink }; if (Messages.noticeLife(pane, i)) |life| { // No page colour to fade to (a terminal's own default): // the text dissolves into its chip instead. const alpha = Messages.messageMotion(p, life).alpha; const page = th.bg orelse chrome.tag_bg; - style.fg = .{ .rgb = Messages.blendRgb(chrome.tag_fg, page, alpha) }; + style.fg = .{ .rgb = Messages.blendRgb(ink, page, alpha) }; style.bg = .{ .rgb = Messages.blendRgb(chrome.tag_bg, page, alpha) }; } s.fill(cx, row, chip, 1, .{ .bg = style.bg, .font_role = .tagline }); @@ -6014,7 +6135,7 @@ pub const Pardes = struct { // the detached wire will encode -- which drops every frame for // as long as the prompt is up. const printed = Messages.printRight(s, cx, row, chip -| 1, text, style); - if (kind != .prompt or id != p.active or pane.prompt == .del_side) continue; + if (kind != .prompt or id != p.active or pane.prompt == .del_side or pane.prompt == .repl_choice) continue; // The cursor follows the text to wherever it landed; a caret // in the part a narrow band dropped has nowhere to be. const prompt_col = panes.File.displayWidth(text[0..@min(@as(usize, @intCast(@max(0, pane.input.cur_col))), text.len)]); diff --git a/src/tagline.zig b/src/tagline.zig index 8c1efa7b..14bf5677 100644 --- a/src/tagline.zig +++ b/src/tagline.zig @@ -7,6 +7,7 @@ const modal = @import("modal.zig"); const ctlfs = @import("ninep/tree.zig"); const panes = @import("panes.zig"); const edit = @import("edit.zig"); +const exec = @import("exec.zig"); const config = @import("config.zig"); const pdf_enabled = panes.Pdf.enabled; const dump = @import("dump.zig"); @@ -114,9 +115,17 @@ pub fn pathPrefix(arena: std.mem.Allocator, pane: *Pane) ![]const u8 { if (pane.command) |line| { const shown = line[0..@min(line.len, 48)]; const more = if (shown.len < line.len) "..." else ""; - if (!pane.command_done) return std.fmt.allocPrint(arena, "{s} ({s}{s}) running", .{ pane.cwdSlice(), shown, more }); - if (pane.command_status) |n| return std.fmt.allocPrint(arena, "{s} ({s}{s}) exit {d}", .{ pane.cwdSlice(), shown, more, n }); - return std.fmt.allocPrint(arena, "{s} ({s}{s}) exit ?", .{ pane.cwdSlice(), shown, more }); + var idbuf: [48]u8 = undefined; + const id = exec.replId(&idbuf, pane); + const gap = if (id.len > 0) " " else ""; + if (!pane.command_done) return std.fmt.allocPrint(arena, "{s} ({s}{s}) running{s}{s}", .{ pane.cwdSlice(), shown, more, gap, id }); + if (pane.command_status) |n| return std.fmt.allocPrint(arena, "{s} ({s}{s}) exit {d}{s}{s}", .{ pane.cwdSlice(), shown, more, n, gap, id }); + return std.fmt.allocPrint(arena, "{s} ({s}{s}) exit ?{s}{s}", .{ pane.cwdSlice(), shown, more, gap, id }); + } + // A bound REPL says what it is bound as, beside its Tty word. + if (pane.repl != null) { + var idbuf: [48]u8 = undefined; + return std.fmt.allocPrint(arena, "{s} {s}", .{ pane.cwdSlice(), exec.replId(&idbuf, pane) }); } if (pane.file) |f| { const name = if (pane.prompt == .name) pane.input.own orelse f.path else f.path; |
