diff options
Diffstat (limited to 'src/pardes.zig')
| -rw-r--r-- | src/pardes.zig | 94 |
1 files changed, 88 insertions, 6 deletions
diff --git a/src/pardes.zig b/src/pardes.zig index 6294b6cb..54fe2df0 100644 --- a/src/pardes.zig +++ b/src/pardes.zig @@ -250,7 +250,11 @@ pub const pdf_raster_policy: PdfRasterPolicy = switch (platform) { pub const Pane = panes.Pane; const Text = panes.Text; -pub const MAX_PANES = 16; +/// Pane slots: 64 on a desktop, where a session keeps many files and +/// terminals open; the board keeps its 16. A slot costs its share of the +/// fixed tables (MAX_PANES-sized arrays in the core and its surface) whether +/// or not a pane is in it; a pane itself is allocated when it opens. +pub const MAX_PANES = if (platform == .esp32p4) 16 else 64; pub const Layer = @import("Layer.zig"); pub const RowMetrics = Layer.Metrics; pub const TagHit = Layer.TagHit; @@ -1638,6 +1642,24 @@ test "a REPL bound for a language takes the exec made on a body in it; tags, Exe _ = exec.executeFrom(p, 0, "a = 1\r\nb = \x1b[201~2", true); Sent.drain(p); try std.testing.expectEqualStrings("a = 1\rb = [201~2\r", Sent.said(1)); + // Into a REPL that takes a bracketed paste (3.13's), the Enter waits + // for the REPL to answer the paste: read with it, it would be part of it. + p.update(.{ .output = .{ .pane = 1, .bytes = "\x1b[?2004h" } }); + Sent.drain(p); + _ = exec.executeFrom(p, 0, "print(1)", true); + Sent.drain(p); + try std.testing.expectEqualStrings("\x1b[200~print(1)\x1b[201~", Sent.said(1)); + p.update(.{ .output = .{ .pane = 1, .bytes = "print(1)" } }); + Sent.drain(p); + try std.testing.expectEqualStrings("\r", Sent.said(1)); + // Nothing answering, it goes when its wait is up. + _ = exec.executeFrom(p, 0, "print(2)", true); + p.stepped_ns += 300 * std.time.ns_per_ms; + p.update(.tick); + Sent.drain(p); + try std.testing.expect(std.mem.endsWith(u8, Sent.said(1), "\x1b[200~print(2)\x1b[201~\r")); + p.update(.{ .output = .{ .pane = 1, .bytes = "\x1b[?2004l" } }); + Sent.len[1] = 0; // A block left open by its indented last line gets the blank line that // ends it: a second Enter. _ = exec.executeFrom(p, 0, "def f():\n return 1", true); @@ -1684,6 +1706,9 @@ test "a REPL bound for a language takes the exec made on a body in it; tags, Exe _ = exec.executeFrom(p, 0, "print(5)", true); var want: [32]u8 = undefined; try std.testing.expect(th.logHas(p, try std.fmt.bufPrint(&want, "ask {d} repl a b\n", .{file.serial}))); + // A choice not offered is refused, and the question stands. + try std.testing.expectEqual(ctlfs.E.INVAL, th.wr(p, ctlfs.Node.of(file.serial, .ctl), "answer z\n").errno()); + try std.testing.expectEqual(Pane.Prompt.repl_choice, file.prompt); const answered = th.wr(p, ctlfs.Node.of(file.serial, .ctl), "answer a\n"); try std.testing.expectEqual(ctlfs.Status.ok, answered.reply.status); try std.testing.expectEqualStrings("print(5)\r", answered.pty()); @@ -3462,6 +3487,11 @@ pub const Mouse = struct { ctrl: bool = false, body_hit: ?BodyHit = null, tag_hit: ?TagHit = null, + /// A pixel frontend's press on the rule it draws at the top of this row, + /// which is where two panes meet if they do: the one grab that resizes + /// them there. A row of text never is one; a terminal, with no rule, + /// resizes by the grip. + rule_above: bool = false, }; pub const Key = struct { @@ -4260,6 +4290,12 @@ pub const Pardes = struct { /// spring. `lift_serials` says whose it is, so a reused slot starts flat. lifts: [MAX_PANES]animation.Spring = @splat(.{}), lift_serials: [MAX_PANES]u32 = @splat(0), + /// The focused cursor's quad on its springs (animation.CursorGlide), + /// the pane and view origin it was last aimed in, and the last key or + /// click, which a blink holds for. + cursor_glide: animation.CursorGlide = .{}, + cursor_view: struct { serial: u32 = 0, line: i64 = 0 } = .{}, + input_ns: u64 = 0, animate_theme_changes: bool = false, native_images: bool = false, /// The shell can take a changed image a few rows at a time (a texture row @@ -4771,7 +4807,7 @@ pub const Pardes = struct { /// A scratch for New (`.scratch`) or 9P's pane/new (`.fs`). pub fn newScratchFor(p: *Pardes, from_id: usize, kind: exec.NewPane) void { - const free = p.freeSlot() orelse return; + const free = p.freeSlot() orelse return p.reportError(from_id, "New", error.NoPaneSlots); _ = p.newScratch(from_id, free) catch return; exec.placeNew(p, from_id, from_id, free, kind); } @@ -5102,6 +5138,20 @@ pub const Pardes = struct { p.fs.late_failure_len = @intCast(kept.len); } + /// A Dump the host could not write: said on the message row and in the + /// log, and the 9P write that asked for it fails with it, as a failed + /// Save's does. + pub fn dumpFailed(p: *Pardes, path: []const u8, err: anyerror) void { + var what: [limits.host_path_cap + 8]u8 = undefined; + const operation = std.fmt.bufPrint(&what, "Dump {s}", .{path}) catch "Dump"; + p.reportError(p.active, operation, err); + var buf: [256]u8 = undefined; + const said = std.fmt.bufPrint(&buf, "{s}: {s}", .{ operation, @errorName(err) }) catch "Dump failed"; + const kept = @import("Messages.zig").clip(said, p.fs.late_failure.len); + @memcpy(p.fs.late_failure[0..kept.len], kept); + p.fs.late_failure_len = @intCast(kept.len); + } + /// The path a watch is about: a real file's, or a PDF's. fn watchPath(p: *const Pardes, id: u8) ?[]const u8 { const pane = p.panes[id] orelse return null; @@ -5265,7 +5315,7 @@ pub const Pardes = struct { p.raw_hover_intent = false; look.cancelLookHover(p); const reply = ctlfs.handle(p, req); - if (reply.status == .err and (req.op == .write or req.op == .setattr)) ctlfs.events.noteError(p, req, reply); + if (reply.status == .err and (req.op == .write or req.op == .setattr or (req.op == .open and reply.errno == ctlfs.E.NFILE) or (req.op == .release and req.remove))) ctlfs.events.noteError(p, req, reply); // The request was a whole step of its own, so it settles the way a // step does: the cursor and scroll reconciled, the scripted panes // told, and the panes it made announced to /log now rather than at @@ -5311,12 +5361,16 @@ pub const Pardes = struct { } switch (ev) { .key, .mouse => { + if (ev == .key or ev.mouse.kind != .motion) p.input_ns = p.now_ns; for (p.panes) |slot| { if (slot) |pane| Messages.dismissMessage(p, pane); } p.clip_pending = null; }, - .tick => Messages.advanceMessages(p), + .tick => { + Messages.advanceMessages(p); + for (0..p.panes.len) |id| exec.flushReplEnter(p, id, false); + }, else => {}, } if (p.fs.listeners != 0) p.fs.origin = switch (ev) { @@ -5360,6 +5414,7 @@ pub const Pardes = struct { ctlfs.events.notePtyOutput(p, o.pane, o.bytes); panes.Terminal.feedOutput(p, pane, o.bytes); ctlfs.pty.noteMarks(p, o.pane, pane); + exec.flushReplEnter(p, o.pane, true); }, .exited => |e| if (p.panes[e.pane]) |pane| { if (pane.command == null) @@ -5499,6 +5554,25 @@ pub const Pardes = struct { /// the REPL to send to (its letter). Null, or a choice not offered, /// takes the question back. A key answers it, or `answer` on the pane's /// ctl; false when the pane asks nothing. + /// The choices the question pane `id` asks offers, space-separated + /// (`k j`, `a b`), or null when it asks none. + pub fn questionChoices(p: *const Pardes, id: usize, buf: []u8) ?[]const u8 { + const asked = p.panes[id] orelse return null; + switch (asked.prompt) { + .del_side => return "k j", + .repl_choice => { + const ask = p.repl_ask orelse return null; + var w = std.Io.Writer.fixed(buf); + for (ask.offered, 0..) |serial, i| if (serial != 0) { + if (w.buffered().len > 0) w.writeByte(' ') catch {}; + w.writeByte(@intCast('a' + i)) catch {}; + }; + return w.buffered(); + }, + else => return null, + } + } + pub fn answerQuestion(p: *Pardes, id: usize, choice: ?u8) bool { const asked = p.panes[id] orelse return false; switch (asked.prompt) { @@ -5602,7 +5676,7 @@ pub const Pardes = struct { // global window ops (any mode): Alt-n new terminal below, Alt-c move // the active terminal into a fresh column if (hit(key, config.new_shell_below)) { - const free = p.freeSlot() orelse return; + const free = p.freeSlot() orelse return p.reportError(p.active, "Tty", error.NoPaneSlots); const nt = p.newShell(free, "") catch return; nt.greet = true; // From a tag, as a 9P new is: acme's t->w == nil. @@ -6438,16 +6512,22 @@ pub const Pardes = struct { const wait = Messages.messageWake(p, pane) orelse continue; frames = if (frames) |soonest| @min(soonest, wait) else wait; }; + // A paste's Enter waiting on its REPL goes at the end of its wait. + for (p.panes) |slot| if (slot) |pane| if (pane.repl_enter > 0) { + const wait = @max(1, (pane.repl_enter_due_ns -| p.stepped_ns) / animation.frame_ns); + frames = if (frames) |soonest| @min(soonest, wait) else wait; + }; return if (frames) |n| p.stepped_ns + n * animation.frame_ns else null; } + /// A render-time animation is moving (a pane's lift): it is sampled at /// each frame's own `now`, so while it moves the shell draws every loop /// and its present (vsync) paces it, at the display's rate. `nextWake` /// is for the stepped animations, which count frames of `frame_ns`. pub fn continuous(p: *const Pardes) bool { for (p.lifts) |spring| if (!spring.settled) return true; - return false; + return p.cursor_glide.moving(); } /// For shells that only need a yes or no: macOS and the board, which @@ -7001,6 +7081,8 @@ test "Del from a key asks which neighbour takes the rows; nothing else asks" { p.update(.{ .key = .{ .cp = 'd' } }); var want: [32]u8 = undefined; try std.testing.expect(th.logHas(p, try std.fmt.bufPrint(&want, "ask {d} del k j\n", .{serial}))); + try std.testing.expectEqual(ctlfs.E.INVAL, th.wr(p, ctl, "answer x\n").errno()); + try std.testing.expectEqual(Pane.Prompt.del_side, p.panes[asked].?.prompt); try std.testing.expectEqual(ctlfs.Status.ok, th.wr(p, ctl, "answer -\n").reply.status); try std.testing.expect(p.panes[asked] != null and p.panes[asked].?.prompt == .none); p.update(.{ .key = .{ .cp = ' ' } }); |
