detached 8a8a4e5

refactor(controller): remove redundant tab_controller.go in favor of enhanced TabCoordinator

- Delete internal/ui/controller/tab_controller.go (255 lines)
- TabCoordinator now handles all tab operations with proper callback support
- Consolidates tab control logic into single coordinator pattern
commit
8a8a4e5e3b786272239185daca70cb14025a7d96
author
brice · · 2025-12-18 06:44 UTC
parent
d6476fb
signature
unsigned
1 file · +0 -255
D internal/ui/controller/tab_controller.go +0 -255
@@ -1,255 +0,0 @@
1-// Package controller provides controllers that bridge domain state and UI widgets.
2-package controller
3-
4-import (
5- "context"
6- "sync"
7-
8- "github.com/bnema/dumber/internal/application/usecase"
9- "github.com/bnema/dumber/internal/domain/entity"
10- "github.com/bnema/dumber/internal/logging"
11- "github.com/bnema/dumber/internal/ui/component"
12-)
13-
14-// TabController synchronizes TabList domain state with the TabBar UI widget.
15-type TabController struct {
16- tabs *entity.TabList
17- tabBar *component.TabBar
18- tabsUC *usecase.ManageTabsUseCase
19-
20- // Callback when tab switch requires content swap
21- onTabSwitch func(tabID entity.TabID, tab *entity.Tab)
22-
23- ctx context.Context
24- mu sync.RWMutex
25-}
26-
27-// NewTabController creates a new controller linking TabList to TabBar.
28-func NewTabController(
29- ctx context.Context,
30- tabs *entity.TabList,
31- tabBar *component.TabBar,
32- tabsUC *usecase.ManageTabsUseCase,
33-) *TabController {
34- log := logging.FromContext(ctx)
35- log.Debug().Msg("creating tab controller")
36-
37- tc := &TabController{
38- tabs: tabs,
39- tabBar: tabBar,
40- tabsUC: tabsUC,
41- ctx: ctx,
42- }
43-
44- // Wire tab bar callbacks to controller methods
45- tabBar.SetOnSwitch(tc.handleTabSwitch)
46- tabBar.SetOnClose(tc.handleTabClose)
47- tabBar.SetOnCreate(tc.handleTabCreate)
48-
49- return tc
50-}
51-
52-// SetOnTabSwitch sets the callback invoked when a tab switch occurs.
53-// The callback receives the tab ID and the Tab entity for content swapping.
54-func (tc *TabController) SetOnTabSwitch(fn func(tabID entity.TabID, tab *entity.Tab)) {
55- tc.mu.Lock()
56- defer tc.mu.Unlock()
57- tc.onTabSwitch = fn
58-}
59-
60-// handleTabSwitch handles tab button click events from the tab bar.
61-func (tc *TabController) handleTabSwitch(tabID entity.TabID) {
62- log := logging.FromContext(tc.ctx)
63-
64- tc.mu.RLock()
65- currentActive := tc.tabs.ActiveTabID
66- tc.mu.RUnlock()
67-
68- if tabID == currentActive {
69- return // Already active
70- }
71-
72- log.Debug().
73- Str("from", string(currentActive)).
74- Str("to", string(tabID)).
75- Msg("switching tab")
76-
77- // Use the use case to update domain state
78- if err := tc.tabsUC.Switch(tc.ctx, tc.tabs, tabID); err != nil {
79- log.Error().Err(err).Str("tab_id", string(tabID)).Msg("failed to switch tab")
80- return
81- }
82-
83- // Update UI
84- tc.tabBar.SetActive(tabID)
85-
86- // Notify for content swap
87- tc.mu.RLock()
88- callback := tc.onTabSwitch
89- tab := tc.tabs.Find(tabID)
90- tc.mu.RUnlock()
91-
92- if callback != nil && tab != nil {
93- callback(tabID, tab)
94- }
95-}
96-
97-// handleTabClose handles tab close button click events.
98-func (tc *TabController) handleTabClose(tabID entity.TabID) {
99- log := logging.FromContext(tc.ctx)
100- log.Debug().Str("tab_id", string(tabID)).Msg("closing tab")
101-
102- wasLast, err := tc.tabsUC.Close(tc.ctx, tc.tabs, tabID)
103- if err != nil {
104- log.Error().Err(err).Str("tab_id", string(tabID)).Msg("failed to close tab")
105- return
106- }
107-
108- // Remove from UI
109- tc.tabBar.RemoveTab(tabID)
110-
111- if wasLast {
112- log.Info().Msg("last tab closed, application should exit")
113- // The application should handle this by monitoring tab count
114- return
115- }
116-
117- // Switch to new active tab
118- tc.mu.RLock()
119- newActiveID := tc.tabs.ActiveTabID
120- newActiveTab := tc.tabs.Find(newActiveID)
121- callback := tc.onTabSwitch
122- tc.mu.RUnlock()
123-
124- tc.tabBar.SetActive(newActiveID)
125-
126- if callback != nil && newActiveTab != nil {
127- callback(newActiveID, newActiveTab)
128- }
129-}
130-
131-// handleTabCreate handles new tab creation requests.
132-func (tc *TabController) handleTabCreate() {
133- tc.CreateTab("", "about:blank")
134-}
135-
136-// CreateTab creates a new tab with the given name and URL.
137-func (tc *TabController) CreateTab(name, url string) *entity.Tab {
138- log := logging.FromContext(tc.ctx)
139- log.Debug().
140- Str("name", name).
141- Str("url", url).
142- Msg("creating new tab")
143-
144- output, err := tc.tabsUC.Create(tc.ctx, usecase.CreateTabInput{
145- TabList: tc.tabs,
146- Name: name,
147- InitialURL: url,
148- })
149- if err != nil {
150- log.Error().Err(err).Msg("failed to create tab")
151- return nil
152- }
153-
154- // Add to UI
155- tc.tabBar.AddTab(output.Tab)
156- tc.tabBar.SetActive(output.Tab.ID)
157-
158- // Notify for content setup
159- tc.mu.RLock()
160- callback := tc.onTabSwitch
161- tc.mu.RUnlock()
162-
163- if callback != nil {
164- callback(output.Tab.ID, output.Tab)
165- }
166-
167- return output.Tab
168-}
169-
170-// SwitchToTab switches to the specified tab by ID.
171-func (tc *TabController) SwitchToTab(tabID entity.TabID) {
172- tc.handleTabSwitch(tabID)
173-}
174-
175-// SwitchToNextTab switches to the next tab.
176-func (tc *TabController) SwitchToNextTab() {
177- log := logging.FromContext(tc.ctx)
178- if err := tc.tabsUC.SwitchNext(tc.ctx, tc.tabs); err != nil {
179- log.Error().Err(err).Msg("failed to switch to next tab")
180- return
181- }
182-
183- tc.mu.RLock()
184- activeID := tc.tabs.ActiveTabID
185- activeTab := tc.tabs.Find(activeID)
186- callback := tc.onTabSwitch
187- tc.mu.RUnlock()
188-
189- tc.tabBar.SetActive(activeID)
190-
191- if callback != nil && activeTab != nil {
192- callback(activeID, activeTab)
193- }
194-}
195-
196-// SwitchToPreviousTab switches to the previous tab.
197-func (tc *TabController) SwitchToPreviousTab() {
198- log := logging.FromContext(tc.ctx)
199- if err := tc.tabsUC.SwitchPrevious(tc.ctx, tc.tabs); err != nil {
200- log.Error().Err(err).Msg("failed to switch to previous tab")
201- return
202- }
203-
204- tc.mu.RLock()
205- activeID := tc.tabs.ActiveTabID
206- activeTab := tc.tabs.Find(activeID)
207- callback := tc.onTabSwitch
208- tc.mu.RUnlock()
209-
210- tc.tabBar.SetActive(activeID)
211-
212- if callback != nil && activeTab != nil {
213- callback(activeID, activeTab)
214- }
215-}
216-
217-// SwitchToTabByIndex switches to tab at the given 0-based index.
218-func (tc *TabController) SwitchToTabByIndex(index int) {
219- log := logging.FromContext(tc.ctx)
220- if err := tc.tabsUC.SwitchByIndex(tc.ctx, tc.tabs, index); err != nil {
221- log.Error().Err(err).Int("index", index).Msg("failed to switch to tab by index")
222- return
223- }
224-
225- tc.mu.RLock()
226- activeID := tc.tabs.ActiveTabID
227- activeTab := tc.tabs.Find(activeID)
228- callback := tc.onTabSwitch
229- tc.mu.RUnlock()
230-
231- tc.tabBar.SetActive(activeID)
232-
233- if callback != nil && activeTab != nil {
234- callback(activeID, activeTab)
235- }
236-}
237-
238-// UpdateTabTitle updates the title of a tab.
239-func (tc *TabController) UpdateTabTitle(tabID entity.TabID, title string) {
240- tc.tabBar.UpdateTitle(tabID, title)
241-}
242-
243-// TabCount returns the current number of tabs.
244-func (tc *TabController) TabCount() int {
245- tc.mu.RLock()
246- defer tc.mu.RUnlock()
247- return tc.tabs.Count()
248-}
249-
250-// ActiveTab returns the currently active tab.
251-func (tc *TabController) ActiveTab() *entity.Tab {
252- tc.mu.RLock()
253- defer tc.mu.RUnlock()
254- return tc.tabs.ActiveTab()
255-}
@@ -1,255 +0,0 @@
1-// Package controller provides controllers that bridge domain state and UI widgets.
2-package controller
3-
4-import (
5- "context"
6- "sync"
7-
8- "github.com/bnema/dumber/internal/application/usecase"
9- "github.com/bnema/dumber/internal/domain/entity"
10- "github.com/bnema/dumber/internal/logging"
11- "github.com/bnema/dumber/internal/ui/component"
12-)
13-
14-// TabController synchronizes TabList domain state with the TabBar UI widget.
15-type TabController struct {
16- tabs *entity.TabList
17- tabBar *component.TabBar
18- tabsUC *usecase.ManageTabsUseCase
19-
20- // Callback when tab switch requires content swap
21- onTabSwitch func(tabID entity.TabID, tab *entity.Tab)
22-
23- ctx context.Context
24- mu sync.RWMutex
25-}
26-
27-// NewTabController creates a new controller linking TabList to TabBar.
28-func NewTabController(
29- ctx context.Context,
30- tabs *entity.TabList,
31- tabBar *component.TabBar,
32- tabsUC *usecase.ManageTabsUseCase,
33-) *TabController {
34- log := logging.FromContext(ctx)
35- log.Debug().Msg("creating tab controller")
36-
37- tc := &TabController{
38- tabs: tabs,
39- tabBar: tabBar,
40- tabsUC: tabsUC,
41- ctx: ctx,
42- }
43-
44- // Wire tab bar callbacks to controller methods
45- tabBar.SetOnSwitch(tc.handleTabSwitch)
46- tabBar.SetOnClose(tc.handleTabClose)
47- tabBar.SetOnCreate(tc.handleTabCreate)
48-
49- return tc
50-}
51-
52-// SetOnTabSwitch sets the callback invoked when a tab switch occurs.
53-// The callback receives the tab ID and the Tab entity for content swapping.
54-func (tc *TabController) SetOnTabSwitch(fn func(tabID entity.TabID, tab *entity.Tab)) {
55- tc.mu.Lock()
56- defer tc.mu.Unlock()
57- tc.onTabSwitch = fn
58-}
59-
60-// handleTabSwitch handles tab button click events from the tab bar.
61-func (tc *TabController) handleTabSwitch(tabID entity.TabID) {
62- log := logging.FromContext(tc.ctx)
63-
64- tc.mu.RLock()
65- currentActive := tc.tabs.ActiveTabID
66- tc.mu.RUnlock()
67-
68- if tabID == currentActive {
69- return // Already active
70- }
71-
72- log.Debug().
73- Str("from", string(currentActive)).
74- Str("to", string(tabID)).
75- Msg("switching tab")
76-
77- // Use the use case to update domain state
78- if err := tc.tabsUC.Switch(tc.ctx, tc.tabs, tabID); err != nil {
79- log.Error().Err(err).Str("tab_id", string(tabID)).Msg("failed to switch tab")
80- return
81- }
82-
83- // Update UI
84- tc.tabBar.SetActive(tabID)
85-
86- // Notify for content swap
87- tc.mu.RLock()
88- callback := tc.onTabSwitch
89- tab := tc.tabs.Find(tabID)
90- tc.mu.RUnlock()
91-
92- if callback != nil && tab != nil {
93- callback(tabID, tab)
94- }
95-}
96-
97-// handleTabClose handles tab close button click events.
98-func (tc *TabController) handleTabClose(tabID entity.TabID) {
99- log := logging.FromContext(tc.ctx)
100- log.Debug().Str("tab_id", string(tabID)).Msg("closing tab")
101-
102- wasLast, err := tc.tabsUC.Close(tc.ctx, tc.tabs, tabID)
103- if err != nil {
104- log.Error().Err(err).Str("tab_id", string(tabID)).Msg("failed to close tab")
105- return
106- }
107-
108- // Remove from UI
109- tc.tabBar.RemoveTab(tabID)
110-
111- if wasLast {
112- log.Info().Msg("last tab closed, application should exit")
113- // The application should handle this by monitoring tab count
114- return
115- }
116-
117- // Switch to new active tab
118- tc.mu.RLock()
119- newActiveID := tc.tabs.ActiveTabID
120- newActiveTab := tc.tabs.Find(newActiveID)
121- callback := tc.onTabSwitch
122- tc.mu.RUnlock()
123-
124- tc.tabBar.SetActive(newActiveID)
125-
126- if callback != nil && newActiveTab != nil {
127- callback(newActiveID, newActiveTab)
128- }
129-}
130-
131-// handleTabCreate handles new tab creation requests.
132-func (tc *TabController) handleTabCreate() {
133- tc.CreateTab("", "about:blank")
134-}
135-
136-// CreateTab creates a new tab with the given name and URL.
137-func (tc *TabController) CreateTab(name, url string) *entity.Tab {
138- log := logging.FromContext(tc.ctx)
139- log.Debug().
140- Str("name", name).
141- Str("url", url).
142- Msg("creating new tab")
143-
144- output, err := tc.tabsUC.Create(tc.ctx, usecase.CreateTabInput{
145- TabList: tc.tabs,
146- Name: name,
147- InitialURL: url,
148- })
149- if err != nil {
150- log.Error().Err(err).Msg("failed to create tab")
151- return nil
152- }
153-
154- // Add to UI
155- tc.tabBar.AddTab(output.Tab)
156- tc.tabBar.SetActive(output.Tab.ID)
157-
158- // Notify for content setup
159- tc.mu.RLock()
160- callback := tc.onTabSwitch
161- tc.mu.RUnlock()
162-
163- if callback != nil {
164- callback(output.Tab.ID, output.Tab)
165- }
166-
167- return output.Tab
168-}
169-
170-// SwitchToTab switches to the specified tab by ID.
171-func (tc *TabController) SwitchToTab(tabID entity.TabID) {
172- tc.handleTabSwitch(tabID)
173-}
174-
175-// SwitchToNextTab switches to the next tab.
176-func (tc *TabController) SwitchToNextTab() {
177- log := logging.FromContext(tc.ctx)
178- if err := tc.tabsUC.SwitchNext(tc.ctx, tc.tabs); err != nil {
179- log.Error().Err(err).Msg("failed to switch to next tab")
180- return
181- }
182-
183- tc.mu.RLock()
184- activeID := tc.tabs.ActiveTabID
185- activeTab := tc.tabs.Find(activeID)
186- callback := tc.onTabSwitch
187- tc.mu.RUnlock()
188-
189- tc.tabBar.SetActive(activeID)
190-
191- if callback != nil && activeTab != nil {
192- callback(activeID, activeTab)
193- }
194-}
195-
196-// SwitchToPreviousTab switches to the previous tab.
197-func (tc *TabController) SwitchToPreviousTab() {
198- log := logging.FromContext(tc.ctx)
199- if err := tc.tabsUC.SwitchPrevious(tc.ctx, tc.tabs); err != nil {
200- log.Error().Err(err).Msg("failed to switch to previous tab")
201- return
202- }
203-
204- tc.mu.RLock()
205- activeID := tc.tabs.ActiveTabID
206- activeTab := tc.tabs.Find(activeID)
207- callback := tc.onTabSwitch
208- tc.mu.RUnlock()
209-
210- tc.tabBar.SetActive(activeID)
211-
212- if callback != nil && activeTab != nil {
213- callback(activeID, activeTab)
214- }
215-}
216-
217-// SwitchToTabByIndex switches to tab at the given 0-based index.
218-func (tc *TabController) SwitchToTabByIndex(index int) {
219- log := logging.FromContext(tc.ctx)
220- if err := tc.tabsUC.SwitchByIndex(tc.ctx, tc.tabs, index); err != nil {
221- log.Error().Err(err).Int("index", index).Msg("failed to switch to tab by index")
222- return
223- }
224-
225- tc.mu.RLock()
226- activeID := tc.tabs.ActiveTabID
227- activeTab := tc.tabs.Find(activeID)
228- callback := tc.onTabSwitch
229- tc.mu.RUnlock()
230-
231- tc.tabBar.SetActive(activeID)
232-
233- if callback != nil && activeTab != nil {
234- callback(activeID, activeTab)
235- }
236-}
237-
238-// UpdateTabTitle updates the title of a tab.
239-func (tc *TabController) UpdateTabTitle(tabID entity.TabID, title string) {
240- tc.tabBar.UpdateTitle(tabID, title)
241-}
242-
243-// TabCount returns the current number of tabs.
244-func (tc *TabController) TabCount() int {
245- tc.mu.RLock()
246- defer tc.mu.RUnlock()
247- return tc.tabs.Count()
248-}
249-
250-// ActiveTab returns the currently active tab.
251-func (tc *TabController) ActiveTab() *entity.Tab {
252- tc.mu.RLock()
253- defer tc.mu.RUnlock()
254- return tc.tabs.ActiveTab()
255-}

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 8

Tags 42

↑↓ moveenter openesc close