diff --git a/src/tui.zig b/src/tui.zig index 6f679b3..9580b14 100644 --- a/src/tui.zig +++ b/src/tui.zig @@ -484,6 +484,16 @@ pub const App = struct { status_msg: [256]u8 = undefined, status_len: usize = 0, + /// True between the refresh keypress and the deferred refresh + /// work completing. While set, the status bar shows the + /// in-progress indicator; the poll tick is armed so the + /// (blocking) refresh runs one frame later, letting the + /// indicator paint first (vxfw draws after the handler returns). + refresh_pending: bool = false, + /// Wall-clock seconds when the last manual refresh completed + /// (0 = never). Drives the "refreshed Xs ago" status readout. + last_refresh_s: i64 = 0, + // Input mode state mode: InputMode = .normal, // SAFETY: paired with `input_len`; only the prefix @@ -536,7 +546,7 @@ pub const App = struct { // picked up automatically because dispatch targets the // active tab. 100ms cadence — fast enough to feel // responsive, slow enough to be invisible in CPU terms. - defer if (!self.poll_tick_armed and self.dispatchBool("wantsPollTick", .{})) { + defer if (!self.poll_tick_armed and (self.refresh_pending or self.dispatchBool("wantsPollTick", .{}))) { if (ctx.tick(100, self.widget())) { self.poll_tick_armed = true; } else |err| { @@ -583,12 +593,21 @@ pub const App = struct { self.loadTabData(); }, .tick => { - // Our scheduled poll timer fired. Mark it - // un-armed (the defer above re-arms if the active - // tab still wants polling) and request a redraw - // so the draw pass runs the tab's tick hook. + // Our scheduled timer fired. Mark it un-armed (the + // defer above re-arms if there's more work). self.poll_tick_armed = false; - if (self.dispatchBool("wantsPollTick", .{})) { + if (self.refresh_pending) { + // Phase 2 of refresh: the indicator painted last + // frame; now run the (blocking) refresh. + self.refresh_pending = false; + self.refreshCurrentTab(); + // wall-clock required: timestamp the completed + // refresh for the "refreshed Xs ago" readout. + self.last_refresh_s = std.Io.Timestamp.now(self.io, .real).toSeconds(); + ctx.redraw = true; + } else if (self.dispatchBool("wantsPollTick", .{})) { + // Poll-driven redraw so the draw pass runs the + // active tab's tick hook. ctx.redraw = true; } }, @@ -1004,7 +1023,11 @@ pub const App = struct { return ctx.consumeAndRedraw(); }, .refresh => { - self.refreshCurrentTab(); + // Two-phase so the "Refreshing..." indicator paints + // before the (blocking) refresh runs. Flag it and + // redraw now; the armed poll tick runs the actual + // refresh one frame later (see the `.tick` branch). + self.refresh_pending = true; return ctx.consumeAndRedraw(); }, .prev_tab => { @@ -1299,15 +1322,29 @@ pub const App = struct { }; } - /// Returns the current status message. When no message has been - /// set, builds a dynamic default hint composed from a small set - /// of always-shown global keys plus the active tab's - /// `status_hints`. Allocated in `arena` for the dynamic default; - /// the user-set buffer is returned by reference. + /// Returns the current status message. While a refresh is in + /// flight, shows the in-progress indicator. Otherwise: a + /// user-set message if present, else a dynamic default hint + /// (global keys + the active tab's `status_hints`), prefixed + /// with a "refreshed Xs ago" readout once a refresh has run. + /// Allocated in `arena` for the dynamic forms; the user-set + /// buffer is returned by reference. fn getStatus(self: *App, arena: std.mem.Allocator) []const u8 { + if (self.refresh_pending) return "Refreshing..."; if (self.status_len > 0) return self.status_msg[0..self.status_len]; - return self.buildDefaultStatusHint(arena) catch + const hint = self.buildDefaultStatusHint(arena) catch "h/l tabs | j/k select | / symbol | ? help"; + if (self.last_refresh_s > 0) { + // wall-clock required: per-frame "now" for the relative + // "refreshed Xs ago" readout. + const now_s = std.Io.Timestamp.now(self.io, .real).toSeconds(); + var ago_buf: [24]u8 = undefined; + const ago = fmt.fmtTimeAgo(&ago_buf, self.last_refresh_s, now_s); + if (ago.len > 0) { + return std.fmt.allocPrint(arena, "refreshed {s} | {s}", .{ ago, hint }) catch hint; + } + } + return hint; } /// Build the dynamic default status hint: a small set of always-