refactor(daemon): name tab chrome rows and derive headless viewports
- commit
- 8be46014f75c7fc067330700174a0717a93c1c8b
- author
- brice · · 2026-07-23 21:31 UTC
- parent
- cb1999a
- signature
- GPG Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· arch-gpg Verified for the committer email
2 files · +71 -1
M internal/usecase/daemon/session.go +28 -1
| @@ -457,14 +457,41 @@ func (tb *tab) closeAllPanes() { | |||
| 457 | 457 | } | |
| 458 | 458 | } | |
| 459 | 459 | ||
| 460 | + | // tabChromeRows is the vertical chrome (tab bar + status bar) between a | |
| 461 | + | // client viewport and a tab's content area. | |
| 462 | + | const tabChromeRows = 2 | |
| 463 | + | ||
| 460 | 464 | func tabSize(clientSize domain.Size) domain.Size { | |
| 461 | 465 | if !clientSize.Valid() { | |
| 462 | 466 | clientSize = defaultSize | |
| 463 | 467 | } | |
| 464 | - | rows := max(clientSize.Rows-2, 1) | |
| 468 | + | rows := max(clientSize.Rows-tabChromeRows, 1) | |
| 465 | 469 | return domain.Size{Cols: clientSize.Cols, Rows: rows} | |
| 466 | 470 | } | |
| 467 | 471 | ||
| 472 | + | // fullViewportSize derives a full client-equivalent viewport from the active | |
| 473 | + | // tab's retained content size, for actions that need a viewport while no | |
| 474 | + | // client is attached. It falls back to defaultSize when no valid size exists. | |
| 475 | + | func (s *session) fullViewportSize() domain.Size { | |
| 476 | + | s.mu.Lock() | |
| 477 | + | var tb *tab | |
| 478 | + | if s.active >= 0 && s.active < len(s.tabs) { | |
| 479 | + | tb = s.tabs[s.active] | |
| 480 | + | } | |
| 481 | + | s.mu.Unlock() | |
| 482 | + | ||
| 483 | + | var content domain.Size | |
| 484 | + | if tb != nil { | |
| 485 | + | tb.mu.Lock() | |
| 486 | + | content = tb.size | |
| 487 | + | tb.mu.Unlock() | |
| 488 | + | } | |
| 489 | + | if !content.Valid() { | |
| 490 | + | return defaultSize | |
| 491 | + | } | |
| 492 | + | return domain.Size{Cols: content.Cols, Rows: content.Rows + tabChromeRows} | |
| 493 | + | } | |
| 494 | + | ||
| 468 | 495 | func (d *Daemon) startTabGoroutines(sess *session, tb *tab) { | |
| 469 | 496 | tb.mu.Lock() | |
| 470 | 497 | panes := tb.panesSnapshot() | |
| @@ -457,14 +457,41 @@ func (tb *tab) closeAllPanes() { | |||||
| 457 | } | 457 | } | ||
| 458 | } | 458 | } | ||
| 459 | 459 | ||||
| 460 | + | // tabChromeRows is the vertical chrome (tab bar + status bar) between a | |||
| 461 | + | // client viewport and a tab's content area. | |||
| 462 | + | const tabChromeRows = 2 | |||
| 463 | + | ||||
| 460 | func tabSize(clientSize domain.Size) domain.Size { | 464 | func tabSize(clientSize domain.Size) domain.Size { | ||
| 461 | if !clientSize.Valid() { | 465 | if !clientSize.Valid() { | ||
| 462 | clientSize = defaultSize | 466 | clientSize = defaultSize | ||
| 463 | } | 467 | } | ||
| 464 | - | rows := max(clientSize.Rows-2, 1) | 468 | + | rows := max(clientSize.Rows-tabChromeRows, 1) |
| 465 | return domain.Size{Cols: clientSize.Cols, Rows: rows} | 469 | return domain.Size{Cols: clientSize.Cols, Rows: rows} | ||
| 466 | } | 470 | } | ||
| 467 | 471 | ||||
| 472 | + | // fullViewportSize derives a full client-equivalent viewport from the active | |||
| 473 | + | // tab's retained content size, for actions that need a viewport while no | |||
| 474 | + | // client is attached. It falls back to defaultSize when no valid size exists. | |||
| 475 | + | func (s *session) fullViewportSize() domain.Size { | |||
| 476 | + | s.mu.Lock() | |||
| 477 | + | var tb *tab | |||
| 478 | + | if s.active >= 0 && s.active < len(s.tabs) { | |||
| 479 | + | tb = s.tabs[s.active] | |||
| 480 | + | } | |||
| 481 | + | s.mu.Unlock() | |||
| 482 | + | ||||
| 483 | + | var content domain.Size | |||
| 484 | + | if tb != nil { | |||
| 485 | + | tb.mu.Lock() | |||
| 486 | + | content = tb.size | |||
| 487 | + | tb.mu.Unlock() | |||
| 488 | + | } | |||
| 489 | + | if !content.Valid() { | |||
| 490 | + | return defaultSize | |||
| 491 | + | } | |||
| 492 | + | return domain.Size{Cols: content.Cols, Rows: content.Rows + tabChromeRows} | |||
| 493 | + | } | |||
| 494 | + | ||||
| 468 | func (d *Daemon) startTabGoroutines(sess *session, tb *tab) { | 495 | func (d *Daemon) startTabGoroutines(sess *session, tb *tab) { | ||
| 469 | tb.mu.Lock() | 496 | tb.mu.Lock() | ||
| 470 | panes := tb.panesSnapshot() | 497 | panes := tb.panesSnapshot() | ||
A internal/usecase/daemon/session_size_test.go +43 -0
| @@ -0,0 +1,43 @@ | |||
| 1 | + | package daemon | |
| 2 | + | ||
| 3 | + | import ( | |
| 4 | + | "testing" | |
| 5 | + | ||
| 6 | + | "github.com/bnema/vev/internal/domain" | |
| 7 | + | ) | |
| 8 | + | ||
| 9 | + | func TestFullViewportSize(t *testing.T) { | |
| 10 | + | tests := []struct { | |
| 11 | + | name string | |
| 12 | + | sess *session | |
| 13 | + | want domain.Size | |
| 14 | + | }{ | |
| 15 | + | { | |
| 16 | + | name: "derives from active tab content size", | |
| 17 | + | sess: &session{tabs: []*tab{{size: domain.Size{Cols: 120, Rows: 38}}}, active: 0}, | |
| 18 | + | want: domain.Size{Cols: 120, Rows: 40}, | |
| 19 | + | }, | |
| 20 | + | { | |
| 21 | + | name: "round-trips through tabSize", | |
| 22 | + | sess: &session{tabs: []*tab{{size: tabSize(domain.Size{Cols: 80, Rows: 24})}}, active: 0}, | |
| 23 | + | want: domain.Size{Cols: 80, Rows: 24}, | |
| 24 | + | }, | |
| 25 | + | { | |
| 26 | + | name: "falls back to defaultSize with no tabs", | |
| 27 | + | sess: &session{}, | |
| 28 | + | want: defaultSize, | |
| 29 | + | }, | |
| 30 | + | { | |
| 31 | + | name: "falls back to defaultSize with invalid tab size", | |
| 32 | + | sess: &session{tabs: []*tab{{}}, active: 0}, | |
| 33 | + | want: defaultSize, | |
| 34 | + | }, | |
| 35 | + | } | |
| 36 | + | for _, tt := range tests { | |
| 37 | + | t.Run(tt.name, func(t *testing.T) { | |
| 38 | + | if got := tt.sess.fullViewportSize(); got != tt.want { | |
| 39 | + | t.Fatalf("fullViewportSize() = %+v, want %+v", got, tt.want) | |
| 40 | + | } | |
| 41 | + | }) | |
| 42 | + | } | |
| 43 | + | } | |
| @@ -0,0 +1,43 @@ | |||||
| 1 | + | package daemon | |||
| 2 | + | ||||
| 3 | + | import ( | |||
| 4 | + | "testing" | |||
| 5 | + | ||||
| 6 | + | "github.com/bnema/vev/internal/domain" | |||
| 7 | + | ) | |||
| 8 | + | ||||
| 9 | + | func TestFullViewportSize(t *testing.T) { | |||
| 10 | + | tests := []struct { | |||
| 11 | + | name string | |||
| 12 | + | sess *session | |||
| 13 | + | want domain.Size | |||
| 14 | + | }{ | |||
| 15 | + | { | |||
| 16 | + | name: "derives from active tab content size", | |||
| 17 | + | sess: &session{tabs: []*tab{{size: domain.Size{Cols: 120, Rows: 38}}}, active: 0}, | |||
| 18 | + | want: domain.Size{Cols: 120, Rows: 40}, | |||
| 19 | + | }, | |||
| 20 | + | { | |||
| 21 | + | name: "round-trips through tabSize", | |||
| 22 | + | sess: &session{tabs: []*tab{{size: tabSize(domain.Size{Cols: 80, Rows: 24})}}, active: 0}, | |||
| 23 | + | want: domain.Size{Cols: 80, Rows: 24}, | |||
| 24 | + | }, | |||
| 25 | + | { | |||
| 26 | + | name: "falls back to defaultSize with no tabs", | |||
| 27 | + | sess: &session{}, | |||
| 28 | + | want: defaultSize, | |||
| 29 | + | }, | |||
| 30 | + | { | |||
| 31 | + | name: "falls back to defaultSize with invalid tab size", | |||
| 32 | + | sess: &session{tabs: []*tab{{}}, active: 0}, | |||
| 33 | + | want: defaultSize, | |||
| 34 | + | }, | |||
| 35 | + | } | |||
| 36 | + | for _, tt := range tests { | |||
| 37 | + | t.Run(tt.name, func(t *testing.T) { | |||
| 38 | + | if got := tt.sess.fullViewportSize(); got != tt.want { | |||
| 39 | + | t.Fatalf("fullViewportSize() = %+v, want %+v", got, tt.want) | |||
| 40 | + | } | |||
| 41 | + | }) | |||
| 42 | + | } | |||
| 43 | + | } | |||