v0.2.1 c0399be

more.go RawHistoryFolder

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

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