v0.1.0 4a0dffe

client.go RawHistoryFolder

1// Package wlturbo provides a high-performance Wayland client implementation optimized for gaming and real-time applications.
2//
3// This package delivers sub-microsecond latency, zero-allocation hot paths, and support for 8000Hz gaming devices.
4// It's designed for video game engines, competitive gaming, and other performance-critical applications.
5package wlturbo
6
7import (
8	"bytes"
9	"encoding/binary"
10	"errors"
11	"fmt"
12	"io"
13	"log"
14
15	// "log"
16	"net"
17	"os"
18	"path/filepath"
19	"sync"
20	"sync/atomic"
21)
22
23// Pre-allocated buffer pool for performance
24var bufferPool = sync.Pool{
25	New: func() interface{} {
26		return &bytes.Buffer{}
27	},
28}
29
30// Fixed represents a 24.8 fixed-point number
31type Fixed int32
32
33// Float64 converts Fixed to float64
34func (f Fixed) Float64() float64 {
35	return float64(f) / 256.0
36}
37
38// NewFixed creates a Fixed from float64
39func NewFixed(v float64) Fixed {
40	return Fixed(v * 256.0)
41}
42
43// Object represents a Wayland object
44type Object interface {
45	ID() uint32
46}
47
48// Display represents a connection to the Wayland display
49type Display struct {
50	conn      net.Conn
51	fd        int
52	objects   sync.Map // map[uint32]Object
53	nextID    uint32
54	sendMu    sync.Mutex
55	recvMu    sync.Mutex
56	listeners sync.Map // map[uint32]map[uint16][]func([]byte)
57
58	// High-performance event dispatcher
59	dispatcher *EventDispatcher
60
61	// Core objects
62	registry *Registry
63	context  *Context // Store context once
64
65	// Error state
66	lastError     error
67	lastErrorCode uint32
68	lastErrorObj  uint32
69
70	// Reusable read buffer for header
71	headerBuf [8]byte
72
73	// Pre-allocated buffer for event bodies (avoid allocations)
74	eventBodyBuf [4096]byte
75}
76
77// Registry represents the global registry
78type Registry struct {
79	id       uint32
80	display  *Display
81	globals  map[uint32]Global
82	mu       sync.RWMutex
83	handlers map[string]GlobalHandler
84}
85
86// callbackObject represents a wl_callback object
87type callbackObject struct {
88	BaseProxy
89	display *Display
90}
91
92func (c *callbackObject) ID() uint32 {
93	return c.id
94}
95
96// Dispatch handles callback events (opcode 0 = done)
97func (c *callbackObject) Dispatch(event *Event) {
98	if event.Opcode == 0 { // done event
99		log.Printf("wlclient: callbackObject.Dispatch fired for ID %d", c.id)
100		// Trigger any listeners for this callback
101		if listeners, ok := c.display.listeners.Load(c.id); ok {
102			if opcodeMap, ok := listeners.(*sync.Map); ok {
103				if handlers, ok := opcodeMap.Load(uint16(0)); ok {
104					if handlerSlice, ok := handlers.(*[]func([]byte)); ok {
105						if handlerSlice == nil {
106							log.Printf("wlclient: WARNING: handlerSlice is nil for callback ID %d", c.id)
107							return
108						}
109						log.Printf("wlclient: Found %d handlers for callback ID %d", len(*handlerSlice), c.id)
110						for i, handler := range *handlerSlice {
111							if handler == nil {
112								log.Printf("wlclient: WARNING: handler %d is nil for callback ID %d", i, c.id)
113								continue
114							}
115							log.Printf("wlclient: Calling handler %d for callback ID %d", i, c.id)
116							handler(event.data)
117						}
118					} else {
119						log.Printf("wlclient: WARNING: handlers not a slice for callback ID %d", c.id)
120					}
121				} else {
122					log.Printf("wlclient: WARNING: no handlers found for opcode 0 on callback ID %d", c.id)
123				}
124			} else {
125				log.Printf("wlclient: WARNING: listeners not a sync.Map for callback ID %d", c.id)
126			}
127		} else {
128			log.Printf("wlclient: WARNING: no listeners found for callback ID %d", c.id)
129		}
130	}
131}
132
133// Global represents a global object
134type Global struct {
135	Name      uint32
136	Interface string
137	Version   uint32
138}
139
140// GlobalHandler is called when a global is announced
141type GlobalHandler func(registry *Registry, name uint32, version uint32)
142
143// Connect connects to the Wayland display
144func Connect(socketPath string) (*Display, error) {
145	if socketPath == "" {
146		socketPath = os.Getenv("WAYLAND_DISPLAY")
147		if socketPath == "" {
148			socketPath = "wayland-0"
149		}
150	}
151
152	// Resolve socket path
153	if !filepath.IsAbs(socketPath) {
154		runDir := os.Getenv("XDG_RUNTIME_DIR")
155		if runDir == "" {
156			return nil, errors.New("XDG_RUNTIME_DIR not set")
157		}
158		socketPath = filepath.Join(runDir, socketPath)
159	}
160
161	// log.Printf("wlclient: Connecting to Wayland socket: %s", socketPath)
162	// Connect to socket
163	conn, err := net.Dial("unix", socketPath)
164	if err != nil {
165		return nil, fmt.Errorf("failed to connect to Wayland: %w", err)
166	}
167	// log.Printf("wlclient: Connected to Wayland socket successfully")
168
169	// Get file descriptor for advanced operations
170	file, err := conn.(*net.UnixConn).File()
171	if err != nil {
172		_ = conn.Close()
173		return nil, fmt.Errorf("failed to get socket fd: %w", err)
174	}
175	fd := int(file.Fd())
176	_ = file.Close() // We only need the fd
177
178	d := &Display{
179		conn:       conn,
180		fd:         fd,
181		nextID:     2, // 1 is reserved for wl_display
182		dispatcher: NewEventDispatcher(),
183	}
184
185	// Create context once and store it
186	d.context = NewContext(d)
187
188	// Register display object (ID 1)
189	d.objects.Store(uint32(1), d)
190
191	// Initialize registry
192	d.registry = &Registry{
193		id:       d.allocateID(),
194		display:  d,
195		globals:  make(map[uint32]Global),
196		handlers: make(map[string]GlobalHandler),
197	}
198	d.objects.Store(d.registry.id, d.registry)
199
200	// Get registry
201	if err := d.getRegistry(); err != nil {
202		_ = conn.Close()
203		return nil, fmt.Errorf("failed to get registry: %w", err)
204	}
205
206	// Don't do initial roundtrip here - let the caller do it after setting up handlers
207	// log.Printf("wlclient: display connected, registry ready")
208
209	return d, nil
210}
211
212// Close closes the display connection
213func (d *Display) Close() error {
214	return d.conn.Close()
215}
216
217// ID returns the display's object ID (always 1)
218func (d *Display) ID() uint32 {
219	return 1
220}
221
222// RegisterEventHandler registers a high-performance event handler
223func (d *Display) RegisterEventHandler(objectID uint32, opcode uint16, handler EventHandler) {
224	d.dispatcher.RegisterHandler(objectID, opcode, handler)
225}
226
227// allocateID allocates a new object ID
228func (d *Display) allocateID() uint32 {
229	id := atomic.AddUint32(&d.nextID, 1) - 1
230	log.Printf("wlclient: allocateID() generated ID %d", id)
231	return id
232}
233
234// AllocateID allocates a new object ID (public method)
235func (d *Display) AllocateID() uint32 {
236	return d.allocateID()
237}
238
239// SendRequest sends a request to the compositor
240func (d *Display) SendRequest(objectID uint32, opcode uint16, args ...interface{}) error {
241	return d.SendRequestWithFDs(objectID, opcode, nil, args...)
242}
243
244// SendRequestWithFDs sends a request with file descriptors
245func (d *Display) SendRequestWithFDs(objectID uint32, opcode uint16, fds []int, args ...interface{}) error {
246	// Get buffer from pool
247	buf := bufferPool.Get().(*bytes.Buffer)
248	defer func() {
249		buf.Reset()
250		bufferPool.Put(buf)
251	}()
252
253	// Write header placeholder
254	header := make([]byte, 8)
255	_, _ = buf.Write(header)
256
257	// Marshal arguments
258	for _, arg := range args {
259		if err := d.marshalArg(buf, arg); err != nil {
260			return fmt.Errorf("failed to marshal argument: %w", err)
261		}
262	}
263
264	// Update header with actual size in bytes
265	bufLen := buf.Len()
266	if bufLen > 0xFFFF {
267		return fmt.Errorf("message too large: %d bytes", bufLen)
268	}
269	size := uint32(bufLen) // Safe: checked above
270	binary.LittleEndian.PutUint32(header[0:4], objectID)
271	// Upper 16 bits = size, lower 16 bits = opcode
272	binary.LittleEndian.PutUint32(header[4:8], (size<<16)|uint32(opcode))
273
274	// Update buffer with correct header
275	data := buf.Bytes()
276	copy(data[0:8], header)
277
278	// Send message with optional file descriptors
279	return d.sendmsgWithFDs(data, fds)
280}
281
282// marshalArg marshals a single argument
283func (d *Display) marshalArg(buf *bytes.Buffer, arg interface{}) error {
284	switch v := arg.(type) {
285	case uint32:
286		return binary.Write(buf, binary.LittleEndian, v)
287	case int32:
288		return binary.Write(buf, binary.LittleEndian, v)
289	case Fixed:
290		return binary.Write(buf, binary.LittleEndian, int32(v))
291	case string:
292		// String format: length (including null) + string + null + padding
293		strlen := len(v) + 1
294		if strlen > 0xFFFFFFFF {
295			return fmt.Errorf("string too long: %d bytes", strlen)
296		}
297		if err := binary.Write(buf, binary.LittleEndian, uint32(strlen)); err != nil { // Safe: checked above
298			return err
299		}
300		_, _ = buf.WriteString(v)
301		_ = buf.WriteByte(0)
302		// Pad to 32-bit boundary
303		padding := (4 - (strlen % 4)) % 4
304		for i := 0; i < padding; i++ {
305			_ = buf.WriteByte(0)
306		}
307	case []byte:
308		// Array format: length + data + padding
309		arrlen := len(v)
310		if arrlen > 0xFFFFFFFF {
311			return fmt.Errorf("array too long: %d bytes", arrlen)
312		}
313		if err := binary.Write(buf, binary.LittleEndian, uint32(arrlen)); err != nil {
314			return err
315		}
316		_, _ = buf.Write(v)
317		// Pad to 32-bit boundary
318		padding := (4 - (arrlen % 4)) % 4
319		for i := 0; i < padding; i++ {
320			_ = buf.WriteByte(0)
321		}
322	case Object:
323		if v != nil {
324			return binary.Write(buf, binary.LittleEndian, v.ID())
325		}
326		return binary.Write(buf, binary.LittleEndian, uint32(0))
327	case nil:
328		// Null object
329		return binary.Write(buf, binary.LittleEndian, uint32(0))
330	case int:
331		// File descriptor - write placeholder in message
332		// Actual FD will be sent via SCM_RIGHTS
333		return binary.Write(buf, binary.LittleEndian, uint32(0))
334	case uintptr:
335		// File descriptor as uintptr - NO placeholder in message for neurlang compatibility
336		// FD is ONLY sent via SCM_RIGHTS, not in the message body
337		return nil
338	default:
339		return fmt.Errorf("unsupported argument type: %T", arg)
340	}
341	return nil
342}
343
344// Dispatch reads and dispatches events
345func (d *Display) Dispatch() error {
346	d.recvMu.Lock()
347	defer d.recvMu.Unlock()
348
349	// Read header with potential file descriptors
350	n, fds, err := d.recvmsgWithFDs(d.headerBuf[:])
351	if err != nil {
352		return fmt.Errorf("failed to read header: %w", err)
353	}
354	if n < 8 {
355		return fmt.Errorf("incomplete header: got %d bytes", n)
356	}
357
358	objectID := binary.LittleEndian.Uint32(d.headerBuf[0:4])
359	sizeOpcode := binary.LittleEndian.Uint32(d.headerBuf[4:8])
360	// Upper 16 bits = size (includes header), lower 16 bits = opcode
361	size := sizeOpcode >> 16
362	opcode := sizeOpcode & 0xffff
363
364	// Only log suspicious events or important ones
365	// if objectID > 1000000 || objectID == 5 {
366	// 	log.Printf("wlclient: Dispatch received event: object=%d (0x%x), opcode=%d, size=%d", objectID, objectID, opcode, size)
367	// }
368	if opcode > 0xFFFF {
369		return fmt.Errorf("invalid opcode: %d", opcode)
370	}
371
372	// Read message body (size includes 8-byte header)
373	var body []byte
374	if size > 8 {
375		bodySize := size - 8
376
377		// Use pre-allocated buffer for small messages (zero allocation)
378		if bodySize <= uint32(len(d.eventBodyBuf)) {
379			body = d.eventBodyBuf[:bodySize]
380		} else {
381			// Large message, allocate new buffer
382			body = make([]byte, bodySize)
383		}
384
385		n, moreFds, err := d.recvmsgWithFDs(body)
386		if err != nil {
387			return fmt.Errorf("failed to read body: %w", err)
388		}
389		if n < int(bodySize) {
390			// Read remaining data if needed
391			remaining := body[n:]
392			if _, err := io.ReadFull(d.conn, remaining); err != nil {
393				return fmt.Errorf("failed to read remaining body: %w", err)
394			}
395		}
396		fds = append(fds, moreFds...)
397	}
398
399	// Handle display events specially
400	if objectID == 1 {
401		return d.handleDisplayEvent(uint16(opcode), body)
402	}
403
404	// Try to find the object
405	if obj, ok := d.objects.Load(objectID); ok {
406		// Only log for non-registry objects to reduce spam
407		if objectID != 2 {
408			log.Printf("wlclient: Found object for ID %d, type: %T", objectID, obj)
409		}
410
411		// CRITICAL: Handle server object creation BEFORE dispatching
412		if d.handleServerObject(objectID, uint16(opcode), body) {
413			log.Printf("wlclient: Handled server object creation for ID %d, opcode %d", objectID, opcode)
414		}
415
416		// Check if it's a Proxy with Dispatch method
417		if proxy, ok := obj.(Proxy); ok && proxy != nil {
418			event := &Event{
419				ProxyID: objectID,
420				Opcode:  uint16(opcode),
421				data:    body,
422				offset:  0,
423			}
424			if objectID != 2 {
425				log.Printf("wlclient: Dispatching event to proxy ID %d, opcode %d", objectID, opcode)
426			}
427			proxy.Dispatch(event)
428			return nil // Event was handled
429		}
430	} else {
431		log.Printf("wlclient: WARNING: No object found for ID %d (0x%x)", objectID, objectID)
432	}
433
434	// Use high-performance dispatcher
435	d.dispatcher.Dispatch(objectID, uint16(opcode), body)
436
437	// Also check listeners (for compatibility)
438	if listeners, ok := d.listeners.Load(objectID); ok {
439		if opcodeMap, ok := listeners.(*sync.Map); ok {
440			if handlers, ok := opcodeMap.Load(uint16(opcode)); ok {
441				if handlerSlice, ok := handlers.(*[]func([]byte)); ok {
442					for _, handler := range *handlerSlice {
443						handler(body)
444					}
445				}
446			}
447		}
448	}
449
450	return nil
451}
452
453// handleDisplayEvent handles events on the display object
454func (d *Display) handleDisplayEvent(opcode uint16, data []byte) error {
455	switch opcode {
456	case 0: // error
457		if len(data) < 8 {
458			return errors.New("invalid error event")
459		}
460		objectID := binary.LittleEndian.Uint32(data[0:4])
461		code := binary.LittleEndian.Uint32(data[4:8])
462
463		var message string
464		if len(data) > 8 {
465			// Message is optional, parse if present
466			if len(data) >= 12 {
467				msgLen := binary.LittleEndian.Uint32(data[8:12])
468				if msgLen > 0 && len(data) >= 12+int(msgLen) {
469					message = string(data[12 : 12+msgLen-1]) // -1 to remove null terminator
470				}
471			}
472		}
473
474		d.lastError = fmt.Errorf("protocol error: object %d, code %d: %s", objectID, code, message)
475		d.lastErrorCode = code
476		d.lastErrorObj = objectID
477		return d.lastError
478
479	case 1: // delete_id
480		if len(data) < 4 {
481			return errors.New("invalid delete_id event")
482		}
483		id := binary.LittleEndian.Uint32(data[0:4])
484		d.objects.Delete(id)
485	}
486
487	return nil
488}
489
490// Roundtrip performs a synchronous roundtrip to the compositor
491func (d *Display) Roundtrip() error {
492	log.Printf("wlclient: Roundtrip starting...")
493	// Create callback
494	callbackID := d.allocateID()
495	done := make(chan error, 1)
496
497	log.Printf("wlclient: Registering callback listener for ID %d", callbackID)
498	// Register callback listener
499	d.AddListener(callbackID, 0, func(_ []byte) {
500		log.Printf("wlclient: Callback %d fired", callbackID)
501		d.objects.Delete(callbackID)
502		done <- nil
503	})
504
505	// Send sync request (opcode 0)
506	log.Printf("wlclient: Sending sync request with callback ID %d", callbackID)
507	if err := d.SendRequest(1, 0, callbackID); err != nil {
508		return err
509	}
510
511	// Store the callback object to track it
512	d.objects.Store(callbackID, &callbackObject{
513		BaseProxy: BaseProxy{
514			context: d.Context(),
515			id:      callbackID,
516		},
517		display: d,
518	})
519
520	// Process events until callback fires - SIMPLIFIED APPROACH
521	maxIterations := 1000 // Prevent infinite loops
522	for i := 0; i < maxIterations; i++ {
523		// Don't set read deadline for now - causes issues
524		if err := d.Dispatch(); err != nil {
525			log.Printf("wlclient: Dispatch error: %v", err)
526			return err
527		}
528
529		// Check if callback completed
530		select {
531		case err := <-done:
532			log.Printf("wlclient: Roundtrip completed successfully")
533			return err
534		default:
535			// Continue dispatching
536		}
537	}
538
539	return fmt.Errorf("roundtrip failed: max iterations reached")
540}
541
542// AddListener adds an event listener for an object
543func (d *Display) AddListener(objectID uint32, opcode uint16, handler func([]byte)) {
544	// Load or create listener map for object
545	listeners, _ := d.listeners.LoadOrStore(objectID, &sync.Map{})
546	opcodeMap := listeners.(*sync.Map)
547
548	// Load or create handlers slice for opcode
549	handlers, _ := opcodeMap.LoadOrStore(opcode, &[]func([]byte){})
550	handlerSlice := handlers.(*[]func([]byte))
551
552	// Add handler (thread-safe)
553	*handlerSlice = append(*handlerSlice, handler)
554}
555
556// getRegistry gets the global registry
557func (d *Display) getRegistry() error {
558	// Add registry listeners
559	d.AddListener(d.registry.id, 0, d.registry.handleGlobal)
560	d.AddListener(d.registry.id, 1, d.registry.handleGlobalRemove)
561
562	// Send get_registry request (opcode 1)
563	return d.SendRequest(1, 1, d.registry.id)
564}
565
566// Registry returns the global registry
567func (d *Display) Registry() *Registry {
568	return d.registry
569}
570
571// ID returns the registry's object ID
572func (r *Registry) ID() uint32 {
573	return r.id
574}
575
576// handleGlobal handles global announcements
577func (r *Registry) handleGlobal(data []byte) {
578	if len(data) < 8 {
579		// log.Printf("wlclient: handleGlobal: data too short (%d bytes)", len(data))
580		return
581	}
582
583	name := binary.LittleEndian.Uint32(data[0:4])
584	ifaceLen := binary.LittleEndian.Uint32(data[4:8])
585
586	if len(data) < 8+int(ifaceLen)+4 {
587		// log.Printf("wlclient: handleGlobal: insufficient data for interface string (need %d, have %d)", 8+int(ifaceLen)+4, len(data))
588		return
589	}
590
591	// String includes null terminator in length
592	iface := string(data[8 : 8+ifaceLen-1]) // -1 to remove null terminator
593
594	// Calculate padding for 32-bit alignment
595	padding := (4 - (ifaceLen % 4)) % 4
596	versionOffset := 8 + int(ifaceLen) + int(padding)
597
598	if len(data) < versionOffset+4 {
599		// log.Printf("wlclient: handleGlobal: insufficient data for version (need %d, have %d)", versionOffset+4, len(data))
600		return
601	}
602
603	version := binary.LittleEndian.Uint32(data[versionOffset:])
604
605	// log.Printf("wlclient: global announced: %s v%d (name=%d)", iface, version, name)
606
607	// Store global
608	r.mu.Lock()
609	r.globals[name] = Global{
610		Name:      name,
611		Interface: iface,
612		Version:   version,
613	}
614	r.mu.Unlock()
615
616	// Call specific handler if registered
617	if handler, ok := r.handlers[iface]; ok {
618		// log.Printf("wlclient: calling handler for interface %s", iface)
619		handler(r, name, version)
620	}
621
622	// Call wildcard handler if registered
623	if handler, ok := r.handlers["*"]; ok {
624		// log.Printf("wlclient: calling wildcard handler for interface %s", iface)
625		handler(r, name, version)
626	}
627}
628
629// handleGlobalRemove handles global removal
630func (r *Registry) handleGlobalRemove(data []byte) {
631	if len(data) < 4 {
632		return
633	}
634
635	name := binary.LittleEndian.Uint32(data[0:4])
636
637	r.mu.Lock()
638	delete(r.globals, name)
639	r.mu.Unlock()
640}
641
642// AddHandler adds a handler for a specific interface
643func (r *Registry) AddHandler(iface string, handler GlobalHandler) {
644	r.handlers[iface] = handler
645}
646
647// Bind binds to a global object and returns a typed proxy
648func (r *Registry) Bind(name uint32, iface string, version uint32, proxy Proxy) error {
649	// Set the ID if not already set
650	if proxy.ID() == 0 {
651		newID := r.display.allocateID()
652		log.Printf("wlclient: Allocating new ID %d for interface %s", newID, iface)
653		proxy.SetID(newID)
654	}
655
656	// Ensure proxy has a context set - CRITICAL FIX
657	if proxy.Context() == nil {
658		if baseProxy, ok := proxy.(*BaseProxy); ok {
659			baseProxy.SetContext(r.display.Context())
660			log.Printf("wlclient: Set context for proxy ID %d", proxy.ID())
661		} else if setter, ok := proxy.(interface{ SetContext(*Context) }); ok {
662			setter.SetContext(r.display.Context())
663			log.Printf("wlclient: Set context for proxy ID %d via interface", proxy.ID())
664		} else {
665			return fmt.Errorf("proxy doesn't have context and can't set it")
666		}
667	}
668
669	// Register the proxy
670	proxy.Context().Register(proxy)
671
672	// Also register directly in display objects map
673	r.display.objects.Store(proxy.ID(), proxy)
674	log.Printf("wlclient: Registered proxy ID %d in display objects map", proxy.ID())
675
676	// Send bind request (opcode 0) with proper arguments
677	if err := r.display.SendRequest(r.id, 0, name, iface, version, proxy.ID()); err != nil {
678		proxy.Context().Unregister(proxy)
679		return err
680	}
681
682	return nil
683}
684
685// BindID binds to a global object and returns just the ID (compatibility method)
686func (r *Registry) BindID(name uint32, iface string, version uint32) (uint32, error) {
687	newID := r.display.allocateID()
688
689	// Send bind request (opcode 0) with proper arguments
690	if err := r.display.SendRequest(r.id, 0, name, iface, version, newID); err != nil {
691		return 0, err
692	}
693
694	return newID, nil
695}
696
697// GetGlobals returns all announced globals
698func (r *Registry) GetGlobals() map[uint32]Global {
699	r.mu.RLock()
700	defer r.mu.RUnlock()
701
702	globals := make(map[uint32]Global)
703	for k, v := range r.globals {
704		globals[k] = v
705	}
706	return globals
707}
708
709// FindGlobal finds a global by interface name
710func (r *Registry) FindGlobal(iface string) (Global, bool) {
711	r.mu.RLock()
712	defer r.mu.RUnlock()
713
714	for _, global := range r.globals {
715		if global.Interface == iface {
716			return global, true
717		}
718	}
719	return Global{}, false
720}
721
722// handleServerObject creates and registers server-allocated objects from events
723func (d *Display) handleServerObject(objectID uint32, opcode uint16, body []byte) bool {
724	// Handle specific known protocols that create server objects
725	obj, ok := d.objects.Load(objectID)
726	if !ok {
727		return false
728	}
729
730	// Check if this is a known object type that creates server objects
731	switch obj.(type) {
732	case *Registry:
733		// Registry doesn't create server objects directly
734		return false
735
736	default:
737		// Check if this might be an output manager (interface zwlr_output_manager_v1)
738		if objectID == 5 { // This is likely the output manager based on your logs
739			return d.handleOutputManagerEvent(objectID, opcode, body)
740		}
741	}
742
743	return false
744}
745
746// handleOutputManagerEvent handles events from zwlr_output_manager_v1
747func (d *Display) handleOutputManagerEvent(objectID uint32, opcode uint16, body []byte) bool {
748	switch opcode {
749	case 0: // head event - creates new zwlr_output_head_v1 object
750		if len(body) < 4 {
751			log.Printf("wlclient: head event body too short: %d bytes", len(body))
752			return false
753		}
754
755		// Parse the new_id for the head object
756		headID := binary.LittleEndian.Uint32(body[0:4])
757		log.Printf("wlclient: Creating server object (head) with ID %d", headID)
758
759		// Create a proper OutputHead object
760		headProxy := &OutputHead{
761			BaseProxy: BaseProxy{
762				id:      headID,
763				context: d.Context(),
764			},
765		}
766
767		// Register the new head object
768		d.objects.Store(headID, headProxy)
769		log.Printf("wlclient: Registered server-created head object ID %d", headID)
770		return true
771
772	case 1: // done event
773		log.Printf("wlclient: Output manager done event")
774		return false
775
776	case 2: // finished event
777		log.Printf("wlclient: Output manager finished event")
778		return false
779	}
780
781	return false
782}
783
784// OutputHead represents a zwlr_output_head_v1 object
785type OutputHead struct {
786	BaseProxy
787	name        string
788	description string
789	width       int32
790	height      int32
791}
792
793// Dispatch handles head events
794func (h *OutputHead) Dispatch(event *Event) {
795	log.Printf("wlclient: OutputHead.Dispatch ID %d, opcode %d", h.id, event.Opcode)
796	switch event.Opcode {
797	case 0: // name
798		h.name = event.String()
799		log.Printf("wlclient: Head %d name: %s", h.id, h.name)
800	case 1: // description
801		h.description = event.String()
802		log.Printf("wlclient: Head %d description: %s", h.id, h.description)
803	case 2: // physical_size
804		h.width = event.Int32()
805		h.height = event.Int32()
806		log.Printf("wlclient: Head %d physical size: %dx%d mm", h.id, h.width, h.height)
807	case 3: // mode (creates new mode object)
808		modeID := event.Uint32()
809		log.Printf("wlclient: Head %d mode object created: ID %d", h.id, modeID)
810		// Create mode object
811		mode := &OutputMode{
812			BaseProxy: BaseProxy{
813				id:      modeID,
814				context: h.context,
815			},
816		}
817		h.context.display.objects.Store(modeID, mode)
818	case 9: // finished
819		log.Printf("wlclient: Head %d finished", h.id)
820		h.context.Unregister(h)
821	default:
822		log.Printf("wlclient: Head %d unhandled opcode %d", h.id, event.Opcode)
823	}
824}
825
826// OutputMode represents a zwlr_output_mode_v1 object
827type OutputMode struct {
828	BaseProxy
829	width   int32
830	height  int32
831	refresh int32
832}
833
834// Dispatch handles mode events
835func (m *OutputMode) Dispatch(event *Event) {
836	log.Printf("wlclient: OutputMode.Dispatch ID %d, opcode %d", m.id, event.Opcode)
837	switch event.Opcode {
838	case 0: // size
839		m.width = event.Int32()
840		m.height = event.Int32()
841		log.Printf("wlclient: Mode %d size: %dx%d", m.id, m.width, m.height)
842	case 1: // refresh
843		m.refresh = event.Int32()
844		log.Printf("wlclient: Mode %d refresh: %d mHz", m.id, m.refresh)
845	case 2: // preferred
846		log.Printf("wlclient: Mode %d is preferred", m.id)
847	case 3: // finished
848		log.Printf("wlclient: Mode %d finished", m.id)
849		m.context.Unregister(m)
850	default:
851		log.Printf("wlclient: Mode %d unhandled opcode %d", m.id, event.Opcode)
852	}
853}

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 8

↑↓ moveenter openesc close