Fix hooks.

This commit is contained in:
Greyson Parrelli
2026-08-12 18:50:16 -04:00
parent dca5ec0b7d
commit 4e0e400372
7 changed files with 471 additions and 126 deletions
+42
View File
@@ -81,12 +81,20 @@ closing: bool = false,
on_empty: *const fn (ctx: ?*anyopaque) void,
on_title: *const fn (ctx: ?*anyopaque) void,
on_status: *const fn (ctx: ?*anyopaque) void,
/// A pane in here just finished work. Separate from `on_status` because the
/// tab needs the *edge*, not the state: "something finished since you were
/// last here" cannot be recovered by looking at the panes afterwards, since
/// one that finished before your last visit looks identical to one that
/// finished after it.
on_finished: *const fn (ctx: ?*anyopaque) void,
ctx: ?*anyopaque = null,
pub const Callbacks = struct {
on_empty: *const fn (ctx: ?*anyopaque) void,
on_title: *const fn (ctx: ?*anyopaque) void,
on_status: *const fn (ctx: ?*anyopaque) void,
on_finished: *const fn (ctx: ?*anyopaque) void,
ctx: ?*anyopaque,
};
@@ -101,6 +109,7 @@ pub fn create(alloc: std.mem.Allocator, cbs: Callbacks) !*View {
.on_empty = cbs.on_empty,
.on_title = cbs.on_title,
.on_status = cbs.on_status,
.on_finished = cbs.on_finished,
.ctx = cbs.ctx,
};
@@ -313,6 +322,19 @@ pub fn setFocused(self: *View, pane: *Pane) void {
self.focused = pane;
pane.setActive(true);
self.on_title(self.ctx);
// Deliberately moving into a pane answers it, the same as typing would.
// The identity check above is what keeps this honest: work finishing in
// the pane you are already sitting in leaves the focus unchanged, so it
// stays flagged — which is the case the whole indicator exists for.
pane.markAnswered();
self.on_status(self.ctx);
}
pub fn paneFinished(self: *View, pane: *Pane) void {
_ = pane;
if (self.closing) return;
self.on_finished(self.ctx);
}
pub fn paneTitleChanged(self: *View, pane: *Pane) void {
@@ -347,6 +369,26 @@ pub fn status(self: *View) Status {
return worst;
}
/// Whether any pane here has finished work that hasn't been answered.
///
/// Any one pane is enough, and deliberately so: the view's own status can't
/// answer this, because a tab holding three agents never goes idle as a whole
/// until the last of them stops, and "one of them is ready for you" is worth
/// hearing before then.
pub fn anyDoneUnanswered(self: *View) bool {
for (self.panes.items) |pane| if (pane.done_unanswered) return true;
return false;
}
/// Answer the pane the user has landed in, without touching its siblings.
///
/// Called when a tab is opened: the pane you arrive in is the one you are
/// looking at, while the others in a split are still holding news you have not
/// gone to yet, and their dots are the only thing that says which.
pub fn answerFocused(self: *View) void {
if (self.focusedPane()) |pane| pane.markAnswered();
}
// -------------------------------------------------------------------------
// Layouts