detached c803dee

Merge pull request #157 from bnema/feat/omnibox-height-adapt

feat: adapt omnibox result count to parent pane height
commit
c803dee46e5b04bac67fc12360cd021054aba1c5
author
bnema · · 2026-01-29 08:45 UTC
committer
GitHub · · 2026-01-29 08:45 UTC
merge of
62a3808 + 97f67bc · diff against the first parent
signature
GPG UnverifiedB5690EEEBB952194 The key is unknown
7 files · +185 -20
M internal/ui/app.go +6 -2
@@ -543,7 +543,7 @@ func (a *App) initKeyboardHandler(ctx context.Context) {
543543 a.keyboardHandler.SetOnModeChange(func(from, to input.Mode) {
544544 a.handleModeChange(ctx, from, to)
545545 })
546- a.keyboardHandler.SetShouldBypassInput(func() bool {
546+ a.keyboardHandler.SetShouldBypassInput(func(modifiers input.Modifier) bool {
547547 // Bypass keyboard handler when modals are visible
548548 if a.sessionManager != nil && a.sessionManager.IsVisible() {
549549 return true
@@ -555,7 +555,11 @@ func (a *App) initKeyboardHandler(ctx context.Context) {
555555 if wsView == nil {
556556 return false
557557 }
558- return wsView.IsOmniboxVisible()
558+ if wsView.IsOmniboxVisible() {
559+ // Let Alt-modified keys through for pane navigation
560+ return modifiers&input.ModAlt == 0
561+ }
562+ return false
559563 })
560564 // Wire accent handler for dead keys support
561565 if a.insertAccentUC != nil {
@@ -543,7 +543,7 @@ func (a *App) initKeyboardHandler(ctx context.Context) {
543 a.keyboardHandler.SetOnModeChange(func(from, to input.Mode) {543 a.keyboardHandler.SetOnModeChange(func(from, to input.Mode) {
544 a.handleModeChange(ctx, from, to)544 a.handleModeChange(ctx, from, to)
545 })545 })
546- a.keyboardHandler.SetShouldBypassInput(func() bool {546+ a.keyboardHandler.SetShouldBypassInput(func(modifiers input.Modifier) bool {
547 // Bypass keyboard handler when modals are visible547 // Bypass keyboard handler when modals are visible
548 if a.sessionManager != nil && a.sessionManager.IsVisible() {548 if a.sessionManager != nil && a.sessionManager.IsVisible() {
549 return true549 return true
@@ -555,7 +555,11 @@ func (a *App) initKeyboardHandler(ctx context.Context) {
555 if wsView == nil {555 if wsView == nil {
556 return false556 return false
557 }557 }
558- return wsView.IsOmniboxVisible()558+ if wsView.IsOmniboxVisible() {
559+ // Let Alt-modified keys through for pane navigation
560+ return modifiers&input.ModAlt == 0
561+ }
562+ return false
559 })563 })
560 // Wire accent handler for dead keys support564 // Wire accent handler for dead keys support
561 if a.insertAccentUC != nil {565 if a.insertAccentUC != nil {
M internal/ui/component/modal_sizing.go +25 -4
@@ -24,8 +24,9 @@ type RowHeightDefaults struct {
2424
2525 // ListDisplayDefaults holds display limits for list-based modals.
2626 type ListDisplayDefaults struct {
27- MaxVisibleRows int // Maximum rows visible before scrolling
28- MaxResults int // Maximum results to fetch/display
27+ MaxVisibleRows int // Maximum rows visible before scrolling
28+ MaxResults int // Maximum results to fetch/display
29+ SmallMaxVisibleRows int // Reduced row count when rows don't fit (0 = no adaptation)
2930 }
3031
3132 // Package-level defaults for row heights.
@@ -46,8 +47,9 @@ var OmniboxSizeDefaults = ModalSizeConfig{
4647
4748 // OmniboxListDefaults provides display limits for omnibox modal.
4849 var OmniboxListDefaults = ListDisplayDefaults{
49- MaxVisibleRows: 10,
50- MaxResults: 10,
50+ MaxVisibleRows: 10,
51+ MaxResults: 10,
52+ SmallMaxVisibleRows: 5,
5153 }
5254
5355 // SessionManagerSizeDefaults provides default sizing for session manager modal.
@@ -80,6 +82,25 @@ var TabPickerListDefaults = ListDisplayDefaults{
8082 MaxResults: 20,
8183 }
8284
85+// EffectiveMaxRows returns the maximum number of visible rows that fit
86+// in the available space. It computes available height by subtracting the
87+// top margin (TopMarginPct of parentHeight) and chrome height (header tabs +
88+// search entry, estimated as 2× rowHeight to scale with the UI). If
89+// MaxVisibleRows worth of rows don't fit, it falls back to SmallMaxVisibleRows.
90+func EffectiveMaxRows(parentHeight, rowHeight int, sizeCfg ModalSizeConfig, defaults ListDisplayDefaults) int {
91+ if parentHeight <= 0 || rowHeight <= 0 || defaults.SmallMaxVisibleRows <= 0 {
92+ return defaults.MaxVisibleRows
93+ }
94+ topMargin := int(float64(parentHeight) * sizeCfg.TopMarginPct)
95+ chromeHeight := 2 * rowHeight // header tabs + search entry, scales with UI
96+ available := parentHeight - topMargin - chromeHeight
97+ needed := defaults.MaxVisibleRows * rowHeight
98+ if available < needed {
99+ return defaults.SmallMaxVisibleRows
100+ }
101+ return defaults.MaxVisibleRows
102+}
103+
83104 // CalculateModalDimensions computes width and top margin based on parent overlay.
84105 // Returns calculated width and top margin in pixels.
85106 func CalculateModalDimensions(parent layout.OverlayWidget, cfg ModalSizeConfig) (width, marginTop int) {
@@ -24,8 +24,9 @@ type RowHeightDefaults struct {
24 24
25 // ListDisplayDefaults holds display limits for list-based modals.25 // ListDisplayDefaults holds display limits for list-based modals.
26 type ListDisplayDefaults struct {26 type ListDisplayDefaults struct {
27- MaxVisibleRows int // Maximum rows visible before scrolling27+ MaxVisibleRows int // Maximum rows visible before scrolling
28- MaxResults int // Maximum results to fetch/display28+ MaxResults int // Maximum results to fetch/display
29+ SmallMaxVisibleRows int // Reduced row count when rows don't fit (0 = no adaptation)
29 }30 }
30 31
31 // Package-level defaults for row heights.32 // Package-level defaults for row heights.
@@ -46,8 +47,9 @@ var OmniboxSizeDefaults = ModalSizeConfig{
46 47
47 // OmniboxListDefaults provides display limits for omnibox modal.48 // OmniboxListDefaults provides display limits for omnibox modal.
48 var OmniboxListDefaults = ListDisplayDefaults{49 var OmniboxListDefaults = ListDisplayDefaults{
49- MaxVisibleRows: 10,50+ MaxVisibleRows: 10,
50- MaxResults: 10,51+ MaxResults: 10,
52+ SmallMaxVisibleRows: 5,
51 }53 }
52 54
53 // SessionManagerSizeDefaults provides default sizing for session manager modal.55 // SessionManagerSizeDefaults provides default sizing for session manager modal.
@@ -80,6 +82,25 @@ var TabPickerListDefaults = ListDisplayDefaults{
80 MaxResults: 20,82 MaxResults: 20,
81 }83 }
82 84
85+// EffectiveMaxRows returns the maximum number of visible rows that fit
86+// in the available space. It computes available height by subtracting the
87+// top margin (TopMarginPct of parentHeight) and chrome height (header tabs +
88+// search entry, estimated as 2× rowHeight to scale with the UI). If
89+// MaxVisibleRows worth of rows don't fit, it falls back to SmallMaxVisibleRows.
90+func EffectiveMaxRows(parentHeight, rowHeight int, sizeCfg ModalSizeConfig, defaults ListDisplayDefaults) int {
91+ if parentHeight <= 0 || rowHeight <= 0 || defaults.SmallMaxVisibleRows <= 0 {
92+ return defaults.MaxVisibleRows
93+ }
94+ topMargin := int(float64(parentHeight) * sizeCfg.TopMarginPct)
95+ chromeHeight := 2 * rowHeight // header tabs + search entry, scales with UI
96+ available := parentHeight - topMargin - chromeHeight
97+ needed := defaults.MaxVisibleRows * rowHeight
98+ if available < needed {
99+ return defaults.SmallMaxVisibleRows
100+ }
101+ return defaults.MaxVisibleRows
102+}
103+
83 // CalculateModalDimensions computes width and top margin based on parent overlay.104 // CalculateModalDimensions computes width and top margin based on parent overlay.
84 // Returns calculated width and top margin in pixels.105 // Returns calculated width and top margin in pixels.
85 func CalculateModalDimensions(parent layout.OverlayWidget, cfg ModalSizeConfig) (width, marginTop int) {106 func CalculateModalDimensions(parent layout.OverlayWidget, cfg ModalSizeConfig) (width, marginTop int) {
M internal/ui/component/modal_sizing_test.go +91 -0
@@ -143,3 +143,94 @@ func TestListDisplayDefaults_Values(t *testing.T) {
143143 assert.Positive(t, SessionManagerListDefaults.MaxVisibleRows)
144144 assert.Positive(t, SessionManagerListDefaults.MaxResults)
145145 }
146+
147+func TestEffectiveMaxRows(t *testing.T) {
148+ sizeCfg := ModalSizeConfig{TopMarginPct: 0.2}
149+ defaults := ListDisplayDefaults{
150+ MaxVisibleRows: 10,
151+ MaxResults: 10,
152+ SmallMaxVisibleRows: 5,
153+ }
154+
155+ tests := []struct {
156+ name string
157+ parentHeight int
158+ rowHeight int
159+ want int
160+ }{
161+ {
162+ name: "tall pane returns MaxVisibleRows",
163+ parentHeight: 1200,
164+ rowHeight: 72,
165+ want: 10,
166+ },
167+ {
168+ name: "short pane returns SmallMaxVisibleRows",
169+ parentHeight: 800,
170+ rowHeight: 72,
171+ want: 5,
172+ },
173+ {
174+ name: "zero parentHeight returns MaxVisibleRows",
175+ parentHeight: 0,
176+ rowHeight: 72,
177+ want: 10,
178+ },
179+ {
180+ name: "zero rowHeight returns MaxVisibleRows",
181+ parentHeight: 800,
182+ rowHeight: 0,
183+ want: 10,
184+ },
185+ {
186+ name: "negative parentHeight returns MaxVisibleRows",
187+ parentHeight: -100,
188+ rowHeight: 72,
189+ want: 10,
190+ },
191+ {
192+ name: "exact fit returns MaxVisibleRows",
193+ parentHeight: 1150, // available = 1150 - 230 - 144 = 776, needed = 720
194+ rowHeight: 72,
195+ want: 10,
196+ },
197+ {
198+ name: "boundary: available equals needed still fits",
199+ parentHeight: 1080, // available = 1080 - 216 - 144 = 720, needed = 720 → fits
200+ rowHeight: 72,
201+ want: 10,
202+ },
203+ {
204+ name: "boundary: one pixel under triggers small",
205+ parentHeight: 1079, // available = 1079 - 215 - 144 = 720, needed = 720 → check
206+ rowHeight: 72,
207+ want: 10, // int(1079*0.2) = 215, 1079-215-144 = 720 → still fits
208+ },
209+ {
210+ name: "just below boundary triggers small",
211+ parentHeight: 1078, // available = 1078 - 215 - 144 = 719, needed = 720 → doesn't fit
212+ rowHeight: 72,
213+ want: 5,
214+ },
215+ }
216+
217+ for _, tt := range tests {
218+ t.Run(tt.name, func(t *testing.T) {
219+ got := EffectiveMaxRows(tt.parentHeight, tt.rowHeight, sizeCfg, defaults)
220+ assert.Equal(t, tt.want, got)
221+ })
222+ }
223+}
224+
225+func TestEffectiveMaxRows_NoAdaptation(t *testing.T) {
226+ sizeCfg := ModalSizeConfig{TopMarginPct: 0.2}
227+ defaults := ListDisplayDefaults{
228+ MaxVisibleRows: 10,
229+ MaxResults: 10,
230+ SmallMaxVisibleRows: 0, // adaptation disabled
231+ }
232+
233+ // Even with a short pane, should return MaxVisibleRows when SmallMaxVisibleRows is 0
234+ got := EffectiveMaxRows(500, 72, sizeCfg, defaults)
235+ assert.Equal(t, 10, got)
236+}
@@ -143,3 +143,94 @@ func TestListDisplayDefaults_Values(t *testing.T) {
143 assert.Positive(t, SessionManagerListDefaults.MaxVisibleRows)143 assert.Positive(t, SessionManagerListDefaults.MaxVisibleRows)
144 assert.Positive(t, SessionManagerListDefaults.MaxResults)144 assert.Positive(t, SessionManagerListDefaults.MaxResults)
145 }145 }
146+
147+func TestEffectiveMaxRows(t *testing.T) {
148+ sizeCfg := ModalSizeConfig{TopMarginPct: 0.2}
149+ defaults := ListDisplayDefaults{
150+ MaxVisibleRows: 10,
151+ MaxResults: 10,
152+ SmallMaxVisibleRows: 5,
153+ }
154+
155+ tests := []struct {
156+ name string
157+ parentHeight int
158+ rowHeight int
159+ want int
160+ }{
161+ {
162+ name: "tall pane returns MaxVisibleRows",
163+ parentHeight: 1200,
164+ rowHeight: 72,
165+ want: 10,
166+ },
167+ {
168+ name: "short pane returns SmallMaxVisibleRows",
169+ parentHeight: 800,
170+ rowHeight: 72,
171+ want: 5,
172+ },
173+ {
174+ name: "zero parentHeight returns MaxVisibleRows",
175+ parentHeight: 0,
176+ rowHeight: 72,
177+ want: 10,
178+ },
179+ {
180+ name: "zero rowHeight returns MaxVisibleRows",
181+ parentHeight: 800,
182+ rowHeight: 0,
183+ want: 10,
184+ },
185+ {
186+ name: "negative parentHeight returns MaxVisibleRows",
187+ parentHeight: -100,
188+ rowHeight: 72,
189+ want: 10,
190+ },
191+ {
192+ name: "exact fit returns MaxVisibleRows",
193+ parentHeight: 1150, // available = 1150 - 230 - 144 = 776, needed = 720
194+ rowHeight: 72,
195+ want: 10,
196+ },
197+ {
198+ name: "boundary: available equals needed still fits",
199+ parentHeight: 1080, // available = 1080 - 216 - 144 = 720, needed = 720 → fits
200+ rowHeight: 72,
201+ want: 10,
202+ },
203+ {
204+ name: "boundary: one pixel under triggers small",
205+ parentHeight: 1079, // available = 1079 - 215 - 144 = 720, needed = 720 → check
206+ rowHeight: 72,
207+ want: 10, // int(1079*0.2) = 215, 1079-215-144 = 720 → still fits
208+ },
209+ {
210+ name: "just below boundary triggers small",
211+ parentHeight: 1078, // available = 1078 - 215 - 144 = 719, needed = 720 → doesn't fit
212+ rowHeight: 72,
213+ want: 5,
214+ },
215+ }
216+
217+ for _, tt := range tests {
218+ t.Run(tt.name, func(t *testing.T) {
219+ got := EffectiveMaxRows(tt.parentHeight, tt.rowHeight, sizeCfg, defaults)
220+ assert.Equal(t, tt.want, got)
221+ })
222+ }
223+}
224+
225+func TestEffectiveMaxRows_NoAdaptation(t *testing.T) {
226+ sizeCfg := ModalSizeConfig{TopMarginPct: 0.2}
227+ defaults := ListDisplayDefaults{
228+ MaxVisibleRows: 10,
229+ MaxResults: 10,
230+ SmallMaxVisibleRows: 0, // adaptation disabled
231+ }
232+
233+ // Even with a short pane, should return MaxVisibleRows when SmallMaxVisibleRows is 0
234+ got := EffectiveMaxRows(500, 72, sizeCfg, defaults)
235+ assert.Equal(t, 10, got)
236+}
M internal/ui/component/omnibox.go +41 -10
@@ -301,16 +301,33 @@ func (o *Omnibox) WidgetAsLayout(factory layout.WidgetFactory) layout.Widget {
301301 return factory.WrapWidget(&o.outerBox.Widget)
302302 }
303303
304+// estimateRowHeight returns the current best estimate for a single row height.
305+func (o *Omnibox) estimateRowHeight() int {
306+ if o.measuredHeights.valid && o.measuredHeights.singleRow > 0 {
307+ return o.measuredHeights.singleRow
308+ }
309+ return ScaleValue(DefaultRowHeights.Standard, o.uiScale)
310+}
311+
312+// effectiveMaxRows returns the max visible rows adapted to the current parent pane height.
313+// Must be called on the GTK main thread.
314+func (o *Omnibox) effectiveMaxRows() int {
315+ if o.parentOverlay == nil {
316+ return OmniboxListDefaults.MaxVisibleRows
317+ }
318+ return EffectiveMaxRows(o.parentOverlay.GetAllocatedHeight(), o.estimateRowHeight(), OmniboxSizeDefaults, OmniboxListDefaults)
319+}
320+
304321 // resizeAndCenter adjusts the omnibox size based on content and centers it.
305-// rowCount is the number of result rows to display (0 = no content, max 10).
322+// rowCount is the number of result rows to display (0 = no content, adapts to parent height).
306323 func (o *Omnibox) resizeAndCenter(rowCount int) {
307324 if o.parentOverlay == nil || o.outerBox == nil || o.mainBox == nil {
308325 return
309326 }
310327
311- // Cap at max results
312- if rowCount > OmniboxListDefaults.MaxVisibleRows {
313- rowCount = OmniboxListDefaults.MaxVisibleRows
328+ // Cap at effective max rows (adapts to parent pane height)
329+ if maxRows := o.effectiveMaxRows(); rowCount > maxRows {
330+ rowCount = maxRows
314331 }
315332
316333 // Schedule measurement after GTK has laid out widgets
@@ -351,8 +368,8 @@ func (o *Omnibox) measureAndResize(width, rowCount int) {
351368 contentHeight = rowCount * rowHeight
352369 }
353370
354- // Cap at max results
355- maxContentHeight := OmniboxListDefaults.MaxVisibleRows * rowHeight
371+ // Cap at effective max rows (adapts to parent pane height)
372+ maxContentHeight := o.effectiveMaxRows() * rowHeight
356373 if contentHeight > maxContentHeight {
357374 contentHeight = maxContentHeight
358375 }
@@ -823,7 +840,12 @@ func (o *Omnibox) onEntryChanged() {
823840 o.debounceTimer.Stop()
824841 }
825842 o.debounceTimer = time.AfterFunc(debounceDelayMs*time.Millisecond, func() {
826- o.performSearch()
843+ // Schedule on GTK main thread — performSearch reads GTK widget state
844+ var cb glib.SourceFunc = func(uintptr) bool {
845+ o.performSearch()
846+ return false
847+ }
848+ glib.IdleAdd(&cb, 0)
827849 })
828850 o.debounceMu.Unlock()
829851 }
@@ -1212,7 +1234,13 @@ func (o *Omnibox) performSearch() {
12121234 return
12131235 }
12141236
1215- // Perform fuzzy search
1237+ // Perform fuzzy history search in background
1238+ o.searchHistory(query, o.effectiveMaxRows())
1239+}
1240+
1241+// searchHistory runs a fuzzy history search in a background goroutine.
1242+// query is the search text; limit caps the number of results.
1243+func (o *Omnibox) searchHistory(query string, limit int) {
12161244 go func() {
12171245 ctx := o.ctx
12181246 log := logging.FromContext(ctx)
@@ -1231,7 +1259,7 @@ func (o *Omnibox) performSearch() {
12311259 go func() {
12321260 searchInput := usecase.SearchInput{
12331261 Query: query,
1234- Limit: OmniboxListDefaults.MaxResults,
1262+ Limit: limit,
12351263 }
12361264 output, err := o.historyUC.Search(ctx, searchInput)
12371265 searchCh <- searchResult{output, err}
@@ -1274,6 +1302,9 @@ func (o *Omnibox) loadInitialHistory() {
12741302 return
12751303 }
12761304
1305+ // Capture effective result limit on the GTK main thread before spawning goroutine
1306+ initialLimit := o.effectiveMaxRows()
1307+
12771308 go func() {
12781309 ctx := o.ctx
12791310 log := logging.FromContext(ctx)
@@ -1295,7 +1326,7 @@ func (o *Omnibox) loadInitialHistory() {
12951326
12961327 go func() {
12971328 // TODO: Implement GetMostVisited in use case if needed
1298- results, err := o.historyUC.GetRecent(ctx, OmniboxListDefaults.MaxResults, 0)
1329+ results, err := o.historyUC.GetRecent(ctx, initialLimit, 0)
12991330 historyCh <- historyResult{results, err}
13001331 }()
13011332
@@ -301,16 +301,33 @@ func (o *Omnibox) WidgetAsLayout(factory layout.WidgetFactory) layout.Widget {
301 return factory.WrapWidget(&o.outerBox.Widget)301 return factory.WrapWidget(&o.outerBox.Widget)
302 }302 }
303 303
304+// estimateRowHeight returns the current best estimate for a single row height.
305+func (o *Omnibox) estimateRowHeight() int {
306+ if o.measuredHeights.valid && o.measuredHeights.singleRow > 0 {
307+ return o.measuredHeights.singleRow
308+ }
309+ return ScaleValue(DefaultRowHeights.Standard, o.uiScale)
310+}
311+
312+// effectiveMaxRows returns the max visible rows adapted to the current parent pane height.
313+// Must be called on the GTK main thread.
314+func (o *Omnibox) effectiveMaxRows() int {
315+ if o.parentOverlay == nil {
316+ return OmniboxListDefaults.MaxVisibleRows
317+ }
318+ return EffectiveMaxRows(o.parentOverlay.GetAllocatedHeight(), o.estimateRowHeight(), OmniboxSizeDefaults, OmniboxListDefaults)
319+}
320+
304 // resizeAndCenter adjusts the omnibox size based on content and centers it.321 // resizeAndCenter adjusts the omnibox size based on content and centers it.
305-// rowCount is the number of result rows to display (0 = no content, max 10).322+// rowCount is the number of result rows to display (0 = no content, adapts to parent height).
306 func (o *Omnibox) resizeAndCenter(rowCount int) {323 func (o *Omnibox) resizeAndCenter(rowCount int) {
307 if o.parentOverlay == nil || o.outerBox == nil || o.mainBox == nil {324 if o.parentOverlay == nil || o.outerBox == nil || o.mainBox == nil {
308 return325 return
309 }326 }
310 327
311- // Cap at max results328+ // Cap at effective max rows (adapts to parent pane height)
312- if rowCount > OmniboxListDefaults.MaxVisibleRows {329+ if maxRows := o.effectiveMaxRows(); rowCount > maxRows {
313- rowCount = OmniboxListDefaults.MaxVisibleRows330+ rowCount = maxRows
314 }331 }
315 332
316 // Schedule measurement after GTK has laid out widgets333 // Schedule measurement after GTK has laid out widgets
@@ -351,8 +368,8 @@ func (o *Omnibox) measureAndResize(width, rowCount int) {
351 contentHeight = rowCount * rowHeight368 contentHeight = rowCount * rowHeight
352 }369 }
353 370
354- // Cap at max results371+ // Cap at effective max rows (adapts to parent pane height)
355- maxContentHeight := OmniboxListDefaults.MaxVisibleRows * rowHeight372+ maxContentHeight := o.effectiveMaxRows() * rowHeight
356 if contentHeight > maxContentHeight {373 if contentHeight > maxContentHeight {
357 contentHeight = maxContentHeight374 contentHeight = maxContentHeight
358 }375 }
@@ -823,7 +840,12 @@ func (o *Omnibox) onEntryChanged() {
823 o.debounceTimer.Stop()840 o.debounceTimer.Stop()
824 }841 }
825 o.debounceTimer = time.AfterFunc(debounceDelayMs*time.Millisecond, func() {842 o.debounceTimer = time.AfterFunc(debounceDelayMs*time.Millisecond, func() {
826- o.performSearch()843+ // Schedule on GTK main thread — performSearch reads GTK widget state
844+ var cb glib.SourceFunc = func(uintptr) bool {
845+ o.performSearch()
846+ return false
847+ }
848+ glib.IdleAdd(&cb, 0)
827 })849 })
828 o.debounceMu.Unlock()850 o.debounceMu.Unlock()
829 }851 }
@@ -1212,7 +1234,13 @@ func (o *Omnibox) performSearch() {
1212 return1234 return
1213 }1235 }
1214 1236
1215- // Perform fuzzy search1237+ // Perform fuzzy history search in background
1238+ o.searchHistory(query, o.effectiveMaxRows())
1239+}
1240+
1241+// searchHistory runs a fuzzy history search in a background goroutine.
1242+// query is the search text; limit caps the number of results.
1243+func (o *Omnibox) searchHistory(query string, limit int) {
1216 go func() {1244 go func() {
1217 ctx := o.ctx1245 ctx := o.ctx
1218 log := logging.FromContext(ctx)1246 log := logging.FromContext(ctx)
@@ -1231,7 +1259,7 @@ func (o *Omnibox) performSearch() {
1231 go func() {1259 go func() {
1232 searchInput := usecase.SearchInput{1260 searchInput := usecase.SearchInput{
1233 Query: query,1261 Query: query,
1234- Limit: OmniboxListDefaults.MaxResults,1262+ Limit: limit,
1235 }1263 }
1236 output, err := o.historyUC.Search(ctx, searchInput)1264 output, err := o.historyUC.Search(ctx, searchInput)
1237 searchCh <- searchResult{output, err}1265 searchCh <- searchResult{output, err}
@@ -1274,6 +1302,9 @@ func (o *Omnibox) loadInitialHistory() {
1274 return1302 return
1275 }1303 }
1276 1304
1305+ // Capture effective result limit on the GTK main thread before spawning goroutine
1306+ initialLimit := o.effectiveMaxRows()
1307+
1277 go func() {1308 go func() {
1278 ctx := o.ctx1309 ctx := o.ctx
1279 log := logging.FromContext(ctx)1310 log := logging.FromContext(ctx)
@@ -1295,7 +1326,7 @@ func (o *Omnibox) loadInitialHistory() {
1295 1326
1296 go func() {1327 go func() {
1297 // TODO: Implement GetMostVisited in use case if needed1328 // TODO: Implement GetMostVisited in use case if needed
1298- results, err := o.historyUC.GetRecent(ctx, OmniboxListDefaults.MaxResults, 0)1329+ results, err := o.historyUC.GetRecent(ctx, initialLimit, 0)
1299 historyCh <- historyResult{results, err}1330 historyCh <- historyResult{results, err}
1300 }()1331 }()
1301 1332
M internal/ui/component/workspace_view.go +5 -0
@@ -224,6 +224,11 @@ func (wv *WorkspaceView) setActivePaneIDInternal(paneID entity.PaneID) error {
224224 wv.hideFindBarInternal()
225225 }
226226
227+ // Hide omnibox if active pane is changing
228+ if currentActiveID != paneID && wv.omnibox != nil {
229+ wv.hideOmniboxInternal()
230+ }
231+
227232 // Deactivate current active pane
228233 if currentActiveID != "" {
229234 if oldPV, ok := wv.paneViews[currentActiveID]; ok {
@@ -224,6 +224,11 @@ func (wv *WorkspaceView) setActivePaneIDInternal(paneID entity.PaneID) error {
224 wv.hideFindBarInternal()224 wv.hideFindBarInternal()
225 }225 }
226 226
227+ // Hide omnibox if active pane is changing
228+ if currentActiveID != paneID && wv.omnibox != nil {
229+ wv.hideOmniboxInternal()
230+ }
231+
227 // Deactivate current active pane232 // Deactivate current active pane
228 if currentActiveID != "" {233 if currentActiveID != "" {
229 if oldPV, ok := wv.paneViews[currentActiveID]; ok {234 if oldPV, ok := wv.paneViews[currentActiveID]; ok {
M internal/ui/input/keyboard.go +3 -3
@@ -41,7 +41,7 @@ type KeyboardHandler struct {
4141 // Action handler callback
4242 onAction ActionHandler
4343 // Optional bypass check (e.g., omnibox visible)
44- shouldBypass func() bool
44+ shouldBypass func(modifiers Modifier) bool
4545 // Optional accent handler for dead keys support
4646 accentHandler AccentHandler
4747
@@ -99,7 +99,7 @@ func (h *KeyboardHandler) SetOnModeChange(fn func(from, to Mode)) {
9999
100100 // SetShouldBypassInput sets a hook to bypass keyboard handling entirely.
101101 // When true, events propagate to focused widgets instead.
102-func (h *KeyboardHandler) SetShouldBypassInput(fn func() bool) {
102+func (h *KeyboardHandler) SetShouldBypassInput(fn func(modifiers Modifier) bool) {
103103 h.mu.Lock()
104104 defer h.mu.Unlock()
105105 h.shouldBypass = fn
@@ -191,7 +191,7 @@ func (h *KeyboardHandler) handleKeyPress(keyval, keycode uint, state gdk.Modifie
191191 return true
192192 }
193193
194- if shouldBypass != nil && shouldBypass() {
194+ if shouldBypass != nil && shouldBypass(modifiers) {
195195 log.Debug().Uint("keyval", keyval).Uint("keycode", keycode).Uint("state", uint(state)).Msg("keyboard handler bypassed")
196196 return false
197197 }
@@ -41,7 +41,7 @@ type KeyboardHandler struct {
41 // Action handler callback41 // Action handler callback
42 onAction ActionHandler42 onAction ActionHandler
43 // Optional bypass check (e.g., omnibox visible)43 // Optional bypass check (e.g., omnibox visible)
44- shouldBypass func() bool44+ shouldBypass func(modifiers Modifier) bool
45 // Optional accent handler for dead keys support45 // Optional accent handler for dead keys support
46 accentHandler AccentHandler46 accentHandler AccentHandler
47 47
@@ -99,7 +99,7 @@ func (h *KeyboardHandler) SetOnModeChange(fn func(from, to Mode)) {
99 99
100 // SetShouldBypassInput sets a hook to bypass keyboard handling entirely.100 // SetShouldBypassInput sets a hook to bypass keyboard handling entirely.
101 // When true, events propagate to focused widgets instead.101 // When true, events propagate to focused widgets instead.
102-func (h *KeyboardHandler) SetShouldBypassInput(fn func() bool) {102+func (h *KeyboardHandler) SetShouldBypassInput(fn func(modifiers Modifier) bool) {
103 h.mu.Lock()103 h.mu.Lock()
104 defer h.mu.Unlock()104 defer h.mu.Unlock()
105 h.shouldBypass = fn105 h.shouldBypass = fn
@@ -191,7 +191,7 @@ func (h *KeyboardHandler) handleKeyPress(keyval, keycode uint, state gdk.Modifie
191 return true191 return true
192 }192 }
193 193
194- if shouldBypass != nil && shouldBypass() {194+ if shouldBypass != nil && shouldBypass(modifiers) {
195 log.Debug().Uint("keyval", keyval).Uint("keycode", keycode).Uint("state", uint(state)).Msg("keyboard handler bypassed")195 log.Debug().Uint("keyval", keyval).Uint("keycode", keycode).Uint("state", uint(state)).Msg("keyboard handler bypassed")
196 return false196 return false
197 }197 }
M webui/package-lock.json +14 -1 generated file: collapsed

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