summaryrefslogtreecommitdiff
path: root/src/tty
diff options
context:
space:
mode:
Diffstat (limited to 'src/tty')
-rw-r--r--src/tty/tty.zig148
1 files changed, 126 insertions, 22 deletions
diff --git a/src/tty/tty.zig b/src/tty/tty.zig
index 03ccd3e2..5976852f 100644
--- a/src/tty/tty.zig
+++ b/src/tty/tty.zig
@@ -21,6 +21,56 @@ extern "c" fn setenv(name: [*:0]const u8, value: [*:0]const u8, overwrite: c_int
const TIOCSWINSZ: c_int = @bitCast(@as(u32, if (@hasDecl(posix.T, "IOCSWINSZ")) posix.T.IOCSWINSZ else 0x80087467));
+/// The terminal answered the shared-memory probe (`kittyShmProbe`): it can
+/// read a raster from a POSIX shm object, so a page costs a name on the wire
+/// instead of its whole base64 body. Set by the input reader, which sees the
+/// reply before vaxis' parser swallows every graphics APC alike.
+var kitty_shm = std.atomic.Value(bool).init(false);
+/// Only a reply that arrives while the probe is out counts. The id is one
+/// vaxis' own image numbering (1, 2, ...) never reaches, because a terminal
+/// answers every transmission `i=<id>;OK` and one of those must not pass.
+var kitty_shm_probing = std.atomic.Value(bool).init(false);
+const kitty_shm_probe_id = 0x7fff_fff1;
+var kitty_shm_serial: u32 = 0;
+
+fn kittyShmName(buf: []u8, n: u32) ![:0]const u8 {
+ return std.fmt.bufPrintSentinel(buf, "/pardes-{d}-{d}", .{ libc.getpid(), n }, 0);
+}
+
+/// Write `bytes` to a fresh shm object; the terminal unlinks it after reading.
+fn kittyShmWrite(name: [:0]const u8, bytes: []const u8) bool {
+ const fd = libc.shm_open(name, @bitCast(libc.O{ .ACCMODE = .RDWR, .CREAT = true, .EXCL = true }), @as(c_uint, 0o600));
+ if (fd < 0) return false;
+ defer _ = libc.close(fd);
+ if (libc.ftruncate(fd, @intCast(bytes.len)) != 0) {
+ _ = libc.shm_unlink(name);
+ return false;
+ }
+ var off: usize = 0;
+ while (off < bytes.len) {
+ const n = libc.write(fd, bytes[off..].ptr, bytes.len - off);
+ if (n <= 0) {
+ _ = libc.shm_unlink(name);
+ return false;
+ }
+ off += @intCast(n);
+ }
+ return true;
+}
+
+/// Ask with a 1x1 RGB image in shm whether the terminal reads that medium. A
+/// remote terminal cannot see our shm and answers an error (or nothing).
+fn kittyShmProbe(w: *std.Io.Writer) void {
+ var buf: [64]u8 = undefined;
+ const name = kittyShmName(&buf, 0) catch return;
+ if (!kittyShmWrite(name, &.{ 0, 0, 0 })) return;
+ var b64: [64]u8 = undefined;
+ const enc = std.base64.standard.Encoder.encode(&b64, name);
+ kitty_shm_probing.store(true, .release);
+ w.print("\x1b_Gi={d},s=1,v=1,a=q,t=s,f=24;{s}\x1b\\", .{ kitty_shm_probe_id, enc }) catch return;
+ w.flush() catch {};
+}
+
pub const Command = struct {
pub var value: union(enum) {
nop,
@@ -81,6 +131,13 @@ fn readInput(loop: *Loop, tty: anytype, cache: *vaxis.GraphemeCache) !void {
const received = try tty.read(buf[carried..]);
if (received == 0) return;
const end = carried + received;
+ if (kitty_shm_probing.load(.acquire)) if (std.mem.indexOf(u8, buf[0..end], std.fmt.comptimePrint("\x1b_Gi={d};", .{kitty_shm_probe_id}))) |at| {
+ kitty_shm_probing.store(false, .release);
+ kitty_shm.store(std.mem.startsWith(u8, buf[at..end], std.fmt.comptimePrint("\x1b_Gi={d};OK", .{kitty_shm_probe_id})), .release);
+ // Answered either way: a terminal that could not read it left it.
+ var name_buf: [64]u8 = undefined;
+ if (kittyShmName(&name_buf, 0)) |name| _ = libc.shm_unlink(name) else |_| {}
+ };
var parse_end = end;
var lead = end;
while (lead > 0 and buf[lead - 1] & 0xc0 == 0x80) lead -= 1;
@@ -717,6 +774,7 @@ fn localSession(
try startInput(loop, input_cache);
defer if (attached.* == null) stopInput(loop);
(try std.Thread.spawn(.{}, winchWatch, .{ loop, vx, tty })).detach();
+ (try std.Thread.spawn(.{}, tickWatch, .{ io, loop })).detach();
if (fs) |f| try f.watch(loop, wakeFs);
try vx.queryTerminalSend(tty.writer());
@@ -726,11 +784,20 @@ fn localSession(
};
if (sh.inotify_fd >= 0) sh.watch_task = io.concurrent(watchFiles, .{ io, sh.inotify_fd, loop }) catch null;
+ // A terminal unlinks what it reads; whatever it never read (the probe
+ // unanswered, a transmission lost with the terminal) is ours to remove.
+ defer {
+ var name_buf: [64]u8 = undefined;
+ var n: u32 = 0;
+ while (n <= kitty_shm_serial) : (n += 1) {
+ if (kittyShmName(&name_buf, n)) |name| _ = libc.shm_unlink(name) else |_| {}
+ }
+ }
frames: while (!core.quit) {
pardes.turn.restoreSettled();
try core.pump(host);
if (core.takeRestore()) |rp| blk: {
- const bytes = filesystem.readRestore(gpa, rp) catch |err| {
+ const bytes = filesystem.readRestore(gpa, rp, core.settings.dump_dir.get()) catch |err| {
core.reportError(core.active, "Restore", err);
break :blk;
};
@@ -872,29 +939,39 @@ const Shell = struct {
fn waitInput(ctx: ?*anyopaque, timeout_ms: u32) void {
const s = of(ctx);
var batch: usize = 0;
- // The wait is the 9P connections' turn with the core.
- if (timeout_ms == 0) {
- pardes.turn.rest();
- const first = s.loop.nextEvent() catch {
- pardes.turn.wake();
- s.core.quit = true;
- return s.reloadWatched();
- };
+ // The wait is the 9P connections' turn with the core. Animating, the
+ // frame's tick arrives through the same queue (`tickWatch`).
+ if (timeout_ms != 0) tick_request.set(s.io);
+ pardes.turn.rest();
+ const first = s.loop.nextEvent() catch {
pardes.turn.wake();
- batch = 1;
- if (s.apply(first)) return s.reloadWatched();
- } else {
- pardes.turn.rest();
- std.Io.sleep(s.io, .fromMilliseconds(timeout_ms), .awake) catch {};
- pardes.turn.wake();
- _ = s.apply(.tick);
- batch = 1;
- }
- while (s.in_paste or batch < 64) {
- const ev = (s.loop.tryEvent() catch null) orelse break;
+ s.core.quit = true;
+ return s.reloadWatched();
+ };
+ pardes.turn.wake();
+ batch = 1;
+ // A run of queued motions is one move to its last cell: each would
+ // otherwise redo hover and selection work the next one undoes.
+ var motion: ?@TypeOf(Command.value) = null;
+ var ev = first;
+ while (true) {
+ if (ev == .mouse and (ev.mouse.type == .motion or ev.mouse.type == .drag)) {
+ if (motion) |m| if (m.mouse.button != ev.mouse.button or m.mouse.type != ev.mouse.type or
+ @as(u3, @bitCast(m.mouse.mods)) != @as(u3, @bitCast(ev.mouse.mods)))
+ {
+ _ = s.apply(m);
+ };
+ motion = ev;
+ } else {
+ if (motion) |m| _ = s.apply(m);
+ motion = null;
+ if (s.apply(ev)) break;
+ }
+ if (!s.in_paste and batch >= 64) break;
+ ev = (s.loop.tryEvent() catch null) orelse break;
batch += 1;
- if (s.apply(ev)) break;
}
+ if (motion) |m| _ = s.apply(m);
s.reloadWatched();
}
@@ -1001,6 +1078,7 @@ const Shell = struct {
fn pollFrame(ctx: ?*anyopaque) void {
const s = of(ctx);
+ s.core.native_image_patches = kitty_shm.load(.acquire);
if (s.fs) |f| if (f.tick().pending) {
_ = s.loop.tryPostEvent(.fs_ready) catch {};
};
@@ -1013,6 +1091,7 @@ const Shell = struct {
s.caps_pending = false;
s.vx.enableDetectedFeatures(s.tty.writer()) catch {};
s.core.native_images = s.vx.caps.kitty_graphics;
+ if (s.vx.caps.kitty_graphics) kittyShmProbe(s.tty.writer());
s.vx.queueRefresh();
}
}
@@ -1039,6 +1118,16 @@ const Shell = struct {
if (!kittyImageRepresentable(place)) continue;
}
const key = place.cacheKey();
+ if (!s.kitty.contains(key) and vx.caps.kitty_graphics and kitty_shm.load(.acquire)) shm: {
+ var buf: [64]u8 = undefined;
+ kitty_shm_serial +%= 1;
+ const name = kittyShmName(&buf, kitty_shm_serial) catch break :shm;
+ if (!kittyShmWrite(name, place.rgba)) break :shm;
+ if (vx.transmitLocalImagePath(s.gpa, s.tty.writer(), name, @intCast(place.iw), @intCast(place.ih), .shared_mem, .rgba) catch null) |handle|
+ s.kitty.put(key, handle) catch vx.freeImage(s.tty.writer(), handle.id)
+ else
+ _ = libc.shm_unlink(name);
+ }
if (!s.kitty.contains(key) and vx.caps.kitty_graphics) {
const enc = std.base64.standard.Encoder;
if (s.gpa.alloc(u8, enc.calcSize(place.rgba.len))) |b64| {
@@ -1160,7 +1249,7 @@ const Shell = struct {
fn writeDump(ctx: ?*anyopaque, bytes: []const u8) void {
const s = of(ctx);
var pbuf: [1024:0]u8 = undefined;
- const path = pardes.dump.outPath(&pbuf) orelse return;
+ const path = pardes.dump.outPath(&pbuf, s.core.settings.dump_dir.get()) orelse return;
filesystem.write(s.core, path, bytes) catch |err| return s.core.reportError(0, "dump", err);
s.core.setLastDump(path);
}
@@ -1415,6 +1504,21 @@ fn winchWatch(loop: *Loop, vx: *vaxis.Vaxis, tty: *vaxis.Tty) void {
}
}
+/// Set by an animating frame's wait: post one `.tick` a frame from now. The
+/// tick joins the input queue, so a key or a click that lands inside the
+/// frame is handled at once instead of after a blind sleep.
+var tick_request: std.Io.Event = .unset;
+
+fn tickWatch(io: std.Io, loop: *Loop) void {
+ while (true) {
+ tick_request.waitUncancelable(io);
+ tick_request.reset();
+ var ts: libc.timespec = .{ .sec = 0, .nsec = @as(isize, pardes.layout.Animation.frame_ms) * std.time.ns_per_ms };
+ _ = libc.nanosleep(&ts, null);
+ loop.postEvent(.tick) catch {};
+ }
+}
+
fn wakeFs(ctx: ?*anyopaque) void {
const loop: *Loop = @ptrCast(@alignCast(ctx.?));
_ = loop.tryPostEvent(.fs_ready) catch {};