v0.30.0 eb3068f

find_controller.go RawHistoryFolder

1package cef
2
3import (
4	"sync"
5
6	purecef "github.com/bnema/purego-cef/cef"
7
8	"github.com/bnema/dumber/internal/application/port"
9)
10
11// Compile-time interface check.
12var _ port.FindController = (*cefFindController)(nil)
13
14// cefFindController implements port.FindController using CEF's BrowserHost.Find API.
15type cefFindController struct {
16	mu        sync.RWMutex
17	host      purecef.BrowserHost
18	searchTxt string
19	matchCase int32 // cached from most recent Search call
20
21	// counting tracks whether the current search is a CountMatches request
22	// so that handleFindResult dispatches to the right callbacks.
23	counting bool
24
25	// Signal callbacks keyed by unique IDs.
26	nextSignalID uint
27	onFound      map[uint]func(matchCount uint)
28	onFailed     map[uint]func()
29	onCounted    map[uint]func(matchCount uint)
30}
31
32func newFindController() *cefFindController {
33	return &cefFindController{
34		onFound:   make(map[uint]func(matchCount uint)),
35		onFailed:  make(map[uint]func()),
36		onCounted: make(map[uint]func(matchCount uint)),
37	}
38}
39
40func (fc *cefFindController) setHost(host purecef.BrowserHost) {
41	fc.mu.Lock()
42	fc.host = host
43	fc.mu.Unlock()
44}
45
46func (fc *cefFindController) Search(text string, opts port.FindOptions, _ uint) {
47	fc.mu.Lock()
48	fc.searchTxt = text
49	fc.counting = false
50	mc := matchCaseFromOpts(opts)
51	fc.matchCase = mc
52	host := fc.host
53	fc.mu.Unlock()
54
55	if host == nil || text == "" {
56		return
57	}
58	host.Find(text, 1, mc, 0) // forward=1, findnext=0 (new search)
59}
60
61// CountMatches starts a search whose results are dispatched to onCounted callbacks.
62func (fc *cefFindController) CountMatches(text string, opts port.FindOptions, _ uint) {
63	fc.mu.Lock()
64	fc.searchTxt = text
65	fc.counting = true
66	mc := matchCaseFromOpts(opts)
67	fc.matchCase = mc
68	host := fc.host
69	fc.mu.Unlock()
70
71	if host == nil || text == "" {
72		return
73	}
74	host.Find(text, 1, mc, 0)
75}
76
77// SearchNext continues the search in the forward direction.
78func (fc *cefFindController) SearchNext() {
79	fc.mu.RLock()
80	host := fc.host
81	text := fc.searchTxt
82	mc := fc.matchCase
83	fc.mu.RUnlock()
84
85	if host == nil || text == "" {
86		return
87	}
88	host.Find(text, 1, mc, 1) // forward=1, findnext=1
89}
90
91// SearchPrevious continues the search in the backward direction.
92func (fc *cefFindController) SearchPrevious() {
93	fc.mu.RLock()
94	host := fc.host
95	text := fc.searchTxt
96	mc := fc.matchCase
97	fc.mu.RUnlock()
98
99	if host == nil || text == "" {
100		return
101	}
102	host.Find(text, 0, mc, 1) // forward=0, findnext=1
103}
104
105func (fc *cefFindController) SearchFinish() {
106	fc.mu.Lock()
107	host := fc.host
108	fc.searchTxt = ""
109	fc.mu.Unlock()
110
111	if host != nil {
112		host.StopFinding(1)
113	}
114}
115
116func (fc *cefFindController) GetSearchText() string {
117	fc.mu.RLock()
118	defer fc.mu.RUnlock()
119	return fc.searchTxt
120}
121
122// OnFoundText registers a callback fired when matches are found.
123func (fc *cefFindController) OnFoundText(callback func(matchCount uint)) uint {
124	if callback == nil {
125		return 0
126	}
127	fc.mu.Lock()
128	defer fc.mu.Unlock()
129	fc.nextSignalID++
130	id := fc.nextSignalID
131	fc.onFound[id] = callback
132	return id
133}
134
135// OnFailedToFindText registers a callback fired when no matches are found.
136func (fc *cefFindController) OnFailedToFindText(callback func()) uint {
137	if callback == nil {
138		return 0
139	}
140	fc.mu.Lock()
141	defer fc.mu.Unlock()
142	fc.nextSignalID++
143	id := fc.nextSignalID
144	fc.onFailed[id] = callback
145	return id
146}
147
148// OnCountedMatches registers a callback fired with the final match count.
149func (fc *cefFindController) OnCountedMatches(callback func(matchCount uint)) uint {
150	if callback == nil {
151		return 0
152	}
153	fc.mu.Lock()
154	defer fc.mu.Unlock()
155	fc.nextSignalID++
156	id := fc.nextSignalID
157	fc.onCounted[id] = callback
158	return id
159}
160
161// DisconnectSignal removes a previously registered callback.
162func (fc *cefFindController) DisconnectSignal(id uint) {
163	if id == 0 {
164		return
165	}
166	fc.mu.Lock()
167	defer fc.mu.Unlock()
168	delete(fc.onFound, id)
169	delete(fc.onFailed, id)
170	delete(fc.onCounted, id)
171}
172
173// handleFindResult is called by the FindHandler when CEF reports find results.
174func (fc *cefFindController) handleFindResult(_, count, _, finalUpdate int32) {
175	fc.mu.RLock()
176	counting := fc.counting
177
178	// Snapshot callback maps so we can invoke them outside the lock.
179	var foundCBs []func(matchCount uint)
180	var failedCBs []func()
181	var countedCBs []func(matchCount uint)
182
183	if count > 0 && !counting {
184		foundCBs = make([]func(matchCount uint), 0, len(fc.onFound))
185		for _, cb := range fc.onFound {
186			foundCBs = append(foundCBs, cb)
187		}
188	}
189	if count == 0 && finalUpdate != 0 && !counting {
190		failedCBs = make([]func(), 0, len(fc.onFailed))
191		for _, cb := range fc.onFailed {
192			failedCBs = append(failedCBs, cb)
193		}
194	}
195	if finalUpdate != 0 && counting {
196		countedCBs = make([]func(matchCount uint), 0, len(fc.onCounted))
197		for _, cb := range fc.onCounted {
198			countedCBs = append(countedCBs, cb)
199		}
200	}
201	fc.mu.RUnlock()
202
203	var safeCount uint
204	if count > 0 {
205		safeCount = uint(count)
206	}
207	for _, cb := range foundCBs {
208		cb(safeCount)
209	}
210	for _, cb := range failedCBs {
211		cb()
212	}
213	for _, cb := range countedCBs {
214		cb(safeCount)
215	}
216}
217
218// matchCaseFromOpts converts FindOptions to CEF's matchcase parameter.
219// CEF: matchcase=1 means case-sensitive, matchcase=0 means case-insensitive.
220func matchCaseFromOpts(opts port.FindOptions) int32 {
221	if opts.CaseInsensitive {
222		return 0
223	}
224	return 1
225}

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