From d4d4d7c6de2465bf838c8f62ad5062910ce5b4d3 Mon Sep 17 00:00:00 2001 From: Kyle Carberry Date: Thu, 11 Jun 2026 06:19:15 +0000 Subject: [PATCH] refactor: drop the new-session button from the ui sidebar The session list now starts on the top sidebar row; C-a c (and boo new) remain the ways to create a session. Removing the button and its gap row frees two list rows and the new_button hit region, and click coordinates shift up accordingly. --- README.md | 6 ++--- src/help.zig | 1 - src/ui.zig | 53 ++++++++++++-------------------------------- test/integration.zig | 38 +++++++++++++------------------ 4 files changed, 33 insertions(+), 65 deletions(-) diff --git a/README.md b/README.md index 13f51f9..3ba6909 100644 --- a/README.md +++ b/README.md @@ -17,9 +17,9 @@ exactly as a human would see it. `boo attach`. - A full-screen session manager: `boo ui` lists sessions in a sidebar with their titles, and renders the focused one next to it. Click to - switch, create, kill, or rename sessions; drag to select and copy - text (OSC 52); scroll the wheel to page through a session's history; - everything also works from the keyboard. + switch or kill sessions; drag to select and copy text (OSC 52); + scroll the wheel to page through a session's history; create, + rename, and everything else works from the keyboard. - One command per session, named after your current directory by default. Sessions are cheap; run one per task. - Faithful redraws from libghostty terminal state, including SGR styles, diff --git a/src/help.zig b/src/help.zig index 6c2ba1f..4abcd30 100644 --- a/src/help.zig +++ b/src/help.zig @@ -115,7 +115,6 @@ pub const commands = [_]Entry{ \\mouse: \\ click a session focus it (steals politely, like attach) \\ click its 'x' kill it (asks for confirmation) - \\ click + new session start a session running $SHELL \\ scroll the sidebar scroll the session list \\ wheel in viewport scroll the session's history; wheel \\ back down or press esc to return to diff --git a/src/ui.zig b/src/ui.zig index b2c19a9..2285e56 100644 --- a/src/ui.zig +++ b/src/ui.zig @@ -65,10 +65,6 @@ pub const Layout = struct { /// Each session occupies two sidebar rows: name and title. pub const entry_rows: u16 = 2; - /// Sidebar rows above the session list: the new-session button - /// and a separating blank row. - pub const list_top: u16 = 2; - pub fn init(rows: u16, cols: u16) Layout { // Narrow terminals get a proportionally smaller sidebar; the // viewport keeps at least a sliver so the focused session @@ -92,11 +88,10 @@ pub const Layout = struct { return self.sidebar_w + 1; } - /// Sidebar rows available for session entries between the - /// new-session button (plus its gap row) and the keybind hint - /// on the bottom row. + /// Sidebar rows available for session entries: everything above + /// the keybind hint on the bottom row. pub fn listRows(self: Layout) u16 { - return self.rows -| (list_top + 1); + return self.rows -| 1; } /// Whole session entries that fit in the list area. @@ -108,7 +103,6 @@ pub const Layout = struct { /// Display row within the visible session list (entry_rows /// rows per session; scroll applied by the caller). session: struct { row: u16, kill: bool }, - new_button, viewport: struct { x: u16, y: u16 }, none, }; @@ -122,10 +116,8 @@ pub const Layout = struct { } if (x >= self.sidebar_w) return .none; // separator column if (y == self.rows -| 1) return .none; // keybind hint row - if (y == 0) return .new_button; - if (y < list_top) return .none; // gap under the button return .{ .session = .{ - .row = y - list_top, + .row = y, .kill = self.sidebar_w >= 12 and x == self.sidebar_w - 2, } }; } @@ -1249,10 +1241,6 @@ const Ui = struct { } self.focusIndex(idx); }, - .new_button => { - if (m.release or m.isMotion()) return; - self.createSession(); - }, else => {}, } } @@ -2377,24 +2365,11 @@ const Ui = struct { try out.appendSlice(alloc, sgr_reset); return; } - if (y == 0) { - try out.appendSlice(alloc, style_dim); - try appendClipped(alloc, out, " + new session", w); - try out.appendSlice(alloc, sgr_reset); - return; - } - if (y < Layout.list_top) { - // Blank gap between the button and the session list. - try appendClipped(alloc, out, "", w); - return; - } - - const row = y - Layout.list_top; - const idx = self.scroll + row / Layout.entry_rows; + const idx = self.scroll + y / Layout.entry_rows; if (idx < self.sessions.items.len) { const entry = self.sessions.items[idx]; const selected = self.selected != null and self.selected.? == idx; - if (row % Layout.entry_rows == 0) { + if (y % Layout.entry_rows == 0) { try appendSessionRow(alloc, out, entry, w, selected); } else { try appendSessionTitleRow(alloc, out, entry, w, selected); @@ -2885,13 +2860,13 @@ test "layout: geometry and hit testing" { try std.testing.expectEqual(@as(u16, 75), l.viewportCols()); try std.testing.expectEqual(@as(u16, 25), l.viewportX()); try std.testing.expectEqual(@as(u16, 24), l.viewportRows()); - try std.testing.expectEqual(@as(usize, 10), l.visibleEntries()); + try std.testing.expectEqual(@as(usize, 11), l.visibleEntries()); - // The new-session button is the top row and a blank gap sits - // under it. The bottom sidebar row holds the keybind hint, and - // the viewport extends through the last row. - try std.testing.expectEqual(Layout.Hit.new_button, l.hit(3, 0)); - try std.testing.expectEqual(Layout.Hit.none, l.hit(3, 1)); + // The session list starts on the top row. The bottom sidebar row + // holds the keybind hint, and the viewport extends through the + // last row. + try std.testing.expectEqual(@as(u16, 0), l.hit(3, 0).session.row); + try std.testing.expectEqual(@as(u16, 1), l.hit(3, 1).session.row); try std.testing.expectEqual(Layout.Hit.none, l.hit(3, 23)); const bottom = l.hit(80, 23); try std.testing.expectEqual(@as(u16, 55), bottom.viewport.x); @@ -2901,10 +2876,10 @@ test "layout: geometry and hit testing" { // Sessions take two display rows: name, then title. const s = l.hit(3, 5); - try std.testing.expectEqual(@as(u16, 3), s.session.row); + try std.testing.expectEqual(@as(u16, 5), s.session.row); try std.testing.expect(!s.session.kill); const k = l.hit(22, 4); - try std.testing.expectEqual(@as(u16, 2), k.session.row); + try std.testing.expectEqual(@as(u16, 4), k.session.row); try std.testing.expect(k.session.kill); const v = l.hit(30, 7); diff --git a/test/integration.zig b/test/integration.zig index 4539a92..939402e 100644 --- a/test/integration.zig +++ b/test/integration.zig @@ -1150,7 +1150,6 @@ test "ui: sidebar lists sessions and the focused session renders in the viewport defer ui.deinit(); try ui.waitFor("aa"); try ui.waitFor("bb"); - try ui.waitFor("+ new session"); try ui.waitFor("BB-VIEW-MARK"); // The UI renders on the alternate screen, like attach. @@ -1174,7 +1173,7 @@ test "ui: dragging in the viewport selects text and copies it via osc 52" { var ui = try PtyClient.spawn(&h, &.{"ui"}, 24, 100); defer ui.deinit(); - try ui.waitFor("+ new session"); + try ui.waitFor("cp"); // The echoed line lands on the session's first row, rendered at // screen row 1 starting at column 26 (24-column sidebar plus the @@ -1229,7 +1228,7 @@ test "ui: a row touching the viewport's right edge keeps its last cell" { var ui = try PtyClient.spawn(&h, &.{"ui"}, 24, 100); defer ui.deinit(); - try ui.waitFor("+ new session"); + try ui.waitFor("edge"); // Paint a marker whose final cell sits in the session's last // column (75 wide inside a 100-column UI), which lands in the @@ -1252,12 +1251,8 @@ test "ui: a row touching the viewport's right edge keeps its last cell" { defer alloc.free(screen); try std.testing.expect(std.mem.indexOf(u8, screen, "EDGEZ") != null); - // The sidebar separates the new-session button from the first - // session row with a blank gap row. + // The session list starts on the first sidebar row. var lines = std.mem.splitScalar(u8, screen, '\n'); - _ = lines.next(); // button row - const gap = lines.next().?; - try std.testing.expect(std.mem.startsWith(u8, gap, " " ** 24 ++ "\u{2502}")); const first = lines.next().?; try std.testing.expect(std.mem.indexOf(u8, first, "edge") != null); } @@ -1301,11 +1296,11 @@ test "ui: clicking a session in the sidebar focuses it" { defer ui.deinit(); try ui.waitFor("TWO-MARK"); // most recent session focused - // Sessions are sorted by name: "one" on sidebar row 3 (1-based, - // under the button and its gap row). An SGR press + release on - // that row switches the viewport. + // Sessions are sorted by name: "one" on sidebar row 1 (1-based, + // top of the list). An SGR press + release on that row switches + // the viewport. ui.clearOutput(); - try ui.send("\x1b[<0;5;3M\x1b[<0;5;3m"); + try ui.send("\x1b[<0;5;1M\x1b[<0;5;1m"); try ui.waitFor("ONE-MARK"); } @@ -1321,10 +1316,9 @@ test "ui: create and kill sessions from the ui" { defer ui.deinit(); try ui.waitFor("keep2"); - // Clicking '+ new session' (the top sidebar row) creates a - // session (named after the cwd or the creating pid) and focuses - // it. - try ui.send("\x1b[<0;5;1M\x1b[<0;5;1m"); + // C-a c creates a session (named after the cwd or the creating + // pid) and focuses it. + try ui.send("\x01c"); try waitUiSessionCount(&h, 3); // C-a k asks for confirmation, then kills the focused (new) @@ -1354,8 +1348,8 @@ test "ui: clicking the kill target asks for confirmation" { try ui.waitFor("victim"); // The kill target is the 'x' in the second-to-last sidebar - // column (sidebar width 24 -> 1-based column 23), row 3. - try ui.send("\x1b[<0;23;3M\x1b[<0;23;3m"); + // column (sidebar width 24 -> 1-based column 23), row 1. + try ui.send("\x1b[<0;23;1M\x1b[<0;23;1m"); try ui.waitFor("kill victim? y/n"); try ui.send("y"); try waitUiSessionCount(&h, 0); @@ -1374,9 +1368,9 @@ test "ui: killing the focused session moves focus to the next one" { defer ui.deinit(); try ui.waitFor("stay"); - // Focus "doomed" (first alphabetically, sidebar row 3) so its + // Focus "doomed" (first alphabetically, sidebar row 1) so its // death has somewhere to fall back to. - try ui.send("\x1b[<0;5;3M\x1b[<0;5;3m"); + try ui.send("\x1b[<0;5;1M\x1b[<0;5;1m"); try h.sendLine("doomed", "DOOM-MARK"); try ui.waitFor("DOOM-MARK"); @@ -1517,7 +1511,7 @@ test "ui: a plain attach steals the focused session" { try ui.waitFor("attached elsewhere"); // Clicking the session in the sidebar steals it back. - try ui.send("\x1b[<0;5;3M\x1b[<0;5;3m"); + try ui.send("\x1b[<0;5;1M\x1b[<0;5;1m"); try thief.waitFor("attached elsewhere"); _ = try thief.waitExit(); } @@ -1931,7 +1925,7 @@ test "ui: wheel scrolls primary-screen scrollback" { var ui = try PtyClient.spawn(&h, &.{"ui"}, 24, 100); defer ui.deinit(); - try ui.waitFor("+ new session"); + try ui.waitFor("scrolly"); // Stream enough lines through cat that the earliest ones scroll // off the 24-row screen into the view's scrollback.