main 5041353

favorites_sidebar_forms.go RawHistoryFolder

1package component
2
3import (
4	"fmt"
5	"strconv"
6	"strings"
7
8	"github.com/bnema/puregotk/v4/glib"
9	"github.com/bnema/puregotk/v4/gtk"
10
11	"github.com/bnema/dumber/internal/application/dto"
12	"github.com/bnema/dumber/internal/domain/entity"
13)
14
15type favoritesSidebarMode int
16
17const (
18	favoritesSidebarModeNone favoritesSidebarMode = iota
19	favoritesSidebarModeAdd
20	favoritesSidebarModeEdit
21	favoritesSidebarModeCreateTag
22	favoritesSidebarModeBindTag
23	favoritesSidebarModeShortcut
24)
25
26type favoritesSidebarFocusZone int
27
28const (
29	favoritesSidebarFocusSearch favoritesSidebarFocusZone = iota
30	favoritesSidebarFocusTags
31	favoritesSidebarFocusList
32	favoritesSidebarFocusForm
33	favoritesSidebarFocusConfirm
34)
35
36func (fs *FavoritesSidebar) beginAddForm() {
37	fs.mu.Lock()
38	if fs.destroyed {
39		fs.mu.Unlock()
40		return
41	}
42	fs.mode = favoritesSidebarModeAdd
43	fs.editingID = 0
44	fs.confirmDelete = false
45	fs.confirmDeleteID = 0
46	fs.setNoticeLocked("Add favorite: URL, title, tags, shortcut 1-9. Press Ctrl+Enter or Save.")
47	fs.mu.Unlock()
48	fs.renderForm(nil)
49	fs.rebuildList()
50}
51
52func (fs *FavoritesSidebar) beginEditForm() {
53	fav := fs.selectedFavorite()
54	if fav == nil {
55		fs.setNotice("Select a favorite to edit")
56		return
57	}
58	fs.mu.Lock()
59	if fs.destroyed {
60		fs.mu.Unlock()
61		return
62	}
63	fs.mode = favoritesSidebarModeEdit
64	fs.editingID = fav.ID
65	fs.confirmDelete = false
66	fs.confirmDeleteID = 0
67	fs.setNoticeLocked("Edit favorite: URL is read-only; use + on the selected favorite to manage tags. Press Ctrl+Enter or Save.")
68	fs.mu.Unlock()
69	fs.renderForm(fav)
70	fs.rebuildList()
71}
72
73func (fs *FavoritesSidebar) renderForm(fav *entity.Favorite) {
74	if fs == nil || fs.formBox == nil {
75		return
76	}
77	clearBoxChildren(fs.formBox)
78	fs.formCallbacks = nil
79	fs.formTagMatchCallbacks = nil
80	fs.formURLEntry = gtk.NewSearchEntry()
81	fs.formTitleEntry = gtk.NewSearchEntry()
82	fs.formTagSearch = gtk.NewSearchEntry()
83	fs.formTagMatches = gtk.NewBox(gtk.OrientationHorizontalValue, 3)
84	fs.formShortcutEntry = gtk.NewSearchEntry()
85	fs.mu.Lock()
86	fs.formTagIDs = tagIDSet(favoriteTags(fav))
87	fs.mu.Unlock()
88	entries := []struct {
89		entry *gtk.SearchEntry
90		label string
91	}{
92		{entry: fs.formURLEntry, label: "URL"},
93		{entry: fs.formTitleEntry, label: "Title"},
94		{entry: fs.formTagSearch, label: "Add tags..."},
95		{entry: fs.formShortcutEntry, label: "Shortcut"},
96	}
97	for _, item := range entries {
98		if item.entry == nil {
99			continue
100		}
101		placeholder := item.label
102		item.entry.SetPlaceholderText(&placeholder)
103		fs.trackTextInputFocus(&item.entry.Widget, &fs.formCallbacks)
104		fs.formBox.Append(&item.entry.Widget)
105	}
106	if fs.formTagMatches != nil {
107		fs.formTagMatches.AddCssClass("favorites-sidebar-form-tag-matches")
108		fs.formBox.Append(&fs.formTagMatches.Widget)
109	}
110	fs.setupFormTagSearch()
111	fs.formSaveButton = gtk.NewButtonWithLabel("Save")
112	if fs.formSaveButton != nil {
113		cb := func(_ gtk.Button) {
114			fs.submitForm()
115		}
116		fs.formCallbacks = append(fs.formCallbacks, cb)
117		fs.formSaveButton.ConnectClicked(&cb)
118		fs.formBox.Append(&fs.formSaveButton.Widget)
119	}
120	if fav != nil {
121		fs.formURL = fav.URL
122		fs.formTitle = fav.Title
123		fs.formShortcut = ""
124		if fav.ShortcutKey != nil {
125			fs.formShortcut = strconv.Itoa(*fav.ShortcutKey)
126		}
127		if fs.formURLEntry != nil {
128			fs.formURLEntry.SetText(fs.formURL)
129			fs.formURLEntry.SetEditable(false)
130		}
131		if fs.formTitleEntry != nil {
132			fs.formTitleEntry.SetText(fs.formTitle)
133		}
134		if fs.formShortcutEntry != nil {
135			fs.formShortcutEntry.SetText(fs.formShortcut)
136		}
137	} else {
138		fs.formURL, fs.formTitle, fs.formShortcut = "", "", ""
139	}
140	fs.renderFormTagMatches("")
141	fs.formBox.SetVisible(true)
142	fs.focusForm()
143}
144
145func (fs *FavoritesSidebar) focusForm() {
146	fs.scheduleIdle(glib.SourceFunc(func(uintptr) bool {
147		if fs == nil {
148			return false
149		}
150		fs.mu.RLock()
151		mode := fs.mode
152		destroyed := fs.destroyed
153		fs.mu.RUnlock()
154		if destroyed {
155			return false
156		}
157		if mode == favoritesSidebarModeEdit && fs.formTitleEntry != nil {
158			fs.formTitleEntry.GrabFocus()
159			return false
160		}
161		if fs.formURLEntry != nil {
162			fs.formURLEntry.GrabFocus()
163		}
164		return false
165	}))
166}
167
168func (fs *FavoritesSidebar) cancelManagement() bool {
169	fs.mu.Lock()
170	if fs.destroyed {
171		fs.mu.Unlock()
172		return false
173	}
174	active := fs.mode != favoritesSidebarModeNone || fs.confirmDelete
175	fs.mode = favoritesSidebarModeNone
176	fs.editingID = 0
177	fs.confirmDelete = false
178	fs.confirmDeleteID = 0
179	if active {
180		fs.setNoticeLocked("")
181	}
182	fs.mu.Unlock()
183	if fs.formBox != nil {
184		fs.formBox.SetVisible(false)
185		clearBoxChildren(fs.formBox)
186		fs.formCallbacks = nil
187		fs.formTagMatchCallbacks = nil
188	}
189	if fs.tagPromptBox != nil {
190		fs.hideTagPrompt()
191	}
192	fs.rebuildList()
193	return active
194}
195
196func (fs *FavoritesSidebar) submitForm() bool {
197	if fs == nil {
198		return false
199	}
200	fs.mu.RLock()
201	mode := fs.mode
202	id := fs.editingID
203	uc := fs.favoritesUC
204	ctx := fs.ctx
205	fs.mu.RUnlock()
206	if uc == nil || (mode != favoritesSidebarModeAdd && mode != favoritesSidebarModeEdit) {
207		return false
208	}
209	url, title, shortcutText := fs.formValues()
210	tags := fs.formTagIDsSnapshot()
211	if mode == favoritesSidebarModeAdd {
212		key, err := parseShortcut(shortcutText)
213		if err != nil {
214			fs.setNotice(err.Error())
215			return true
216		}
217		input := dto.FavoriteCreateInput{URL: url, Title: title, Tags: tags}
218		fav, err := uc.AddFavorite(ctx, input)
219		if err != nil {
220			fs.setNotice(err.Error())
221			return true
222		}
223		if key != nil && fav != nil {
224			if err := uc.SetShortcut(ctx, fav.ID, key); err != nil {
225				fs.cancelManagement()
226				fs.startLoad()
227				fs.setNotice("Added favorite, but failed to set shortcut: " + err.Error())
228				return true
229			}
230		}
231	} else {
232		key, err := parseShortcut(shortcutText)
233		if err != nil {
234			fs.setNotice(err.Error())
235			return true
236		}
237		_, err = uc.UpdateFavorite(ctx, dto.FavoriteUpdateInput{ID: id, Title: title, ShortcutKey: key, ShortcutKeySet: true})
238		if err != nil {
239			fs.setNotice(err.Error())
240			return true
241		}
242		if !fs.updateFavoriteTags(id, tags) {
243			return true
244		}
245	}
246	fs.cancelManagement()
247	fs.startLoad()
248	return true
249}
250
251func (fs *FavoritesSidebar) enterShortcutMode() {
252	fs.setModeNotice(favoritesSidebarModeShortcut, "Shortcut mode: press 1-9 to assign, Backspace/Delete to clear")
253}
254
255func (fs *FavoritesSidebar) setModeNotice(mode favoritesSidebarMode, notice string) {
256	fs.mu.Lock()
257	if !fs.destroyed {
258		fs.mode = mode
259		fs.confirmDelete = false
260		fs.confirmDeleteID = 0
261		fs.setNoticeLocked(notice)
262	}
263	fs.mu.Unlock()
264	fs.rebuildList()
265}
266
267func (fs *FavoritesSidebar) setShortcutKey(key *int) bool {
268	fav := fs.selectedFavorite()
269	if fav == nil {
270		fs.setNotice("Select a favorite for shortcut")
271		return true
272	}
273	fs.mu.RLock()
274	uc := fs.favoritesUC
275	ctx := fs.ctx
276	fs.mu.RUnlock()
277	if uc == nil {
278		return true
279	}
280	if err := uc.SetShortcut(ctx, fav.ID, key); err != nil {
281		fs.setNotice(err.Error())
282		return true
283	}
284	fs.startLoad()
285	return true
286}
287
288func (fs *FavoritesSidebar) requestDeleteConfirmation() bool {
289	fav := fs.selectedFavorite()
290	if fav == nil {
291		fs.setNotice("Select a favorite to delete")
292		return true
293	}
294	fs.mu.Lock()
295	if !fs.destroyed {
296		fs.confirmDelete = true
297		fs.confirmDeleteID = fav.ID
298		fs.setNoticeLocked("Delete selected favorite? Press Delete again to confirm, Esc to cancel.")
299	}
300	fs.mu.Unlock()
301	fs.rebuildList()
302	return true
303}
304
305func (fs *FavoritesSidebar) formModeActive() bool {
306	if fs == nil {
307		return false
308	}
309	fs.mu.RLock()
310	defer fs.mu.RUnlock()
311	return fs.mode == favoritesSidebarModeAdd || fs.mode == favoritesSidebarModeEdit
312}
313
314func (fs *FavoritesSidebar) confirmDeleteActive() bool {
315	if fs == nil {
316		return false
317	}
318	fs.mu.RLock()
319	defer fs.mu.RUnlock()
320	return fs.confirmDelete
321}
322
323func (fs *FavoritesSidebar) confirmDeleteFavorite() bool {
324	fav := fs.selectedFavorite()
325	if fav == nil {
326		return fs.requestDeleteConfirmation()
327	}
328	fs.mu.RLock()
329	confirmed := fs.confirmDelete
330	confirmedID := fs.confirmDeleteID
331	uc := fs.favoritesUC
332	ctx := fs.ctx
333	fs.mu.RUnlock()
334	if !confirmed || confirmedID != fav.ID {
335		return fs.requestDeleteConfirmation()
336	}
337	if uc == nil {
338		return true
339	}
340	if err := uc.DeleteFavorite(ctx, fav.ID); err != nil {
341		fs.setNotice(err.Error())
342		return true
343	}
344	fs.cancelManagement()
345	fs.startLoad()
346	return true
347}
348
349func (fs *FavoritesSidebar) selectedFavorite() *entity.Favorite {
350	fs.mu.RLock()
351	defer fs.mu.RUnlock()
352	if len(fs.displayRows) == 1 {
353		return fs.displayRows[0].Favorite
354	}
355	idx := firstSelectableIndex(fs.displayRows)
356	if fs.listBox != nil {
357		if row := fs.listBox.GetSelectedRow(); row != nil {
358			idx = row.GetIndex()
359		}
360	}
361	if idx >= 0 && idx < len(fs.displayRows) {
362		return fs.displayRows[idx].Favorite
363	}
364	return nil
365}
366
367func (fs *FavoritesSidebar) setNotice(notice string) {
368	fs.mu.Lock()
369	if !fs.destroyed {
370		fs.setNoticeLocked(notice)
371	}
372	fs.mu.Unlock()
373	fs.rebuildList()
374}
375
376func (fs *FavoritesSidebar) formValues() (string, string, string) {
377	url, title, shortcut := fs.formURL, fs.formTitle, fs.formShortcut
378	if fs.formURLEntry != nil {
379		url = fs.formURLEntry.GetText()
380	}
381	if fs.formTitleEntry != nil {
382		title = fs.formTitleEntry.GetText()
383	}
384	if fs.formShortcutEntry != nil {
385		shortcut = fs.formShortcutEntry.GetText()
386	}
387	return strings.TrimSpace(url), strings.TrimSpace(title), strings.TrimSpace(shortcut)
388}
389
390func parseShortcut(text string) (*int, error) {
391	text = strings.TrimSpace(text)
392	if text == "" {
393		return nil, nil
394	}
395	v, err := strconv.Atoi(text)
396	if err != nil || v < 1 || v > 9 {
397		return nil, fmt.Errorf("invalid shortcut %q (use 1-9)", text)
398	}
399	return &v, nil
400}

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