main 5041353

app_mode_frame.go RawHistoryFolder

1package ui
2
3import (
4	"context"
5	"sync"
6
7	"github.com/bnema/dumber/internal/logging"
8	"github.com/bnema/dumber/internal/ui/input"
9	"github.com/bnema/dumber/internal/ui/layout"
10)
11
12func (a *App) modeFrameTarget(bw *browserWindow, mode input.Mode) layout.Widget {
13	wsView := a.activeWorkspaceViewForBrowserWindow(bw)
14	if wsView == nil {
15		return nil
16	}
17	if mode == input.ModeTab || mode == input.ModeSession || mode == input.ModePrefix {
18		return wsView.Widget()
19	}
20	ws := a.activeWorkspaceForBrowserWindow(bw)
21	if ws == nil {
22		return nil
23	}
24	if mode == input.ModePane || mode == input.ModeResize {
25		if stack := wsView.GetStackContainerWidget(ws.ActivePaneID); stack != nil {
26			return stack
27		}
28	}
29	if mode == input.ModeVim || mode == input.ModePane || mode == input.ModeResize {
30		return wsView.GetPaneWidget(ws.ActivePaneID)
31	}
32	return nil
33}
34
35func (a *App) retargetModeFrame(bw *browserWindow, mode input.Mode) {
36	if bw == nil || bw.modeFrame == nil {
37		return
38	}
39	a.setModeFrameTab(bw)
40	// Rebuilds can temporarily lack a pane widget. Drop the old border while
41	// the next focus/rebuild event retries the target.
42	bw.modeFrame.setTarget(a.modeFrameTarget(bw, mode), modeFrameClass(mode))
43	if bw.modeFrame.target != nil {
44		bw.modeFrame.refreshGeometry()
45	}
46}
47
48// updateModeFrame reconciles the window's mode border and mode toaster with
49// mode and the live configuration.
50func (a *App) updateModeFrame(bw *browserWindow, mode input.Mode) {
51	if bw == nil {
52		return
53	}
54	a.setModeFrameTab(bw)
55	cfg := a.runtimeConfigSnapshot().UI
56	if bw.modeFrame != nil {
57		bw.modeFrame.setMode(mode, a.modeFrameTarget(bw, mode))
58	}
59	if bw.modeToaster != nil {
60		styling := cfg.Workspace.Styling
61		styling.ModeLegendCollapsed = a.modeLegendCollapsed()
62		bw.modeToaster.apply(mode, styling, cfg.Omnibox.Style, legendActions(mode, cfg.Workspace, cfg.Session))
63	}
64}
65
66func (a *App) setModeFrameTab(bw *browserWindow) {
67	if bw.modeFrame == nil {
68		return
69	}
70	tabID := bw.modeFrame.tabID
71	bw.modeFrame.tabID = ""
72	if tab := a.activeTabForBrowserWindow(bw); tab != nil {
73		bw.modeFrame.tabID = tab.ID
74	}
75	if tabID != bw.modeFrame.tabID {
76		bw.modeToaster.dismissLinger()
77	}
78}
79
80func (a *App) modeFrameAction(bw *browserWindow, action input.Action) {
81	if bw != nil && bw.modeToaster != nil {
82		bw.modeToaster.flash(action)
83	}
84}
85
86// modeLegendPreference is the app-wide "legend collapsed" state, shared by
87// every mode and window. A click is applied at once and saved in the
88// background. It keeps priority over the configured value, which may still be
89// the previous one while saves are in flight, until a configuration reload
90// received after the writer drained settles it (see configReloaded).
91type modeLegendPreference struct {
92	mu        sync.Mutex
93	collapsed bool // last click; meaningful while clicked is set
94	clicked   bool
95	// pending is the next value to save; saving reports a running writer. The
96	// writer drains pending until it is empty, so saves go out in click order
97	// and the file ends on the last click.
98	pending *bool
99	saving  bool
100}
101
102// current returns the state to show, given the configured one.
103func (p *modeLegendPreference) current(configured bool) bool {
104	p.mu.Lock()
105	defer p.mu.Unlock()
106	if p.clicked {
107		return p.collapsed
108	}
109	return configured
110}
111
112// click records a click and returns whether a writer must be started.
113func (p *modeLegendPreference) click(collapsed bool) (startWriter bool) {
114	p.mu.Lock()
115	defer p.mu.Unlock()
116	p.clicked, p.collapsed = true, collapsed
117	p.pending = &collapsed
118	if p.saving {
119		return false
120	}
121	p.saving = true
122	return true
123}
124
125// next hands the writer its next value, or ends it when none is left.
126func (p *modeLegendPreference) next() (collapsed, ok bool) {
127	p.mu.Lock()
128	defer p.mu.Unlock()
129	if p.pending == nil {
130		p.saving = false
131		return false, false
132	}
133	collapsed, p.pending = *p.pending, nil
134	return collapsed, true
135}
136
137// configReloaded hands the state back to the configuration on a reload
138// received once the writer has drained: that reload reflects the saved clicks
139// (or an external edit, or a failed save). Reloads received while the writer
140// runs are ignored: they may come from an earlier save and would undo the
141// latest click.
142func (p *modeLegendPreference) configReloaded(_ bool) {
143	p.mu.Lock()
144	defer p.mu.Unlock()
145	if !p.saving {
146		p.clicked = false
147	}
148}
149
150// modeLegendCollapsed returns the state new windows and mode changes start from.
151func (a *App) modeLegendCollapsed() bool {
152	return a.modeLegend.current(a.runtimeConfigSnapshot().UI.Workspace.Styling.ModeLegendCollapsed)
153}
154
155// setModeLegendCollapsed applies a click on a mode toaster: it updates every
156// open window at once, then saves the choice off the GTK thread. A failed save
157// is logged; the state stays applied until the next configuration reload.
158func (a *App) setModeLegendCollapsed(ctx context.Context, collapsed bool) {
159	for _, bw := range a.browserWindows {
160		if bw != nil && bw.modeToaster != nil {
161			bw.modeToaster.setCollapsed(collapsed)
162		}
163	}
164	start := a.modeLegend.click(collapsed)
165	if !start {
166		return
167	}
168	var save func(context.Context, bool) error
169	if a.deps != nil {
170		save = a.deps.HandlerDeps.SaveModeLegendCollapsed
171	}
172	go func() {
173		for {
174			next, ok := a.modeLegend.next()
175			if !ok {
176				return
177			}
178			if save == nil {
179				continue
180			}
181			if err := save(ctx, next); err != nil {
182				logging.FromContext(ctx).Warn().Err(err).Bool("collapsed", next).
183					Msg("failed to save mode legend collapsed preference")
184			}
185		}
186	}()
187}

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