summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--docs/detached.md5
-rw-r--r--src/Messages.zig50
-rw-r--r--src/detached/server.zig2
-rw-r--r--src/panes.zig2
-rw-r--r--src/pardes.zig5
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;