summaryrefslogtreecommitdiff
path: root/src/ninep/events.zig
diff options
context:
space:
mode:
Diffstat (limited to 'src/ninep/events.zig')
-rw-r--r--src/ninep/events.zig110
1 files changed, 104 insertions, 6 deletions
diff --git a/src/ninep/events.zig b/src/ninep/events.zig
index 69dc271d..ffdea95b 100644
--- a/src/ninep/events.zig
+++ b/src/ninep/events.zig
@@ -7,6 +7,7 @@ const look = @import("../look.zig");
const cloud9 = @import("cloud9");
const tree = @import("tree.zig");
const pane_files = @import("pane.zig");
+const dump = @import("../dump.zig");
const Pardes = pardes.Pardes;
const Pane = pardes.Pane;
@@ -155,6 +156,13 @@ pub fn noteMessage(p: *Pardes, serial: u32, text: []const u8) void {
/// kernel mapped the reply to (`Invalid argument`), and here is the reason.
/// The log is the one place for it, as acme's `errors` file takes text and
/// answers nothing: a per-pane readable error file would be a second.
+/// `dump <path>` when a Dump is written, `restore <path>` first in a
+/// Restore's replacement.
+pub fn notePath(p: *Pardes, what: []const u8, path: []const u8) void {
+ var buf: [pardes.memory.limits.host_path_cap + 16]u8 = undefined;
+ pushLog(p, std.fmt.bufPrint(&buf, "{s} {s}\n", .{ what, path }) catch return);
+}
+
pub fn noteError(p: *Pardes, req: Req, reply: Reply) void {
const why = if (reply.ename.len > 0) reply.ename else cloud9.fs.errString(reply.errno);
var name: [16]u8 = undefined;
@@ -460,7 +468,7 @@ pub fn notePtyOutput(p: *Pardes, id: usize, bytes: []const u8) void {
p.fs.news = true;
}
-const EventRecord = struct { action: Action, q0: u32, q1: u32 };
+const EventRecord = struct { origin: u8, action: Action, q0: u32, q1: u32, text: ?[]const u8 = null };
const EventReader = struct {
data: []const u8,
@@ -470,15 +478,27 @@ const EventReader = struct {
if (er.i >= er.data.len) return null;
var i = er.i;
if (i + 2 > er.data.len) return null;
+ const origin = er.data[i];
i += 1;
const action = Action.fromChar(er.data[i]) orelse return null;
i += 1;
const q0 = scanNumber(er.data, &i) orelse return null;
const q1 = scanNumber(er.data, &i) orelse return null;
while (i < er.data.len and er.data[i] == ' ') i += 1;
+ // acme takes only `origin action q0 q1`; the record whole, as read,
+ // is taken too, so a click with no place in the text (one written
+ // to a pane's own look or exec) can be written back and done.
+ var text: ?[]const u8 = null;
+ if (i < er.data.len and er.data[i] != '\n') {
+ _ = scanNumber(er.data, &i) orelse return null;
+ const n = scanNumber(er.data, &i) orelse return null;
+ if (i >= er.data.len or er.data[i] != ' ' or er.data.len - (i + 1) < n) return null;
+ text = er.data[i + 1 ..][0..n];
+ i += 1 + n;
+ }
if (i >= er.data.len or er.data[i] != '\n') return null;
er.i = i + 1;
- return .{ .action = action, .q0 = q0, .q1 = q1 };
+ return .{ .origin = origin, .action = action, .q0 = q0, .q1 = q1, .text = text };
}
};
@@ -517,9 +537,15 @@ pub fn writeEvent(p: *Pardes, req: Req, id: usize) Reply {
const whole = if (r.action.onTag()) pane_files.tagOf(p, pane) else pane_files.bodyOf(pane);
const lo = @min(@as(usize, r.q0), whole.len);
const hi = @max(lo, @min(@as(usize, r.q1), whole.len));
- const text = p.scratch.allocator().dupe(u8, whole[lo..hi]) catch continue;
+ const said = if (r.q0 == r.q1) r.text orelse whole[lo..hi] else whole[lo..hi];
+ const text = p.scratch.allocator().dupe(u8, said) catch continue;
switch (r.action) {
- .body_exec, .tag_exec => _ = exec.execute(p, live, text),
+ // A body's click written back is done as the click would have
+ // been (acme's xfideventwrite, xfid.c:842-844): a REPL bound for
+ // it takes it. One that came from a write to exec (`F`) was
+ // never a click, and runs as the command it was.
+ .body_exec => _ = exec.executeFrom(p, live, text, r.origin != 'F'),
+ .tag_exec => _ = exec.execute(p, live, text),
.body_look, .tag_look => look.lookAt(p, live, text),
else => unreachable,
}
@@ -664,7 +690,8 @@ test "a pane deleted while its event file is open leaves no suppression behind"
const b = call(p, .{ .tag = 19, .op = .open, .node = event, .omode = 1 });
try testing.expectEqual(@as(u16, 2), p.fs.listeners);
- _ = wr(p, Node.of(serial, .exec), "Del\n");
+ // Through ctl: a line written to its exec would go to the reader.
+ _ = wr(p, Node.of(serial, .ctl), "Del\n");
try testing.expect(p.paneBySerial(serial) == null);
try testing.expectEqual(@as(u16, 0), p.fs.listeners);
// The reader's next read hears what acme says of a window gone under it.
@@ -850,7 +877,7 @@ test "a refused or failed write is an err record in the log, saying which file a
_ = wr(p, @intFromEnum(tree.TopFile.ctl), "Bogus");
try testing.expectEqualStrings("err - ctl: unknown control message \"Bogus\"\n", next.record(p, f, frozen));
_ = wr(p, @intFromEnum(tree.TopFile.focus), "99999");
- try testing.expectEqualStrings("err - focus: no such window\n", next.record(p, f, frozen));
+ try testing.expectEqualStrings("err - focus: no such pane\n", next.record(p, f, frozen));
// A write that succeeds says nothing.
_ = wr(p, Node.of(serial, .addr), "#0");
try testing.expectEqual(Status.again, call(p, .{ .tag = 5, .op = .read, .node = log, .handle = f, .off = frozen, .size = 4096 }).reply.status);
@@ -870,6 +897,21 @@ test "a refused or failed write is an err record in the log, saying which file a
_ = call(p, .{ .tag = 9, .op = .release, .node = log, .handle = g });
}
+test "a Dump written and a Restore made are in the log, with their files" {
+ const p = try withFile(testing.allocator, "one\n");
+ defer p.deinit();
+ p.setLastDump("/tmp/pardes.dump.zon");
+ try dump.dumpState(p);
+ const restored = try dump.restore(p, p.dump_out.?, "/tmp/pardes.dump.zon");
+ defer restored.deinit();
+ for ([_]*Pardes{ p, restored }, [_][]const u8{ "dump", "restore" }) |core, what| {
+ const log = try freezeLog(core);
+ defer core.gpa.free(log.bytes);
+ var want: [64]u8 = undefined;
+ try testing.expect(std.mem.endsWith(u8, log.bytes, try std.fmt.bufPrint(&want, "{s} /tmp/pardes.dump.zon\n", .{what})));
+ }
+}
+
test "opens of the log share the open records, and a closed one frees its record" {
const gpa = testing.allocator;
const p = try withFile(gpa, "x\n");
@@ -885,3 +927,59 @@ test "opens of the log share the open records, and a closed one frees its record
for (handles) |h| _ = call(p, .{ .tag = 5, .op = .release, .node = log, .handle = h });
for (p.fs.opens) |o| try testing.expect(o.node == 0);
}
+
+test "a click in the body reports the offsets of the word it takes, so the record can be written back" {
+ const config = @import("../config.zig");
+ const gpa = testing.allocator;
+ const text = "alpha qty beta\n qty\n\tqty\ndef f(qty): return qty.x\n" ++ "w" ** 100 ++ " qty\n";
+ const p = try withFile(gpa, text);
+ defer p.deinit();
+ // A scratch made over 9P, clicked while another pane has the keyboard.
+ const scratch = try th.newPane(p);
+ _ = wr(p, Node.of(scratch, .body), text);
+ for ([_]u32{ serialOf(p), scratch }) |serial| {
+ const id = p.paneBySerial(serial).?;
+ p.active = p.paneBySerial(if (serial == scratch) serialOf(p) else scratch).?;
+ const event = Node.of(serial, .event);
+ const h = call(p, .{ .tag = 1, .op = .open, .node = event }).reply.handle;
+ var frame = std.heap.ArenaAllocator.init(gpa);
+ defer frame.deinit();
+ _ = try p.render(frame.allocator());
+ const rect = p.rects[id];
+ const x0 = rect.x + config.GUTTER + config.PREFIX_W;
+ const y0 = if (p.settings.tag_bottom) rect.y else rect.y + p.panes[id].?.tag_rows;
+ const tab: u16 = config.tab_width;
+ const Click = struct { x: u16, y: u16, look: []const u8, exec: []const u8, scroll: i32 = 0 };
+ // Where the wrapped line's `qty` landed, as drawn.
+ const surface = try p.render(frame.allocator());
+ var wrap_col: u16 = 0;
+ while (wrap_col < rect.w and !std.mem.eql(u8, surface.at(x0 + wrap_col, y0 + 5).grapheme(), "q")) wrap_col += 1;
+ for ([_]Click{
+ .{ .x = x0 + 7, .y = y0, .look = "ML6 9 0 3 qty\n", .exec = "MX6 9 0 3 qty\n" },
+ .{ .x = x0 + 5, .y = y0 + 1, .look = "ML19 22 0 3 qty\n", .exec = "MX19 22 0 3 qty\n" },
+ .{ .x = x0 + tab + 1, .y = y0 + 2, .look = "ML24 27 0 3 qty\n", .exec = "MX24 27 0 3 qty\n" },
+ .{ .x = x0 + 7, .y = y0 + 3, .look = "ML34 37 0 3 qty\n", .exec = "MX34 37 0 3 qty\n" },
+ // Scrolled a line, the first row shows the second.
+ .{ .x = x0 + 5, .y = y0, .look = "ML19 22 0 3 qty\n", .exec = "MX19 22 0 3 qty\n", .scroll = 1 },
+ // Wrapped: the word on the continuation row.
+ .{ .x = x0 + wrap_col, .y = y0 + 5, .look = "ML154 157 0 3 qty\n", .exec = "MX154 157 0 3 qty\n" },
+ }) |c| for ([_]pardes.Mouse.Button{ config.look_button, config.exec_button }, [_][]const u8{ c.look, c.exec }) |button, want| {
+ // The pointer rests on the word first, as a hand's does, and
+ // the hover preview comes up under it.
+ p.panes[id].?.scrollBy(c.scroll);
+ _ = try p.render(frame.allocator());
+ p.update(.{ .mouse = .{ .button = .none, .kind = .motion, .col = c.x, .row = c.y } });
+ for (0..200) |_| p.update(.tick);
+ p.update(.{ .mouse = .{ .button = button, .kind = .press, .col = c.x, .row = c.y } });
+ p.update(.{ .mouse = .{ .button = button, .kind = .release, .col = c.x, .row = c.y } });
+ try testing.expectEqualStrings(want, call(p, .{ .tag = 2, .op = .read, .node = event, .handle = h, .size = 256 }).bytes);
+ p.panes[id].?.scrollBy(-c.scroll);
+ };
+ // A click on nothing expands to nothing, and acme then says
+ // nothing (look.c:37, exec.c:141).
+ p.update(.{ .mouse = .{ .button = config.look_button, .kind = .press, .col = x0 + 60, .row = y0 + 1 } });
+ p.update(.{ .mouse = .{ .button = config.look_button, .kind = .release, .col = x0 + 60, .row = y0 + 1 } });
+ try testing.expectEqual(Status.again, call(p, .{ .tag = 2, .op = .read, .node = event, .handle = h, .size = 256 }).reply.status);
+ _ = call(p, .{ .tag = 3, .op = .release, .node = event, .handle = h });
+ }
+}