╭ changed ─╮ │src/ui.zig│ ╰──────────╯ ╭───────────╮ │← all files│ ╰───────────╯ ╭────────────────────────────────────────────────╮ │13 pub const Home = @import("./ui/Home.zig"); │ │14 pub const User = @import("./ui/User.zig"); │ │15 pub const Title = @import("./ui/Title.zig");│ │16 +pub const Quit = @import("./ui/Quit.zig"); │ │17 │ │18 pub const PageKind = enum { │ │19 home, │ ╰────────────────────────────────────────────────╯ ╭───────────────────────────────────────────────────────────────────────────────────────────────────────╮ │195 haxy_moment: ?evt.AdminDB.HashMap(.read_only) = null, // db cursor (null on the wasm side) │ │196 pending: std.ArrayList(Action) = .empty, // actions queued by widgets this frame │ │197 nav_back: bool = false, // set by input (escape) to request the native TUI pop a page; see Nav│ │198 + is_terminal: bool = false, // true on remote SSH and local TUI │ │199 + quit_requested: bool = false, │ │200 │ │201 const Self = @This(); │ │202 │ ╰───────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭───────────────────────────────────────────────────────────────────────────────────╮ │325 // local run has no repo to persist to, so just apply in-memory │ │326 session.applyPending(); │ │327 │ │325 - // reconcile navigation: forward to a new page, or back on escape (and│ │326 - // quit when there's nothing left to go back to). │ │327 - if (!try nav.sync(allocator, session)) terminal.requestQuit(); │ │328 + // reconcile navigation: forward to a new page, or back on escape. │ │329 + try nav.sync(allocator, session); │ │330 + │ │331 + // the quit button (on the quit tab) asks the host to tear down. │ │332 + if (session.quit_requested) terminal.requestQuit(); │ │333 │ │334 try nav.root.build(allocator, .{ │ │335 .min_size = .{ .width = null, .height = null }, │ ╰───────────────────────────────────────────────────────────────────────────────────╯ ╭──────────────────────────────────────────────────────────────────────────────────────────╮ │459 allocator.destroy(arena); │ │460 } │ │461 │ │457 - // reconcile the displayed root with the session's navigation state. returns │ │458 - // false when escape was pressed with no history left, so the caller can quit. │ │459 - // a forward nav (current_page moved to a different parent page) pushes the │ │460 - // current root and builds the new page in a fresh arena; a back request frees │ │461 - // the current page and restores the previous one. │ │462 - pub fn sync(self: *Nav, allocator: std.mem.Allocator, session: *Session) !bool { │ │462 + // reconcile the displayed root with the session's navigation state. a │ │463 + // forward nav (current_page moved to a different parent page) pushes the │ │464 + // current root and builds the new page in a fresh arena; a back request │ │465 + // frees the current page and restores the previous one. when escape is │ │466 + // pressed with no history left we switch to the quit tab instead of quitting. │ │467 + pub fn sync(self: *Nav, allocator: std.mem.Allocator, session: *Session) !void { │ │468 if (session.nav_back) { │ │469 session.nav_back = false; │ │465 - const entry = self.history.pop() orelse return false; │ │466 - self.root.deinit(allocator); │ │467 - freeArena(allocator, self.arena); │ │468 - self.root = entry.root; │ │469 - self.route = entry.route; │ │470 - self.arena = entry.arena; │ │471 - session.page_arena = entry.arena; │ │472 - session.data.current_page = entry.route; │ │473 - return true; │ │470 + if (self.history.pop()) |entry| { │ │471 + self.root.deinit(allocator); │ │472 + freeArena(allocator, self.arena); │ │473 + self.root = entry.root; │ │474 + self.route = entry.route; │ │475 + self.arena = entry.arena; │ │476 + session.page_arena = entry.arena; │ │477 + session.data.current_page = entry.route; │ │478 + return; │ │479 + } │ │480 + // nothing to go back to; switch to the quit confirmation │ │481 + const root_focus = self.root.getFocus(); │ │482 + var iter = root_focus.children.iterator(); │ │483 + while (iter.next()) |entry| { │ │484 + switch (entry.value_ptr.focus.kind) { │ │485 + .custom => |custom| if (std.mem.eql(u8, custom, Quit.tab_kind)) {│ │486 + try root_focus.setFocus(entry.key_ptr.*); │ │487 + // a Stack only builds its selected child, so the quit │ │488 + // button isn't in the focus tree until a build runs with │ │489 + // the quit tab selected. build once, then send a │ │490 + // synthetic arrow_down to drop focus from the tab onto │ │491 + // the button so it's ready to confirm. │ │492 + try self.root.build(allocator, .{ │ │493 + .min_size = .{ .width = null, .height = 40 }, │ │494 + .max_size = .{ .width = 80, .height = null }, │ │495 + }, root_focus); │ │496 + try self.root.input(allocator, .arrow_down, root_focus); │ │497 + break; │ │498 + }, │ │499 + else => {}, │ │500 + } │ │501 + } │ │502 + return; │ │503 } │ │504 │ │505 if (session.data.current_page.parent() != self.route.parent()) { │ ╰──────────────────────────────────────────────────────────────────────────────────────────╯ ╭─────────────────────────────────────────────────────────────────────────────╮ │477 - const moment = session.haxy_moment orelse return true; │ │506 + const moment = session.haxy_moment orelse return; │ │507 const arena = try allocator.create(std.heap.ArenaAllocator);│ │508 arena.* = std.heap.ArenaAllocator.init(allocator); │ │509 errdefer freeArena(allocator, arena); │ ╰─────────────────────────────────────────────────────────────────────────────╯ ╭────────────────────────────────────╮ │526 self.route = route;│ │527 self.arena = arena;│ │528 } │ │500 - return true; │ │529 } │ │530 }; │ │531 │ ╰────────────────────────────────────╯ ╭───────────────────────────────────────╮ │566 background: AnsiBackground, │ │567 home: Home.View, │ │568 user: User.View, │ │569 + quit: Quit.View, │ │570 title: Title.View, │ │571 home_header: Home.Header.View,│ │572 user_header: User.Header.View,│ ╰───────────────────────────────────────╯