detached 5690117

feat(tabs): add direct tab switching with index and last-active support

- Add PreviousActiveTabID to TabList for tracking previous active tab
- Add SetActive() method to TabList to update active tab and track previous
- Add TabAt() method to TabList for 0-based index access
- Update Switch() usecase to use SetActive() for proper state tracking
- Add SwitchToLastActive() usecase for Alt+Tab style tab switching
commit
56901179757430d7637f4447da9f24335ad6f6b0
author
brice · · 2025-12-18 06:44 UTC
parent
bba9426
signature
unsigned
2 files · +44 -3
M internal/application/usecase/manage_tabs.go +25 -1
@@ -133,7 +133,7 @@ func (uc *ManageTabsUseCase) Switch(ctx context.Context, tabs *entity.TabList, t
133133 }
134134
135135 oldActive := tabs.ActiveTabID
136- tabs.ActiveTabID = tabID
136+ tabs.SetActive(tabID) // Uses SetActive to track previous tab
137137
138138 log.Info().
139139 Str("from", string(oldActive)).
@@ -258,6 +258,30 @@ func (uc *ManageTabsUseCase) SwitchByIndex(ctx context.Context, tabs *entity.Tab
258258 return uc.Switch(ctx, tabs, tabs.Tabs[index].ID)
259259 }
260260
261+// SwitchToLastActive switches to the previously active tab (Alt+Tab style).
262+func (uc *ManageTabsUseCase) SwitchToLastActive(ctx context.Context, tabs *entity.TabList) error {
263+ log := logging.FromContext(ctx)
264+
265+ if tabs == nil {
266+ return fmt.Errorf("tab list is required")
267+ }
268+
269+ // If no previous tab recorded, do nothing
270+ if tabs.PreviousActiveTabID == "" {
271+ log.Debug().Msg("no previous active tab")
272+ return nil
273+ }
274+
275+ // Verify previous tab still exists
276+ if tabs.Find(tabs.PreviousActiveTabID) == nil {
277+ log.Debug().Str("prev_id", string(tabs.PreviousActiveTabID)).Msg("previous tab no longer exists")
278+ tabs.PreviousActiveTabID = ""
279+ return nil
280+ }
281+
282+ return uc.Switch(ctx, tabs, tabs.PreviousActiveTabID)
283+}
284+
261285 // Pin toggles the pinned state of a tab.
262286 func (uc *ManageTabsUseCase) Pin(ctx context.Context, tabs *entity.TabList, tabID entity.TabID, pinned bool) error {
263287 log := logging.FromContext(ctx)
@@ -133,7 +133,7 @@ func (uc *ManageTabsUseCase) Switch(ctx context.Context, tabs *entity.TabList, t
133 }133 }
134 134
135 oldActive := tabs.ActiveTabID135 oldActive := tabs.ActiveTabID
136- tabs.ActiveTabID = tabID136+ tabs.SetActive(tabID) // Uses SetActive to track previous tab
137 137
138 log.Info().138 log.Info().
139 Str("from", string(oldActive)).139 Str("from", string(oldActive)).
@@ -258,6 +258,30 @@ func (uc *ManageTabsUseCase) SwitchByIndex(ctx context.Context, tabs *entity.Tab
258 return uc.Switch(ctx, tabs, tabs.Tabs[index].ID)258 return uc.Switch(ctx, tabs, tabs.Tabs[index].ID)
259 }259 }
260 260
261+// SwitchToLastActive switches to the previously active tab (Alt+Tab style).
262+func (uc *ManageTabsUseCase) SwitchToLastActive(ctx context.Context, tabs *entity.TabList) error {
263+ log := logging.FromContext(ctx)
264+
265+ if tabs == nil {
266+ return fmt.Errorf("tab list is required")
267+ }
268+
269+ // If no previous tab recorded, do nothing
270+ if tabs.PreviousActiveTabID == "" {
271+ log.Debug().Msg("no previous active tab")
272+ return nil
273+ }
274+
275+ // Verify previous tab still exists
276+ if tabs.Find(tabs.PreviousActiveTabID) == nil {
277+ log.Debug().Str("prev_id", string(tabs.PreviousActiveTabID)).Msg("previous tab no longer exists")
278+ tabs.PreviousActiveTabID = ""
279+ return nil
280+ }
281+
282+ return uc.Switch(ctx, tabs, tabs.PreviousActiveTabID)
283+}
284+
261 // Pin toggles the pinned state of a tab.285 // Pin toggles the pinned state of a tab.
262 func (uc *ManageTabsUseCase) Pin(ctx context.Context, tabs *entity.TabList, tabID entity.TabID, pinned bool) error {286 func (uc *ManageTabsUseCase) Pin(ctx context.Context, tabs *entity.TabList, tabID entity.TabID, pinned bool) error {
263 log := logging.FromContext(ctx)287 log := logging.FromContext(ctx)
M internal/domain/entity/tab.go +19 -2
@@ -48,8 +48,9 @@ func (t *Tab) PaneCount() int {
4848
4949 // TabList manages an ordered collection of tabs.
5050 type TabList struct {
51- Tabs []*Tab
52- ActiveTabID TabID
51+ Tabs []*Tab
52+ ActiveTabID TabID
53+ PreviousActiveTabID TabID // Tracks last active tab for Alt+Tab style switching
5354 }
5455
5556 // NewTabList creates an empty tab list.
@@ -111,6 +112,22 @@ func (tl *TabList) Count() int {
111112 return len(tl.Tabs)
112113 }
113114
115+// SetActive sets the active tab and updates the previous active tab.
116+func (tl *TabList) SetActive(id TabID) {
117+ if id != tl.ActiveTabID && tl.ActiveTabID != "" {
118+ tl.PreviousActiveTabID = tl.ActiveTabID
119+ }
120+ tl.ActiveTabID = id
121+}
122+
123+// TabAt returns the tab at the given 0-based index.
124+func (tl *TabList) TabAt(index int) *Tab {
125+ if index < 0 || index >= len(tl.Tabs) {
126+ return nil
127+ }
128+ return tl.Tabs[index]
129+}
130+
114131 // Move moves a tab to a new position.
115132 func (tl *TabList) Move(id TabID, newPos int) bool {
116133 if newPos < 0 || newPos >= len(tl.Tabs) {
@@ -48,8 +48,9 @@ func (t *Tab) PaneCount() int {
48 48
49 // TabList manages an ordered collection of tabs.49 // TabList manages an ordered collection of tabs.
50 type TabList struct {50 type TabList struct {
51- Tabs []*Tab51+ Tabs []*Tab
52- ActiveTabID TabID52+ ActiveTabID TabID
53+ PreviousActiveTabID TabID // Tracks last active tab for Alt+Tab style switching
53 }54 }
54 55
55 // NewTabList creates an empty tab list.56 // NewTabList creates an empty tab list.
@@ -111,6 +112,22 @@ func (tl *TabList) Count() int {
111 return len(tl.Tabs)112 return len(tl.Tabs)
112 }113 }
113 114
115+// SetActive sets the active tab and updates the previous active tab.
116+func (tl *TabList) SetActive(id TabID) {
117+ if id != tl.ActiveTabID && tl.ActiveTabID != "" {
118+ tl.PreviousActiveTabID = tl.ActiveTabID
119+ }
120+ tl.ActiveTabID = id
121+}
122+
123+// TabAt returns the tab at the given 0-based index.
124+func (tl *TabList) TabAt(index int) *Tab {
125+ if index < 0 || index >= len(tl.Tabs) {
126+ return nil
127+ }
128+ return tl.Tabs[index]
129+}
130+
114 // Move moves a tab to a new position.131 // Move moves a tab to a new position.
115 func (tl *TabList) Move(id TabID, newPos int) bool {132 func (tl *TabList) Move(id TabID, newPos int) bool {
116 if newPos < 0 || newPos >= len(tl.Tabs) {133 if newPos < 0 || newPos >= len(tl.Tabs) {

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