v0.2.1 c0399be

glib RawHistoryFolder

1package glib
2
3import (
4	"fmt"
5	"reflect"
6	"sync"
7
8	"github.com/ebitengine/purego"
9	"github.com/bnema/puregotk/pkg/core"
10)
11
12var callbacks = struct {
13	sync.RWMutex
14	refs              map[uintptr]uintptr
15	closures          map[uintptr]interface{}
16	handlerToCallback map[uint]uintptr
17	sourceToCallback  map[uint]uintptr
18	callbackRefCount  map[uintptr]int
19}{
20	refs:              make(map[uintptr]uintptr),
21	closures:          make(map[uintptr]interface{}),
22	handlerToCallback: make(map[uint]uintptr),
23	sourceToCallback:  make(map[uint]uintptr),
24	callbackRefCount:  make(map[uintptr]int),
25}
26
27// GetCallback retrives a callback reference by value.
28// Users should not need to call this.
29func GetCallback(cbPtr uintptr) (uintptr, bool) {
30	callbacks.RLock()
31	defer callbacks.RUnlock()
32	refPtr, ok := callbacks.refs[cbPtr]
33	return refPtr, ok
34}
35
36// SaveCallback saves a reference to the callback value.
37// Users should not need to call this.
38func SaveCallback(cbPtr uintptr, refPtr uintptr) {
39	callbacks.Lock()
40	callbacks.refs[cbPtr] = refPtr
41	callbacks.Unlock()
42}
43
44// SaveCallbackWithClosure saves a reference to the callback value and retains the
45// provided closure to prevent it from being garbage collected.
46// Users should not need to call this.
47func SaveCallbackWithClosure(cbPtr uintptr, refPtr uintptr, closure interface{}) {
48	callbacks.Lock()
49	callbacks.refs[cbPtr] = refPtr
50	callbacks.closures[cbPtr] = closure
51	if _, ok := callbacks.callbackRefCount[cbPtr]; !ok {
52		callbacks.callbackRefCount[cbPtr] = 1
53	}
54	callbacks.Unlock()
55}
56
57// RemoveCallback removes a callback from the registry, allowing it to be garbage
58// collected.
59// Users should not need to call this.
60func RemoveCallback(cbPtr uintptr) {
61	callbacks.Lock()
62	for handlerID, mappedCbPtr := range callbacks.handlerToCallback {
63		if mappedCbPtr == cbPtr {
64			delete(callbacks.handlerToCallback, handlerID)
65		}
66	}
67	for sourceID, mappedCbPtr := range callbacks.sourceToCallback {
68		if mappedCbPtr == cbPtr {
69			delete(callbacks.sourceToCallback, sourceID)
70		}
71	}
72	delete(callbacks.refs, cbPtr)
73	delete(callbacks.closures, cbPtr)
74	delete(callbacks.callbackRefCount, cbPtr)
75	callbacks.Unlock()
76}
77
78// acquireCallbackRef increments callbackRefCount for cbPtr.
79// Caller must hold callbacks.Lock().
80func acquireCallbackRef(cbPtr uintptr) {
81	callbacks.callbackRefCount[cbPtr]++
82}
83
84func hasCallbackMappings(cbPtr uintptr) bool {
85	for _, mappedCbPtr := range callbacks.handlerToCallback {
86		if mappedCbPtr == cbPtr {
87			return true
88		}
89	}
90	for _, mappedCbPtr := range callbacks.sourceToCallback {
91		if mappedCbPtr == cbPtr {
92			return true
93		}
94	}
95	return false
96}
97
98// releaseCallbackRef decrements callbackRefCount for cbPtr and removes callback
99// data when it reaches zero.
100// Caller must hold callbacks.Lock().
101// Handler/source mappings to cbPtr are expected to be removed or replaced by
102// the caller (RemoveCallbackByHandler, RemoveCallbackBySource,
103// SaveHandlerMapping, SaveSourceMapping).
104func releaseCallbackRef(cbPtr uintptr) {
105	count, ok := callbacks.callbackRefCount[cbPtr]
106	if !ok {
107		return
108	}
109
110	count--
111	if count > 0 {
112		callbacks.callbackRefCount[cbPtr] = count
113		return
114	}
115
116	delete(callbacks.callbackRefCount, cbPtr)
117	delete(callbacks.refs, cbPtr)
118	delete(callbacks.closures, cbPtr)
119}
120
121// SaveHandlerMapping records a signal handler ID → callback pointer mapping
122// so that DisconnectSignal can clean up the callback registry.
123func SaveHandlerMapping(handlerID uint, cbPtr uintptr) {
124	if handlerID == 0 {
125		return
126	}
127
128	callbacks.Lock()
129	defer callbacks.Unlock()
130	if prevCbPtr, ok := callbacks.handlerToCallback[handlerID]; ok {
131		if prevCbPtr == cbPtr {
132			return
133		}
134		releaseCallbackRef(prevCbPtr)
135		if !hasCallbackMappings(prevCbPtr) {
136			releaseCallbackRef(prevCbPtr)
137		}
138	}
139	callbacks.handlerToCallback[handlerID] = cbPtr
140	acquireCallbackRef(cbPtr)
141}
142
143// RemoveCallbackByHandler removes a callback from the registry using a signal handler ID.
144func RemoveCallbackByHandler(handlerID uint) {
145	callbacks.Lock()
146	if cbPtr, ok := callbacks.handlerToCallback[handlerID]; ok {
147		delete(callbacks.handlerToCallback, handlerID)
148		releaseCallbackRef(cbPtr)
149		if !hasCallbackMappings(cbPtr) {
150			releaseCallbackRef(cbPtr)
151		}
152	}
153	callbacks.Unlock()
154}
155
156// SaveSourceMapping records a source ID -> callback pointer mapping.
157func SaveSourceMapping(sourceID uint, cbPtr uintptr) {
158	if sourceID == 0 {
159		return
160	}
161
162	callbacks.Lock()
163	defer callbacks.Unlock()
164	if prevCbPtr, ok := callbacks.sourceToCallback[sourceID]; ok {
165		if prevCbPtr == cbPtr {
166			return
167		}
168		releaseCallbackRef(prevCbPtr)
169		if !hasCallbackMappings(prevCbPtr) {
170			releaseCallbackRef(prevCbPtr)
171		}
172	}
173	callbacks.sourceToCallback[sourceID] = cbPtr
174	acquireCallbackRef(cbPtr)
175}
176
177// RemoveCallbackBySource removes a callback mapping using a source ID.
178func RemoveCallbackBySource(sourceID uint) {
179	callbacks.Lock()
180	if cbPtr, ok := callbacks.sourceToCallback[sourceID]; ok {
181		delete(callbacks.sourceToCallback, sourceID)
182		releaseCallbackRef(cbPtr)
183		if !hasCallbackMappings(cbPtr) {
184			releaseCallbackRef(cbPtr)
185		}
186	}
187	callbacks.Unlock()
188}
189
190// ---------------------------------------------------------------------------
191// Source callback trampoline
192//
193// GLib source functions (IdleAdd, TimeoutAdd, etc.) previously allocated a new
194// purego callback slot for every invocation. purego has a hard limit of 2000
195// slots, so long-running programs that schedule many one-shot idle/timeout
196// callbacks would exhaust the pool and panic.
197//
198// The trampoline uses a single purego callback that dispatches through a
199// Go-side map keyed by an opaque ID passed as GLib's user_data pointer.
200// This means all IdleAdd/TimeoutAdd calls share ONE purego slot regardless
201// of how many are outstanding.
202// ---------------------------------------------------------------------------
203
204// sourceEntry holds a registered GLib source callback.
205type sourceEntry struct {
206	fn   SourceFunc
207	once bool // if true, automatically remove after first call (SourceOnceFunc semantics)
208}
209
210var sourceTrampolines = struct {
211	sync.Mutex
212	nextID         uintptr
213	funcs          map[uintptr]*sourceEntry
214	sourceToDataID map[uint]uintptr // GLib source ID → trampoline data ID
215}{
216	funcs:          make(map[uintptr]*sourceEntry),
217	sourceToDataID: make(map[uint]uintptr),
218}
219
220// sourceTrampolineCb is the single purego callback shared by all source functions.
221// GLib calls it with the user_data we provided (our map key).
222var sourceTrampolineCb uintptr
223
224// sourceTrampolineOnceCb is the single purego callback for SourceOnceFunc sources
225// (IdleAddOnce, TimeoutAddOnce). These have signature func(uintptr) with no return.
226var sourceTrampolineOnceCb uintptr
227
228func initSourceTrampoline() {
229	fn := func(id uintptr) uintptr {
230		sourceTrampolines.Lock()
231		entry, ok := sourceTrampolines.funcs[id]
232		if !ok {
233			sourceTrampolines.Unlock()
234			return 0 // SOURCE_REMOVE — entry already cleaned up (e.g. by SourceRemove)
235		}
236		cb := entry.fn
237		sourceTrampolines.Unlock()
238
239		result := cb(0)
240
241		if !result {
242			sourceTrampolines.Lock()
243			delete(sourceTrampolines.funcs, id)
244			// Also clean up the reverse mapping (source ID → data ID).
245			for sid, did := range sourceTrampolines.sourceToDataID {
246				if did == id {
247					delete(sourceTrampolines.sourceToDataID, sid)
248					break
249				}
250			}
251			sourceTrampolines.Unlock()
252		}
253		if result {
254			return 1
255		}
256		return 0
257	}
258	sourceTrampolineCb = purego.NewCallback(fn)
259
260	onceFn := func(id uintptr) {
261		sourceTrampolines.Lock()
262		entry, ok := sourceTrampolines.funcs[id]
263		if !ok {
264			sourceTrampolines.Unlock()
265			return
266		}
267		cb := entry.fn
268		delete(sourceTrampolines.funcs, id)
269		// Also clean up the reverse mapping.
270		for sid, did := range sourceTrampolines.sourceToDataID {
271			if did == id {
272				delete(sourceTrampolines.sourceToDataID, sid)
273				break
274			}
275		}
276		sourceTrampolines.Unlock()
277
278		cb(0)
279	}
280	sourceTrampolineOnceCb = purego.NewCallback(onceFn)
281}
282
283// registerSourceFunc stores a SourceFunc in the trampoline map and returns
284// the trampoline callback pointer and the user_data key.
285func registerSourceFunc(fn *SourceFunc, once bool) (trampolineCb uintptr, userData uintptr) {
286	if fn == nil {
287		return 0, 0
288	}
289	sourceTrampolines.Lock()
290	sourceTrampolines.nextID++
291	id := sourceTrampolines.nextID
292	sourceTrampolines.funcs[id] = &sourceEntry{fn: *fn, once: once}
293	sourceTrampolines.Unlock()
294	if once {
295		return sourceTrampolineOnceCb, id
296	}
297	return sourceTrampolineCb, id
298}
299
300// registerSourceOnceFunc stores a SourceOnceFunc in the trampoline map and
301// returns the trampoline callback pointer and the user_data key.
302func registerSourceOnceFunc(fn *SourceOnceFunc) (trampolineCb uintptr, userData uintptr) {
303	if fn == nil {
304		return 0, 0
305	}
306	// Wrap SourceOnceFunc as SourceFunc so the entry type is uniform.
307	wrapped := SourceFunc(func(data uintptr) bool {
308		(*fn)(data)
309		return false
310	})
311	return registerSourceFunc(&wrapped, true)
312}
313
314// saveSourceTrampolineMapping records the GLib source ID → trampoline data ID
315// mapping so that SourceRemove can clean up the trampoline entry.
316func saveSourceTrampolineMapping(sourceID uint, dataID uintptr) {
317	if sourceID == 0 {
318		return
319	}
320	sourceTrampolines.Lock()
321	sourceTrampolines.sourceToDataID[sourceID] = dataID
322	sourceTrampolines.Unlock()
323}
324
325// removeSourceTrampolineBySourceID cleans up trampoline state when a GLib
326// source is removed via SourceRemove (before the callback fires).
327func removeSourceTrampolineBySourceID(sourceID uint) {
328	sourceTrampolines.Lock()
329	if dataID, ok := sourceTrampolines.sourceToDataID[sourceID]; ok {
330		delete(sourceTrampolines.sourceToDataID, sourceID)
331		delete(sourceTrampolines.funcs, dataID)
332	}
333	sourceTrampolines.Unlock()
334}
335
336// UnrefCallback removes puregotk's bookkeeping for a callback created from
337// a function pointer.
338//
339// NOTE: Upstream purego does not support releasing callback slots after
340// purego.NewCallback, so this only clears puregotk's local registry state.
341func UnrefCallback(fnPtr interface{}) error {
342	return unrefCallback(fnPtr)
343}
344
345// NewCallback converts a pointer-to-function into a C callback pointer.
346// It reuses an existing callback for the same function pointer via the
347// local callback registry.
348func NewCallback(fnPtr interface{}) uintptr {
349	val := reflect.ValueOf(fnPtr)
350	if val.IsNil() {
351		panic("purego: function must not be nil")
352	}
353	if val.Kind() != reflect.Ptr || val.Elem().Kind() != reflect.Func {
354		panic("purego: the type must be a function pointer but was not")
355	}
356
357	cbPtr := val.Pointer()
358	if refPtr, ok := GetCallback(cbPtr); ok {
359		return refPtr
360	}
361
362	refPtr := purego.NewCallback(val.Elem().Interface())
363	SaveCallbackWithClosure(cbPtr, refPtr, fnPtr)
364	return refPtr
365}
366
367// NewCallbackNullable is an alias to purego.NewCallback that returns a null pointer for null functions
368func NewCallbackNullable(fn interface{}) uintptr {
369	val := reflect.ValueOf(fn)
370	if val.IsNil() {
371		return 0
372	}
373
374	return NewCallback(fn)
375}
376
377func init() {
378	initSourceTrampoline()
379}
380
381func (e *Error) Error() string {
382	return fmt.Sprintf("Gtk reported an error with message: '%s', domain: '%v' and code: '%v'", e.MessageGo(), e.Domain, e.Code)
383}
384
385func (e *Error) MessageGo() string {
386	return core.GoString(e.Message)
387}

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 1

Tags 102

↑↓ moveenter openesc close