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