v0.2.0 ab733e5

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	"net"
14	"os"
15	"path/filepath"
16	"sync"
17	"sync/atomic"
18)
19
20// Pre-allocated buffer pool for performance
21var bufferPool = sync.Pool{
22	New: func() interface{} {
23		return &bytes.Buffer{}
24	},
25}
26
27// Fixed represents a 24.8 fixed-point number
28type Fixed int32
29
30// Float64 converts Fixed to float64
31func (f Fixed) Float64() float64 {
32	return float64(f) / 256.0
33}
34
35// NewFixed creates a Fixed from float64
36func NewFixed(v float64) Fixed {
37	return Fixed(v * 256.0)
38}
39
40// Object represents a Wayland object
41type Object interface {
42	ID() uint32
43}
44
45// Display represents a connection to the Wayland display
46type Display struct {
47	conn      net.Conn
48	unix      *net.UnixConn // set when conn is a Unix socket, nil otherwise
49	objects   sync.Map      // map[uint32]Object
50	nextID    uint32
51	sendMu    sync.Mutex
52	recvMu    sync.Mutex
53	listeners sync.Map // map[uint32]map[uint16][]func([]byte)
54
55	// High-performance event dispatcher
56	dispatcher *EventDispatcher
57
58	// Core objects
59	registry *Registry
60	context  *Context // Store context once
61
62	// Error state
63	lastError     error
64	lastErrorCode uint32
65	lastErrorObj  uint32
66
67	// Connection-local receive state. rbuf holds bytes read from the socket
68	// that do not yet form a complete frame; recvBuf is the scratch buffer
69	// used for a single read. pendingFDs holds ancillary descriptors that
70	// arrived with those bytes and have not been consumed yet.
71	rbuf       []byte
72	recvBuf    []byte
73	readErr    error
74	maxMsgLen  uint32
75	closed     atomic.Bool
76	fdMu       sync.Mutex
77	pendingFDs []int
78}
79
80// newDisplay builds a Display over an established connection.
81func newDisplay(conn net.Conn) *Display {
82	d := &Display{
83		conn:       conn,
84		nextID:     2, // 1 is reserved for wl_display
85		dispatcher: NewEventDispatcher(),
86	}
87	if uc, ok := conn.(*net.UnixConn); ok {
88		d.unix = uc
89	}
90	d.context = NewContext(d)
91	// Register display object (ID 1)
92	d.objects.Store(uint32(displayObjectID), d)
93	// Initialize registry
94	d.registry = &Registry{
95		id:       d.allocateID(),
96		display:  d,
97		globals:  make(map[uint32]Global),
98		handlers: make(map[string]GlobalHandler),
99	}
100	d.objects.Store(d.registry.id, d.registry)
101	return d
102}
103
104// Registry represents the global registry
105type Registry struct {
106	id       uint32
107	display  *Display
108	globals  map[uint32]Global
109	mu       sync.RWMutex
110	handlers map[string]GlobalHandler
111}
112
113// callbackObject represents a wl_callback object
114type callbackObject struct {
115	BaseProxy
116	display *Display
117}
118
119func (c *callbackObject) ID() uint32 {
120	return c.id
121}
122
123// Dispatch handles callback events (opcode 0 = done)
124func (c *callbackObject) Dispatch(event *Event) {
125	if event.Opcode != 0 { // done event
126		return
127	}
128	c.display.notifyListeners(c.id, 0, event.data)
129}
130
131// Global represents a global object
132type Global struct {
133	Name      uint32
134	Interface string
135	Version   uint32
136}
137
138// GlobalHandler is called when a global is announced
139type GlobalHandler func(registry *Registry, name uint32, version uint32)
140
141// Connect connects to the Wayland display and requests the registry.
142func Connect(socketPath string) (*Display, error) {
143	if socketPath == "" {
144		socketPath = os.Getenv("WAYLAND_DISPLAY")
145		if socketPath == "" {
146			socketPath = "wayland-0"
147		}
148	}
149
150	// Resolve socket path
151	if !filepath.IsAbs(socketPath) {
152		runDir := os.Getenv("XDG_RUNTIME_DIR")
153		if runDir == "" {
154			return nil, errors.New("XDG_RUNTIME_DIR not set")
155		}
156		socketPath = filepath.Join(runDir, socketPath)
157	}
158
159	conn, err := net.Dial("unix", socketPath)
160	if err != nil {
161		return nil, fmt.Errorf("failed to connect to Wayland: %w", err)
162	}
163
164	d := newDisplay(conn)
165
166	// Get registry
167	if err := d.getRegistry(); err != nil {
168		_ = d.Close()
169		return nil, fmt.Errorf("failed to get registry: %w", err)
170	}
171
172	// Don't do initial roundtrip here - let the caller do it after setting up handlers
173	return d, nil
174}
175
176// Close closes the display connection. It is safe to call more than once;
177// later calls are no-ops. After Close, Dispatch returns net.ErrClosed.
178func (d *Display) Close() error {
179	if !d.closed.CompareAndSwap(false, true) {
180		return nil
181	}
182	d.closePendingFDs()
183	return d.conn.Close()
184}
185
186// Closed reports whether the connection has been closed.
187func (d *Display) Closed() bool {
188	return d.closed.Load()
189}
190
191// ID returns the display's object ID (always 1)
192func (d *Display) ID() uint32 {
193	return 1
194}
195
196// RegisterEventHandler registers a high-performance event handler
197func (d *Display) RegisterEventHandler(objectID uint32, opcode uint16, handler EventHandler) {
198	d.dispatcher.RegisterHandler(objectID, opcode, handler)
199}
200
201// allocateID allocates a new object ID
202func (d *Display) allocateID() uint32 {
203	return atomic.AddUint32(&d.nextID, 1) - 1
204}
205
206// AllocateID allocates a new object ID (public method)
207func (d *Display) AllocateID() uint32 {
208	return d.allocateID()
209}
210
211// SendRequest sends a request to the compositor
212func (d *Display) SendRequest(objectID uint32, opcode uint16, args ...interface{}) error {
213	return d.SendRequestWithFDs(objectID, opcode, nil, args...)
214}
215
216// SendRequestWithFDs sends a request with file descriptors
217func (d *Display) SendRequestWithFDs(objectID uint32, opcode uint16, fds []int, args ...interface{}) error {
218	// Get buffer from pool
219	buf := bufferPool.Get().(*bytes.Buffer)
220	defer func() {
221		buf.Reset()
222		bufferPool.Put(buf)
223	}()
224
225	// Write header placeholder
226	header := make([]byte, 8)
227	_, _ = buf.Write(header)
228
229	// Marshal arguments
230	for _, arg := range args {
231		if err := d.marshalArg(buf, arg); err != nil {
232			return fmt.Errorf("failed to marshal argument: %w", err)
233		}
234	}
235
236	// Update header with actual size in bytes
237	bufLen := buf.Len()
238	if bufLen > 0xFFFF {
239		return fmt.Errorf("message too large: %d bytes", bufLen)
240	}
241	size := uint32(bufLen) // Safe: checked above
242	binary.LittleEndian.PutUint32(header[0:4], objectID)
243	// Upper 16 bits = size, lower 16 bits = opcode
244	binary.LittleEndian.PutUint32(header[4:8], (size<<16)|uint32(opcode))
245
246	// Update buffer with correct header
247	data := buf.Bytes()
248	copy(data[0:8], header)
249
250	// Send message with optional file descriptors
251	return d.sendmsgWithFDs(data, fds)
252}
253
254// marshalArg marshals a single argument
255func (d *Display) marshalArg(buf *bytes.Buffer, arg interface{}) error {
256	switch v := arg.(type) {
257	case uint32:
258		return binary.Write(buf, binary.LittleEndian, v)
259	case int32:
260		return binary.Write(buf, binary.LittleEndian, v)
261	case Fixed:
262		return binary.Write(buf, binary.LittleEndian, int32(v))
263	case string:
264		// String format: length (including null) + string + null + padding
265		strlen := len(v) + 1
266		if strlen > 0xFFFFFFFF {
267			return fmt.Errorf("string too long: %d bytes", strlen)
268		}
269		if err := binary.Write(buf, binary.LittleEndian, uint32(strlen)); err != nil { // Safe: checked above
270			return err
271		}
272		_, _ = buf.WriteString(v)
273		_ = buf.WriteByte(0)
274		// Pad to 32-bit boundary
275		padding := (4 - (strlen % 4)) % 4
276		for i := 0; i < padding; i++ {
277			_ = buf.WriteByte(0)
278		}
279	case []byte:
280		// Array format: length + data + padding
281		arrlen := len(v)
282		if arrlen > 0xFFFFFFFF {
283			return fmt.Errorf("array too long: %d bytes", arrlen)
284		}
285		if err := binary.Write(buf, binary.LittleEndian, uint32(arrlen)); err != nil {
286			return err
287		}
288		_, _ = buf.Write(v)
289		// Pad to 32-bit boundary
290		padding := (4 - (arrlen % 4)) % 4
291		for i := 0; i < padding; i++ {
292			_ = buf.WriteByte(0)
293		}
294	case Object:
295		if v != nil {
296			return binary.Write(buf, binary.LittleEndian, v.ID())
297		}
298		return binary.Write(buf, binary.LittleEndian, uint32(0))
299	case nil:
300		// Null object
301		return binary.Write(buf, binary.LittleEndian, uint32(0))
302	case int:
303		// File descriptor - write placeholder in message
304		// Actual FD will be sent via SCM_RIGHTS
305		return binary.Write(buf, binary.LittleEndian, uint32(0))
306	case uintptr:
307		// File descriptor as uintptr - NO placeholder in message for neurlang compatibility
308		// FD is ONLY sent via SCM_RIGHTS, not in the message body
309		return nil
310	default:
311		return fmt.Errorf("unsupported argument type: %T", arg)
312	}
313	return nil
314}
315
316// Dispatch reads one complete Wayland message and delivers it to the object
317// that owns it.
318//
319// Read boundaries never define message boundaries: a header split across
320// several reads and several messages arriving in one read are both handled.
321// Dispatch returns net.ErrClosed after Close, a typed ProtocolError for a
322// malformed frame, and io.ErrUnexpectedEOF when the peer closes mid-frame.
323func (d *Display) Dispatch() error {
324	d.recvMu.Lock()
325	defer d.recvMu.Unlock()
326
327	if d.closed.Load() {
328		return net.ErrClosed
329	}
330
331	frame, err := d.nextFrame()
332	if err != nil {
333		return d.fail(err)
334	}
335	return d.fail(d.dispatchFrame(frame))
336}
337
338// fail releases resources that cannot be used once the connection is
339// unusable. A protocol violation invalidates the whole connection, so queued
340// descriptors are closed; transport errors may be transient and keep them.
341func (d *Display) fail(err error) error {
342	if err == nil {
343		return nil
344	}
345	var perr *ProtocolError
346	if errors.As(err, &perr) {
347		d.closePendingFDs()
348	}
349	return err
350}
351
352// maxMessageSize reports the largest accepted message size.
353func (d *Display) maxMessageSize() uint32 {
354	if d.maxMsgLen == 0 {
355		return DefaultMaxMessageSize
356	}
357	return d.maxMsgLen
358}
359
360// nextFrame returns the next complete frame, reading more bytes as needed.
361func (d *Display) nextFrame() (receivedFrame, error) {
362	for {
363		if len(d.rbuf) >= HeaderSize {
364			object, opcode, size, err := parseHeader(d.rbuf[:HeaderSize], d.maxMessageSize())
365			if err != nil {
366				return receivedFrame{}, err
367			}
368			if uint32(len(d.rbuf)) >= size {
369				frame := receivedFrame{object: object, opcode: opcode}
370				if size > HeaderSize {
371					frame.body = make([]byte, size-HeaderSize)
372					copy(frame.body, d.rbuf[HeaderSize:size])
373				}
374				d.consume(size)
375				return frame, nil
376			}
377		}
378		if err := d.fill(); err != nil {
379			return receivedFrame{}, err
380		}
381	}
382}
383
384// consume removes the first n bytes from the receive buffer.
385func (d *Display) consume(n uint32) {
386	rest := copy(d.rbuf, d.rbuf[n:])
387	d.rbuf = d.rbuf[:rest]
388}
389
390// fill reads one chunk from the connection into the receive buffer. Bytes that
391// arrive together with an error are kept so they are still parsed before the
392// error surfaces.
393func (d *Display) fill() error {
394	if d.readErr != nil {
395		return d.readErr
396	}
397	if d.recvBuf == nil {
398		d.recvBuf = make([]byte, readChunkSize)
399	}
400	n, err := d.readChunk()
401	if n > 0 {
402		d.rbuf = append(d.rbuf, d.recvBuf[:n]...)
403	}
404	if err != nil {
405		if errors.Is(err, io.EOF) {
406			err = io.ErrUnexpectedEOF
407		}
408		d.readErr = err
409		// A control-message failure corrupts the descriptor stream, so it is
410		// reported immediately instead of after the buffered bytes are used.
411		var perr *ProtocolError
412		if n == 0 || errors.As(err, &perr) {
413			return err
414		}
415	}
416	return nil
417}
418
419// dispatchFrame delivers one complete frame to the object that owns it.
420func (d *Display) dispatchFrame(f receivedFrame) error {
421	if f.object == displayObjectID {
422		return d.handleDisplayEvent(f.opcode, f.body)
423	}
424
425	obj, ok := d.objects.Load(f.object)
426	if !ok {
427		return &ProtocolError{
428			Kind:   "unknown_object",
429			Object: f.object,
430			Opcode: f.opcode,
431			Size:   uint32(len(f.body)) + HeaderSize,
432			Err:    ErrUnknownObject,
433		}
434	}
435
436	// Server-created objects are announced by an event on a known object.
437	d.handleServerObject(f.object, f.opcode, f.body)
438
439	if proxy, ok := obj.(Proxy); ok && proxy != nil {
440		proxy.Dispatch(&Event{
441			ProxyID: f.object,
442			Opcode:  f.opcode,
443			data:    f.body,
444			display: d,
445		})
446		return nil
447	}
448
449	// High-performance dispatcher, then compatibility listeners.
450	d.dispatcher.Dispatch(f.object, f.opcode, f.body)
451	d.notifyListeners(f.object, f.opcode, f.body)
452	return nil
453}
454
455// notifyListeners runs the listeners registered for an object and opcode.
456func (d *Display) notifyListeners(objectID uint32, opcode uint16, body []byte) {
457	listeners, ok := d.listeners.Load(objectID)
458	if !ok {
459		return
460	}
461	opcodeMap, ok := listeners.(*sync.Map)
462	if !ok {
463		return
464	}
465	handlers, ok := opcodeMap.Load(opcode)
466	if !ok {
467		return
468	}
469	handlerSlice, ok := handlers.(*[]func([]byte))
470	if !ok || handlerSlice == nil {
471		return
472	}
473	for _, handler := range *handlerSlice {
474		if handler != nil {
475			handler(body)
476		}
477	}
478}
479
480// handleDisplayEvent handles events on the display object
481func (d *Display) handleDisplayEvent(opcode uint16, data []byte) error {
482	switch opcode {
483	case 0: // error
484		if len(data) < 8 {
485			return &ProtocolError{Kind: "short_display_error", Object: displayObjectID, Opcode: opcode, Size: uint32(len(data)) + HeaderSize, Err: ErrMalformedFrame}
486		}
487		event := &Event{ProxyID: displayObjectID, Opcode: opcode, data: data, display: d}
488		objectID := event.Uint32()
489		code := event.Uint32()
490		message := event.String()
491
492		d.lastErrorCode = code
493		d.lastErrorObj = objectID
494		d.lastError = &DisplayError{ObjectID: objectID, Code: code, Message: message}
495		return d.lastError
496
497	case 1: // delete_id
498		if len(data) < 4 {
499			return &ProtocolError{Kind: "short_delete_id", Object: displayObjectID, Opcode: opcode, Size: uint32(len(data)) + HeaderSize, Err: ErrMalformedFrame}
500		}
501		id := binary.LittleEndian.Uint32(data[0:4])
502		d.objects.Delete(id)
503		return nil
504
505	default:
506		return &ProtocolError{Kind: "unknown_opcode", Object: displayObjectID, Opcode: opcode, Err: ErrUnknownOpcode}
507	}
508}
509
510// Roundtrip performs a synchronous roundtrip to the compositor
511func (d *Display) Roundtrip() error {
512	callbackID := d.allocateID()
513	done := make(chan error, 1)
514
515	d.AddListener(callbackID, 0, func(_ []byte) {
516		d.objects.Delete(callbackID)
517		done <- nil
518	})
519
520	// Send sync request (opcode 0)
521	if err := d.SendRequest(displayObjectID, 0, callbackID); err != nil {
522		return err
523	}
524
525	d.objects.Store(callbackID, &callbackObject{
526		BaseProxy: BaseProxy{
527			context: d.Context(),
528			id:      callbackID,
529		},
530		display: d,
531	})
532
533	for i := 0; i < 1000; i++ {
534		if err := d.Dispatch(); err != nil {
535			return err
536		}
537		select {
538		case err := <-done:
539			return err
540		default:
541		}
542	}
543
544	return fmt.Errorf("roundtrip failed: max iterations reached")
545}
546
547// AddListener adds an event listener for an object
548func (d *Display) AddListener(objectID uint32, opcode uint16, handler func([]byte)) {
549	// Load or create listener map for object
550	listeners, _ := d.listeners.LoadOrStore(objectID, &sync.Map{})
551	opcodeMap := listeners.(*sync.Map)
552
553	// Load or create handlers slice for opcode
554	handlers, _ := opcodeMap.LoadOrStore(opcode, &[]func([]byte){})
555	handlerSlice := handlers.(*[]func([]byte))
556
557	// Add handler (thread-safe)
558	*handlerSlice = append(*handlerSlice, handler)
559}
560
561// getRegistry gets the global registry
562func (d *Display) getRegistry() error {
563	// Add registry listeners
564	d.AddListener(d.registry.id, 0, d.registry.handleGlobal)
565	d.AddListener(d.registry.id, 1, d.registry.handleGlobalRemove)
566
567	// Send get_registry request (opcode 1)
568	return d.SendRequest(1, 1, d.registry.id)
569}
570
571// Registry returns the global registry
572func (d *Display) Registry() *Registry {
573	return d.registry
574}
575
576// ID returns the registry's object ID
577func (r *Registry) ID() uint32 {
578	return r.id
579}
580
581// handleGlobal handles global announcements
582func (r *Registry) handleGlobal(data []byte) {
583	if len(data) < 8 {
584		// log.Printf("wlclient: handleGlobal: data too short (%d bytes)", len(data))
585		return
586	}
587
588	name := binary.LittleEndian.Uint32(data[0:4])
589	ifaceLen := binary.LittleEndian.Uint32(data[4:8])
590
591	if len(data) < 8+int(ifaceLen)+4 {
592		// log.Printf("wlclient: handleGlobal: insufficient data for interface string (need %d, have %d)", 8+int(ifaceLen)+4, len(data))
593		return
594	}
595
596	// String includes null terminator in length
597	iface := string(data[8 : 8+ifaceLen-1]) // -1 to remove null terminator
598
599	// Calculate padding for 32-bit alignment
600	padding := (4 - (ifaceLen % 4)) % 4
601	versionOffset := 8 + int(ifaceLen) + int(padding)
602
603	if len(data) < versionOffset+4 {
604		// log.Printf("wlclient: handleGlobal: insufficient data for version (need %d, have %d)", versionOffset+4, len(data))
605		return
606	}
607
608	version := binary.LittleEndian.Uint32(data[versionOffset:])
609
610	// log.Printf("wlclient: global announced: %s v%d (name=%d)", iface, version, name)
611
612	// Store global
613	r.mu.Lock()
614	r.globals[name] = Global{
615		Name:      name,
616		Interface: iface,
617		Version:   version,
618	}
619	r.mu.Unlock()
620
621	// Call specific handler if registered
622	if handler, ok := r.handlers[iface]; ok {
623		// log.Printf("wlclient: calling handler for interface %s", iface)
624		handler(r, name, version)
625	}
626
627	// Call wildcard handler if registered
628	if handler, ok := r.handlers["*"]; ok {
629		// log.Printf("wlclient: calling wildcard handler for interface %s", iface)
630		handler(r, name, version)
631	}
632}
633
634// handleGlobalRemove handles global removal
635func (r *Registry) handleGlobalRemove(data []byte) {
636	if len(data) < 4 {
637		return
638	}
639
640	name := binary.LittleEndian.Uint32(data[0:4])
641
642	r.mu.Lock()
643	delete(r.globals, name)
644	r.mu.Unlock()
645}
646
647// AddHandler adds a handler for a specific interface
648func (r *Registry) AddHandler(iface string, handler GlobalHandler) {
649	r.handlers[iface] = handler
650}
651
652// Bind binds to a global object and returns a typed proxy
653func (r *Registry) Bind(name uint32, iface string, version uint32, proxy Proxy) error {
654	// Set the ID if not already set
655	if proxy.ID() == 0 {
656		proxy.SetID(r.display.allocateID())
657	}
658
659	// Ensure proxy has a context set - CRITICAL FIX
660	if proxy.Context() == nil {
661		if baseProxy, ok := proxy.(*BaseProxy); ok {
662			baseProxy.SetContext(r.display.Context())
663		} else if setter, ok := proxy.(interface{ SetContext(*Context) }); ok {
664			setter.SetContext(r.display.Context())
665		} else {
666			return fmt.Errorf("proxy doesn't have context and can't set it")
667		}
668	}
669
670	// Register the proxy
671	proxy.Context().Register(proxy)
672
673	// Also register directly in display objects map
674	r.display.objects.Store(proxy.ID(), proxy)
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			return false
752		}
753
754		// Parse the new_id for the head object
755		headID := binary.LittleEndian.Uint32(body[0:4])
756
757		// Create a proper OutputHead object
758		headProxy := &OutputHead{
759			BaseProxy: BaseProxy{
760				id:      headID,
761				context: d.Context(),
762			},
763		}
764
765		// Register the new head object
766		d.objects.Store(headID, headProxy)
767		return true
768
769	case 1: // done event
770		return false
771
772	case 2: // finished event
773		return false
774	}
775
776	return false
777}
778
779// OutputHead represents a zwlr_output_head_v1 object
780type OutputHead struct {
781	BaseProxy
782	name        string
783	description string
784	width       int32
785	height      int32
786}
787
788// Dispatch handles head events
789func (h *OutputHead) Dispatch(event *Event) {
790	switch event.Opcode {
791	case 0: // name
792		h.name = event.String()
793	case 1: // description
794		h.description = event.String()
795	case 2: // physical_size
796		h.width = event.Int32()
797		h.height = event.Int32()
798	case 3: // mode (creates new mode object)
799		modeID := event.Uint32()
800		// Create mode object
801		mode := &OutputMode{
802			BaseProxy: BaseProxy{
803				id:      modeID,
804				context: h.context,
805			},
806		}
807		h.context.display.objects.Store(modeID, mode)
808	case 9: // finished
809		h.context.Unregister(h)
810	default:
811	}
812}
813
814// OutputMode represents a zwlr_output_mode_v1 object
815type OutputMode struct {
816	BaseProxy
817	width   int32
818	height  int32
819	refresh int32
820}
821
822// Dispatch handles mode events
823func (m *OutputMode) Dispatch(event *Event) {
824	switch event.Opcode {
825	case 0: // size
826		m.width = event.Int32()
827		m.height = event.Int32()
828	case 1: // refresh
829		m.refresh = event.Int32()
830	case 2: // preferred
831	case 3: // finished
832		m.context.Unregister(m)
833	default:
834	}
835}

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