v0.7.1 ff2c2cc

objc_runtime_darwin.go RawHistoryFolder

1// SPDX-License-Identifier: Apache-2.0
2// SPDX-FileCopyrightText: 2022 The Ebitengine Authors
3
4// Package objc is a low-level pure Go objective-c runtime. This package is easy to use incorrectly, so it is best
5// to use a wrapper that provides the functionality you need in a safer way.
6package objc
7
8import (
9	"errors"
10	"fmt"
11	"math"
12	"reflect"
13	"regexp"
14	"runtime"
15	"unicode"
16	"unsafe"
17
18	"github.com/ebitengine/purego"
19)
20
21// TODO: support try/catch?
22// https://stackoverflow.com/questions/7062599/example-of-how-objective-cs-try-catch-implementation-is-executed-at-runtime
23var (
24	objc_msgSend_fn             uintptr
25	objc_msgSend_stret_fn       uintptr
26	objc_msgSend                func(obj ID, cmd SEL, args ...interface{}) ID
27	objc_msgSendSuper2_fn       uintptr
28	objc_msgSendSuper2_stret_fn uintptr
29	objc_msgSendSuper2          func(super *objc_super, cmd SEL, args ...interface{}) ID
30	objc_getClass               func(name string) Class
31	objc_getProtocol            func(name string) *Protocol
32	objc_allocateClassPair      func(super Class, name string, extraBytes uintptr) Class
33	objc_registerClassPair      func(class Class)
34	sel_registerName            func(name string) SEL
35	class_getSuperclass         func(class Class) Class
36	class_getInstanceVariable   func(class Class, name string) Ivar
37	class_getInstanceSize       func(class Class) uintptr
38	class_addMethod             func(class Class, name SEL, imp IMP, types string) bool
39	class_addIvar               func(class Class, name string, size uintptr, alignment uint8, types string) bool
40	class_addProtocol           func(class Class, protocol *Protocol) bool
41	ivar_getOffset              func(ivar Ivar) uintptr
42	ivar_getName                func(ivar Ivar) string
43	object_getClass             func(obj ID) Class
44	object_getIvar              func(obj ID, ivar Ivar) ID
45	object_setIvar              func(obj ID, ivar Ivar, value ID)
46	protocol_getName            func(protocol *Protocol) string
47	protocol_isEqual            func(p *Protocol, p2 *Protocol) bool
48)
49
50func init() {
51	objc, err := purego.Dlopen("/usr/lib/libobjc.A.dylib", purego.RTLD_GLOBAL)
52	if err != nil {
53		panic(fmt.Errorf("objc: %w", err))
54	}
55	objc_msgSend_fn, err = purego.Dlsym(objc, "objc_msgSend")
56	if err != nil {
57		panic(fmt.Errorf("objc: %w", err))
58	}
59	if runtime.GOARCH == "amd64" {
60		objc_msgSend_stret_fn, err = purego.Dlsym(objc, "objc_msgSend_stret")
61		if err != nil {
62			panic(fmt.Errorf("objc: %w", err))
63		}
64		objc_msgSendSuper2_stret_fn, err = purego.Dlsym(objc, "objc_msgSendSuper2_stret")
65		if err != nil {
66			panic(fmt.Errorf("objc: %w", err))
67		}
68	}
69	purego.RegisterFunc(&objc_msgSend, objc_msgSend_fn)
70	objc_msgSendSuper2_fn, err = purego.Dlsym(objc, "objc_msgSendSuper2")
71	if err != nil {
72		panic(fmt.Errorf("objc: %w", err))
73	}
74	purego.RegisterFunc(&objc_msgSendSuper2, objc_msgSendSuper2_fn)
75	purego.RegisterLibFunc(&object_getClass, objc, "object_getClass")
76	purego.RegisterLibFunc(&objc_getClass, objc, "objc_getClass")
77	purego.RegisterLibFunc(&objc_getProtocol, objc, "objc_getProtocol")
78	purego.RegisterLibFunc(&objc_allocateClassPair, objc, "objc_allocateClassPair")
79	purego.RegisterLibFunc(&objc_registerClassPair, objc, "objc_registerClassPair")
80	purego.RegisterLibFunc(&sel_registerName, objc, "sel_registerName")
81	purego.RegisterLibFunc(&class_getSuperclass, objc, "class_getSuperclass")
82	purego.RegisterLibFunc(&class_getInstanceVariable, objc, "class_getInstanceVariable")
83	purego.RegisterLibFunc(&class_addMethod, objc, "class_addMethod")
84	purego.RegisterLibFunc(&class_addIvar, objc, "class_addIvar")
85	purego.RegisterLibFunc(&class_addProtocol, objc, "class_addProtocol")
86	purego.RegisterLibFunc(&class_getInstanceSize, objc, "class_getInstanceSize")
87	purego.RegisterLibFunc(&ivar_getOffset, objc, "ivar_getOffset")
88	purego.RegisterLibFunc(&ivar_getName, objc, "ivar_getName")
89	purego.RegisterLibFunc(&protocol_getName, objc, "protocol_getName")
90	purego.RegisterLibFunc(&protocol_isEqual, objc, "protocol_isEqual")
91	purego.RegisterLibFunc(&object_getIvar, objc, "object_getIvar")
92	purego.RegisterLibFunc(&object_setIvar, objc, "object_setIvar")
93}
94
95// ID is an opaque pointer to some Objective-C object
96type ID uintptr
97
98// Class returns the class of the object.
99func (id ID) Class() Class {
100	return object_getClass(id)
101}
102
103// Send is a convenience method for sending messages to objects. This function takes a SEL
104// instead of a string since RegisterName grabs the global Objective-C lock. It is best to cache the result
105// of RegisterName.
106func (id ID) Send(sel SEL, args ...interface{}) ID {
107	return objc_msgSend(id, sel, args...)
108}
109
110// GetIvar reads the value of an instance variable in an object.
111func (id ID) GetIvar(ivar Ivar) ID {
112	return object_getIvar(id, ivar)
113}
114
115// SetIvar sets the value of an instance variable in an object.
116func (id ID) SetIvar(ivar Ivar, value ID) {
117	object_setIvar(id, ivar, value)
118}
119
120// keep in sync with func.go
121const maxRegAllocStructSize = 16
122
123// Send is a convenience method for sending messages to objects that can return any type.
124// This function takes a SEL instead of a string since RegisterName grabs the global Objective-C lock.
125// It is best to cache the result of RegisterName.
126func Send[T any](id ID, sel SEL, args ...any) T {
127	var fn func(id ID, sel SEL, args ...any) T
128	var zero T
129	if runtime.GOARCH == "amd64" &&
130		reflect.ValueOf(zero).Kind() == reflect.Struct &&
131		reflect.ValueOf(zero).Type().Size() > maxRegAllocStructSize {
132		purego.RegisterFunc(&fn, objc_msgSend_stret_fn)
133	} else {
134		purego.RegisterFunc(&fn, objc_msgSend_fn)
135	}
136	return fn(id, sel, args...)
137}
138
139// objc_super data structure is generated by the Objective-C compiler when it encounters the super keyword
140// as the receiver of a message. It specifies the class definition of the particular superclass that should
141// be messaged.
142type objc_super struct {
143	receiver   ID
144	superClass Class
145}
146
147// SendSuper is a convenience method for sending message to object's super. This function takes a SEL
148// instead of a string since RegisterName grabs the global Objective-C lock. It is best to cache the result
149// of RegisterName.
150func (id ID) SendSuper(sel SEL, args ...interface{}) ID {
151	super := &objc_super{
152		receiver:   id,
153		superClass: id.Class(),
154	}
155	return objc_msgSendSuper2(super, sel, args...)
156}
157
158// SendSuper is a convenience method for sending message to object's super that can return any type.
159// This function takes a SEL instead of a string since RegisterName grabs the global Objective-C lock.
160// It is best to cache the result of RegisterName.
161func SendSuper[T any](id ID, sel SEL, args ...any) T {
162	super := &objc_super{
163		receiver:   id,
164		superClass: id.Class(),
165	}
166	var fn func(objcSuper *objc_super, sel SEL, args ...any) T
167	var zero T
168	if runtime.GOARCH == "amd64" &&
169		reflect.ValueOf(zero).Kind() == reflect.Struct &&
170		reflect.ValueOf(zero).Type().Size() > maxRegAllocStructSize {
171		purego.RegisterFunc(&fn, objc_msgSendSuper2_stret_fn)
172	} else {
173		purego.RegisterFunc(&fn, objc_msgSendSuper2_fn)
174	}
175	return fn(super, sel, args...)
176}
177
178// SEL is an opaque type that represents a method selector
179type SEL uintptr
180
181// RegisterName registers a method with the Objective-C runtime system, maps the method name to a selector,
182// and returns the selector value. This function grabs the global Objective-c lock. It is best the cache the
183// result of this function.
184func RegisterName(name string) SEL {
185	return sel_registerName(name)
186}
187
188// Class is an opaque type that represents an Objective-C class.
189type Class uintptr
190
191// GetClass returns the Class object for the named class, or nil if the class is not registered with the Objective-C runtime.
192func GetClass(name string) Class {
193	return objc_getClass(name)
194}
195
196// AllocateClassPair creates a new class and metaclass. Then returns the new class, or Nil if the class could not be created
197//
198// Deprecated: use RegisterClass instead
199func AllocateClassPair(super Class, name string, extraBytes uintptr) Class {
200	return objc_allocateClassPair(super, name, extraBytes)
201}
202
203// MethodDef represents the Go function and the selector that ObjC uses to access that function.
204type MethodDef struct {
205	Cmd SEL
206	Fn  any
207}
208
209// IvarAttrib is the attribute that an ivar has. It affects if and which methods are automatically
210// generated when creating a class with RegisterClass. See [Apple Docs] for an understanding of these attributes.
211// The fields are still accessible using objc.GetIvar and objc.SetIvar regardless of the value of IvarAttrib.
212//
213// Take for example this Objective-C code:
214//
215//	@property (readwrite) float value;
216//
217// In Go, the functions can be accessed as followed:
218//
219//	var value = purego.Send[float32](id, purego.RegisterName("value"))
220//	id.Send(purego.RegisterName("setValue:"), 3.46)
221//
222// [Apple Docs]: https://developer.apple.com/library/archive/documentation/Cocoa/Conceptual/ObjectiveC/Chapters/ocProperties.html
223type IvarAttrib int
224
225const (
226	ReadOnly IvarAttrib = 1 << iota
227	ReadWrite
228)
229
230// FieldDef is a definition of a field to add to an Objective-C class.
231// The name of the field is what will be used to access it through the Ivar. If the type is bool
232// the name cannot start with `is` since a getter will be generated with the name `isBoolName`.
233// The name also cannot contain any spaces.
234// The type is the Go equivalent type of the Ivar.
235// Attribute determines if a getter and or setter method is generated for this field.
236type FieldDef struct {
237	Name      string
238	Type      reflect.Type
239	Attribute IvarAttrib
240}
241
242// ivarRegex checks to make sure the Ivar is correctly formatted
243var ivarRegex = regexp.MustCompile("[a-z_][a-zA-Z0-9_]*")
244
245// RegisterClass takes the name of the class to create, the superclass, a list of protocols this class
246// implements, a list of fields this class has and a list of methods. It returns the created class or an error
247// describing what went wrong.
248func RegisterClass(name string, superClass Class, protocols []*Protocol, ivars []FieldDef, methods []MethodDef) (Class, error) {
249	class := objc_allocateClassPair(superClass, name, 0)
250	if class == 0 {
251		return 0, fmt.Errorf("objc: failed to create class with name '%s'", name)
252	}
253	// Add Protocols
254	for _, p := range protocols {
255		if !class.AddProtocol(p) {
256			return 0, fmt.Errorf("objc: couldn't add Protocol %s", protocol_getName(p))
257		}
258	}
259	// Add exported methods based on the selectors returned from ClassDef(string) SEL
260	for idx, def := range methods {
261		imp, err := func() (imp IMP, err error) {
262			defer func() {
263				if r := recover(); r != nil {
264					err = fmt.Errorf("objc: failed to create IMP: %s", r)
265				}
266			}()
267			return NewIMP(def.Fn), nil
268		}()
269		if err != nil {
270			return 0, fmt.Errorf("objc: couldn't add Method at index %d: %w", idx, err)
271		}
272		encoding, err := encodeFunc(def.Fn)
273		if err != nil {
274			return 0, fmt.Errorf("objc: couldn't add Method at index %d: %w", idx, err)
275		}
276		if !class.AddMethod(def.Cmd, imp, encoding) {
277			return 0, fmt.Errorf("objc: couldn't add Method at index %d", idx)
278		}
279	}
280	// Add Ivars
281	for _, instVar := range ivars {
282		ivar := instVar
283		if !ivarRegex.MatchString(ivar.Name) {
284			return 0, fmt.Errorf("objc: Ivar must start with a lowercase letter and only contain ASCII letters and numbers: '%s'", ivar.Name)
285		}
286		size := ivar.Type.Size()
287		alignment := uint8(math.Log2(float64(ivar.Type.Align())))
288		enc, err := encodeType(ivar.Type, false)
289		if err != nil {
290			return 0, fmt.Errorf("objc: couldn't add Ivar %s: %w", ivar.Name, err)
291		}
292		if !class_addIvar(class, ivar.Name, size, alignment, enc) {
293			return 0, fmt.Errorf("objc: couldn't add Ivar %s", ivar.Name)
294		}
295		offset := class.InstanceVariable(ivar.Name).Offset()
296		switch ivar.Attribute {
297		case ReadWrite:
298			ty := reflect.FuncOf(
299				[]reflect.Type{
300					reflect.TypeOf(ID(0)), reflect.TypeOf(SEL(0)), ivar.Type,
301				},
302				nil, false,
303			)
304			var encoding string
305			if encoding, err = encodeFunc(reflect.New(ty).Elem().Interface()); err != nil {
306				return 0, fmt.Errorf("objc: failed to create read method for '%s': %w", ivar.Name, err)
307			}
308			val := reflect.MakeFunc(ty, func(args []reflect.Value) (results []reflect.Value) {
309				// on entry the first and second arguments are ID and SEL followed by the value
310				if len(args) != 3 {
311					panic(fmt.Sprintf("objc: incorrect number of args. expected 3 got %d", len(args)))
312				}
313				// The following reflect code does the equivalent of this:
314				//
315				//	((*struct {
316				//		Padding [offset]byte
317				//		Value int
318				//	})(unsafe.Pointer(args[0].Interface().(ID)))).v = 123
319				//
320				// However, since the type of the variable is unknown reflection is used to actually assign the value
321				id := args[0].Interface().(ID)
322				ptr := *(*unsafe.Pointer)(unsafe.Pointer(&id)) // circumvent go vet
323				reflect.NewAt(ivar.Type, unsafe.Add(ptr, offset)).Elem().Set(args[2])
324				return nil
325			}).Interface()
326			// this code only works for ascii but that shouldn't be a problem
327			selector := "set" + string(unicode.ToUpper(rune(ivar.Name[0]))) + ivar.Name[1:] + ":\x00"
328			class.AddMethod(RegisterName(selector), NewIMP(val), encoding)
329			fallthrough // also implement the read method
330		case ReadOnly:
331			ty := reflect.FuncOf(
332				[]reflect.Type{
333					reflect.TypeOf(ID(0)), reflect.TypeOf(SEL(0)),
334				},
335				[]reflect.Type{ivar.Type}, false,
336			)
337			var encoding string
338			if encoding, err = encodeFunc(reflect.New(ty).Elem().Interface()); err != nil {
339				return 0, fmt.Errorf("objc: failed to create read method for '%s': %w", ivar.Name, err)
340			}
341			val := reflect.MakeFunc(ty, func(args []reflect.Value) (results []reflect.Value) {
342				// on entry the first and second arguments are ID and SEL
343				if len(args) != 2 {
344					panic(fmt.Sprintf("objc: incorrect number of args. expected 2 got %d", len(args)))
345				}
346				id := args[0].Interface().(ID)
347				ptr := *(*unsafe.Pointer)(unsafe.Pointer(&id)) // circumvent go vet
348				// the variable is located at an offset from the id
349				return []reflect.Value{reflect.NewAt(ivar.Type, unsafe.Add(ptr, offset)).Elem()}
350			}).Interface()
351			if ivar.Type.Kind() == reflect.Bool {
352				// this code only works for ascii but that shouldn't be a problem
353				ivar.Name = "is" + string(unicode.ToUpper(rune(ivar.Name[0]))) + ivar.Name[1:]
354			}
355			class.AddMethod(RegisterName(ivar.Name), NewIMP(val), encoding)
356		default:
357			return 0, fmt.Errorf("objc: unknown Ivar Attribute (%d)", ivar.Attribute)
358		}
359	}
360	objc_registerClassPair(class)
361	return class, nil
362}
363
364const (
365	encId          = "@"
366	encClass       = "#"
367	encSelector    = ":"
368	encChar        = "c"
369	encUChar       = "C"
370	encShort       = "s"
371	encUShort      = "S"
372	encInt         = "i"
373	encUInt        = "I"
374	encLong        = "l"
375	encULong       = "L"
376	encFloat       = "f"
377	encDouble      = "d"
378	encBool        = "B"
379	encVoid        = "v"
380	encPtr         = "^"
381	encCharPtr     = "*"
382	encStructBegin = "{"
383	encStructEnd   = "}"
384	encUnsafePtr   = "^v"
385)
386
387// encodeType returns a string representing a type as if it was given to @encode(typ)
388// Source: https://developer.apple.com/library/archive/documentation/Cocoa/Conceptual/ObjCRuntimeGuide/Articles/ocrtTypeEncodings.html#//apple_ref/doc/uid/TP40008048-CH100
389func encodeType(typ reflect.Type, insidePtr bool) (string, error) {
390	switch typ {
391	case reflect.TypeOf(Class(0)):
392		return encClass, nil
393	case reflect.TypeOf(ID(0)):
394		return encId, nil
395	case reflect.TypeOf(SEL(0)):
396		return encSelector, nil
397	}
398
399	kind := typ.Kind()
400	switch kind {
401	case reflect.Bool:
402		return encBool, nil
403	case reflect.Int:
404		return encLong, nil
405	case reflect.Int8:
406		return encChar, nil
407	case reflect.Int16:
408		return encShort, nil
409	case reflect.Int32:
410		return encInt, nil
411	case reflect.Int64:
412		return encULong, nil
413	case reflect.Uint:
414		return encULong, nil
415	case reflect.Uint8:
416		return encUChar, nil
417	case reflect.Uint16:
418		return encUShort, nil
419	case reflect.Uint32:
420		return encUInt, nil
421	case reflect.Uint64:
422		return encULong, nil
423	case reflect.Uintptr:
424		return encPtr, nil
425	case reflect.Float32:
426		return encFloat, nil
427	case reflect.Float64:
428		return encDouble, nil
429	case reflect.Ptr:
430		enc, err := encodeType(typ.Elem(), true)
431		return encPtr + enc, err
432	case reflect.Struct:
433		if insidePtr {
434			return encStructBegin + typ.Name() + encStructEnd, nil
435		}
436		encoding := encStructBegin
437		encoding += typ.Name()
438		encoding += "="
439		for i := 0; i < typ.NumField(); i++ {
440			f := typ.Field(i)
441			tmp, err := encodeType(f.Type, false)
442			if err != nil {
443				return "", err
444			}
445			encoding += tmp
446		}
447		encoding = encStructEnd
448		return encoding, nil
449	case reflect.UnsafePointer:
450		return encUnsafePtr, nil
451	case reflect.String:
452		return encCharPtr, nil
453	}
454
455	return "", errors.New(fmt.Sprintf("unhandled/invalid kind %v typed %v", kind, typ))
456}
457
458// encodeFunc returns a functions type as if it was given to @encode(fn)
459func encodeFunc(fn interface{}) (string, error) {
460	typ := reflect.TypeOf(fn)
461	if typ.Kind() != reflect.Func {
462		return "", errors.New("not a func")
463	}
464
465	encoding := ""
466	switch typ.NumOut() {
467	case 0:
468		encoding += encVoid
469	case 1:
470		tmp, err := encodeType(typ.Out(0), false)
471		if err != nil {
472			return "", err
473		}
474		encoding += tmp
475	default:
476		return "", errors.New("too many output parameters")
477	}
478
479	if typ.NumIn() < 2 {
480		return "", errors.New("func doesn't take ID and SEL as its first two parameters")
481	}
482
483	encoding += encId
484
485	for i := 1; i < typ.NumIn(); i++ {
486		tmp, err := encodeType(typ.In(i), false)
487		if err != nil {
488			return "", err
489		}
490		encoding += tmp
491	}
492	return encoding, nil
493}
494
495// SuperClass returns the superclass of a class.
496// You should usually use NSObject‘s superclass method instead of this function.
497func (c Class) SuperClass() Class {
498	return class_getSuperclass(c)
499}
500
501// AddMethod adds a new method to a class with a given name and implementation.
502// The types argument is a string containing the mapping of parameters and return type.
503// Since the function must take at least two arguments—self and _cmd, the second and third
504// characters must be “@:” (the first character is the return type).
505func (c Class) AddMethod(name SEL, imp IMP, types string) bool {
506	return class_addMethod(c, name, imp, types)
507}
508
509// AddIvar adds a new instance variable to a class.
510// It may only be called after AllocateClassPair and before Register.
511// Adding an instance variable to an existing class is not supported.
512// The class must not be a metaclass. Adding an instance variable to a metaclass is not supported.
513// It takes the instance of the type of the Ivar and a string representing the type.
514//
515// Deprecated: use RegisterClass instead
516func (c Class) AddIvar(name string, ty interface{}, types string) bool {
517	typeOf := reflect.TypeOf(ty)
518	size := typeOf.Size()
519	alignment := uint8(math.Log2(float64(typeOf.Align())))
520	return class_addIvar(c, name, size, alignment, types)
521}
522
523// AddProtocol adds a protocol to a class.
524// Returns true if the protocol was added successfully, otherwise false (for example,
525// the class already conforms to that protocol).
526func (c Class) AddProtocol(protocol *Protocol) bool {
527	return class_addProtocol(c, protocol)
528}
529
530// InstanceSize returns the size in bytes of instances of the class or 0 if cls is nil
531func (c Class) InstanceSize() uintptr {
532	return class_getInstanceSize(c)
533}
534
535// InstanceVariable returns an Ivar data structure containing information about the instance variable specified by name.
536func (c Class) InstanceVariable(name string) Ivar {
537	return class_getInstanceVariable(c, name)
538}
539
540// Register registers a class that was allocated using AllocateClassPair.
541// It can now be used to make objects by sending it either alloc and init or new.
542//
543// Deprecated: use RegisterClass instead
544func (c Class) Register() {
545	objc_registerClassPair(c)
546}
547
548// Ivar an opaque type that represents an instance variable.
549type Ivar uintptr
550
551// Offset returns the offset of an instance variable that can be used to assign and read the Ivar's value.
552//
553// For instance variables of type ID or other object types, call Ivar and SetIvar instead
554// of using this offset to access the instance variable data directly.
555func (i Ivar) Offset() uintptr {
556	return ivar_getOffset(i)
557}
558
559func (i Ivar) Name() string {
560	return ivar_getName(i)
561}
562
563// Protocol is a type that declares methods that can be implemented by any class.
564type Protocol [0]func()
565
566// GetProtocol returns the protocol for the given name or nil if there is no protocol by that name.
567func GetProtocol(name string) *Protocol {
568	return objc_getProtocol(name)
569}
570
571// Equals return true if the two protocols are the same.
572func (p *Protocol) Equals(p2 *Protocol) bool {
573	return protocol_isEqual(p, p2)
574}
575
576// IMP is a function pointer that can be called by Objective-C code.
577type IMP uintptr
578
579// NewIMP takes a Go function that takes (ID, SEL) as its first two arguments.
580// It returns an IMP function pointer that can be called by Objective-C code.
581// The function panics if an error occurs.
582// The function pointer is never deallocated.
583func NewIMP(fn interface{}) IMP {
584	ty := reflect.TypeOf(fn)
585	if ty.Kind() != reflect.Func {
586		panic("objc: not a function")
587	}
588	// IMP is stricter than a normal callback
589	// id (*IMP)(id, SEL, ...)
590	switch {
591	case ty.NumIn() < 2:
592		fallthrough
593	case ty.In(0) != reflect.TypeOf(ID(0)):
594		fallthrough
595	case ty.In(1) != reflect.TypeOf(SEL(0)):
596		panic("objc: NewIMP must take a (id, SEL) as its first two arguments; got " + ty.String())
597	}
598	return IMP(purego.NewCallback(fn))
599}

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