diff options
| -rw-r--r-- | docs/detached.md | 5 | ||||
| -rw-r--r-- | src/Messages.zig | 50 | ||||
| -rw-r--r-- | src/detached/server.zig | 2 | ||||
| -rw-r--r-- | src/panes.zig | 2 | ||||
| -rw-r--r-- | src/pardes.zig | 5 |
5 files changed, 64 insertions, 0 deletions
diff --git a/docs/detached.md b/docs/detached.md index 2111e3dc..17c225f2 100644 --- a/docs/detached.md +++ b/docs/detached.md @@ -17,6 +17,11 @@ Inside an editor, `Attach work` (`SPC s a`) switches the current window to that session. `Detach` (`SPC s D`) closes only that frontend. It does not turn a local editor into a detached session. +A message stays until the next key or click dismisses it. With no frontend +attached there is none to come, so a detached session drops each pane's +messages once the newest has been up `MessageLinger` on the clock, as the +editor is stepped or served over 9P; /screen then shows what is current. + ## Files and transport The frontend socket is `pardes-detached-<name>.sock` under diff --git a/src/Messages.zig b/src/Messages.zig index 72b3b63c..0652f974 100644 --- a/src/Messages.zig +++ b/src/Messages.zig @@ -184,6 +184,7 @@ fn showMessage(p: *Pardes, id: usize, text: []const u8, stack: bool) void { 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; pane.msg_len = @intCast(n); + pane.msg_said_ms = nowMs(); p.needs_frame = true; if (arrives) { pane.msg_life = .{ .phase = if (p.settings.message_animation and n > 0) .entering else .shown }; @@ -197,6 +198,33 @@ fn showMessage(p: *Pardes, id: usize, text: []const u8, stack: bool) void { } } +fn nowMs() i64 { + if (comptime !pardes.hosted) return 0; + var ts: std.c.timespec = undefined; + if (std.c.clock_gettime(.MONOTONIC, &ts) != 0) return 0; + return @as(i64, ts.sec) * 1000 + @divTrunc(@as(i64, ts.nsec), 1_000_000); +} + +/// With no frontend attached no input dismisses a message, and a detached +/// session's /screen and messages kept what was said an hour ago: each +/// pane's lines go once its newest has been up `message_linger_ms`, on the +/// clock, checked as the core is stepped or served. +/// ponytail: a pane's older lines go with its newest, not each on its own +/// clock; the render pipeline's clock will give every line its own. +pub fn expireUnattended(p: *Pardes) void { + if (!p.unattended) return; + const now = nowMs(); + for (p.panes) |slot| { + const pane = slot orelse continue; + if (pane.msg_len == 0 and pane.msg_older_len == 0) continue; + if (now - pane.msg_said_ms < p.settings.message_linger_ms) continue; + pane.msg_len = 0; + pane.msg_life = .{}; + pane.msg_older_len = 0; + p.needs_frame = true; + } +} + /// The key or click that used to clear the message row now starts each /// line's exit: it stays `message_linger_ms`, then dissolves. A line /// already on its way out keeps its own timing. @@ -529,6 +557,28 @@ test "Msg writes the transient row by hand, bare or with text, and input ends it try std.testing.expectEqual(p.panes[2].?.msg.len, row(p, 2).len); } +test "with no frontend attached a message goes once it has lingered, on the clock" { + const p = try Pardes.init(std.testing.allocator, .{ .tty_only = true }); + defer p.deinit(); + const pane = p.panes[p.active].?; + p.setMessage(p.active, "said"); + p.setMessage(p.active, "said again"); + // Attached: it waits for input, however long. + pane.msg_said_ms -= 60_000; + p.update(.tick); + try std.testing.expect(pane.msg_len > 0); + // Detached with nobody attached: past its linger it is gone, and the + // line above it with it. + p.unattended = true; + _ = p.serveFs(.{ .tag = 1, .op = .read, .node = @intFromEnum(ctlfs.TopFile.index), .size = 64 }); + try std.testing.expectEqual(@as(u16, 0), pane.msg_len); + try std.testing.expectEqual(@as(u8, 0), pane.msg_older_len); + // A fresh one stays until its linger is up. + p.setMessage(p.active, "fresh"); + p.update(.tick); + try std.testing.expect(pane.msg_len > 0); +} + test "a message falls in, lingers past the input that dismisses it, and dissolves" { const p = try Pardes.init(std.testing.allocator, .{ .tty_only = true, .cols = 80, .rows = 24 }); defer p.deinit(); diff --git a/src/detached/server.zig b/src/detached/server.zig index b088d833..6a4b5001 100644 --- a/src/detached/server.zig +++ b/src/detached/server.zig @@ -770,6 +770,8 @@ pub const Session = struct { const completed = s.ninep_wake.swap(false, .acq_rel) or drained; for (&s.clients) |*c| if (c.fd >= 0) s.flush(c); const regridded = s.reconcile(); + // Nobody attached, nobody to dismiss a message (expireUnattended). + s.core.unattended = s.primary() == null; const now = monotonicMs(); var fds: [poll_slots]libc.pollfd = undefined; diff --git a/src/panes.zig b/src/panes.zig index d4b336d7..888465f7 100644 --- a/src/panes.zig +++ b/src/panes.zig @@ -168,6 +168,8 @@ pub const Pane = struct { msg_len: u16 = 0, /// Where the newest message line is in its life. msg_life: MessageLife = .{}, + /// When it was said, in monotonic ms (Messages.expireUnattended). + msg_said_ms: i64 = 0, /// The row (among the notice rows) the newest line holds for its life. msg_slot: u8 = 0, /// Earlier message lines still on screen, oldest first, each running its diff --git a/src/pardes.zig b/src/pardes.zig index c049fa39..2eab3d62 100644 --- a/src/pardes.zig +++ b/src/pardes.zig @@ -3806,6 +3806,9 @@ pub const Pardes = struct { /// it is dragged instead of once on release. native_image_patches: bool = false, quit: bool = false, + /// A detached session with no frontend attached: nothing renders, so + /// no input ever dismisses a message (Messages.expireUnattended). + unattended: bool = false, haptic: HapticSlot = if (platform == .macos) .none else {}, drag: mouse.Drag = .none, hover_col: u16 = 0, @@ -4672,6 +4675,7 @@ pub const Pardes = struct { /// that changes a pane costs a frame, which is why a round trip on a /// local socket costs microseconds and not a vsync. pub fn serveFs(p: *Pardes, req: ctlfs.Req) ctlfs.Reply { + Messages.expireUnattended(p); if (!ctlfs.changesPane(req)) return ctlfs.handle(p, req); p.needs_frame = true; p.raw_hover_intent = false; @@ -4694,6 +4698,7 @@ pub const Pardes = struct { // the screen. p.needs_frame = p.needs_frame or ev != .tick; p.shell_rows.sweep(p.gpa); + Messages.expireUnattended(p); // An announcement stands in only until its own step says more. for (p.panes) |slot| if (slot) |pane| { pane.msg_announcement = false; |
