v0.1.0 d9218d6

drm.go RawHistoryFolder

1package app
2
3import (
4	"context"
5	"errors"
6	"fmt"
7	"path/filepath"
8	"runtime/debug"
9	"sort"
10	"sync"
11
12	"github.com/bnema/neferwl/internal/adapters/config"
13	"github.com/bnema/neferwl/internal/adapters/drm"
14	"github.com/bnema/neferwl/internal/adapters/seat"
15	"github.com/bnema/neferwl/internal/logging"
16	"github.com/bnema/neferwl/internal/ports"
17	"github.com/bnema/zerowrap"
18)
19
20var errTimeout = errors.New("timeout")
21
22// drmBackend is the seat and the cards with at least one usable output.
23type drmBackend struct {
24	seat  *seat.Seat
25	cards []*drmCard
26}
27
28type drmCard struct {
29	*drm.Card
30	fd int
31}
32
33// safe turns a panic in a hardware goroutine into an error, so the TTY is still restored.
34func safe(name string, fn func() error) (err error) {
35	defer func() {
36		if p := recover(); p != nil {
37			err = fmt.Errorf("%s panic: %v\n%s", name, p, debug.Stack())
38		}
39	}()
40	return fn()
41}
42
43// openDRM opens the seat and every card with a connected display. Outputs
44// are found by the first Scan of each card.
45func openDRM(ctx context.Context, cfg ports.Config) (*drmBackend, error) {
46	log := logging.For(ctx, "drm")
47	s, err := seat.Open(ctx, logging.For(ctx, "seat"))
48	if err != nil {
49		log.Error().Err(err).Msg("open seat")
50		return nil, fmt.Errorf("open seat: %w", err)
51	}
52	want := wantFromConfig(cfg)
53	paths, _ := filepath.Glob("/dev/dri/card[0-9]*")
54	sort.Strings(paths)
55	b := &drmBackend{seat: s}
56	var errs []error
57	for _, path := range paths {
58		fd, err := s.OpenDevice(path)
59		if err != nil {
60			errs = append(errs, err)
61			continue
62		}
63		card, err := drm.OpenCard(fd, path, want, log)
64		if err != nil {
65			log.Warn().Err(err).Str("card", path).Msg("card unusable")
66			errs = append(errs, err)
67			s.CloseDevice(fd)
68			continue
69		}
70		b.cards = append(b.cards, &drmCard{Card: card, fd: fd})
71	}
72	if len(b.cards) == 0 {
73		s.Close()
74		err = fmt.Errorf("no usable DRM card in %v: %w", paths, errors.Join(errs...))
75		log.Error().Err(err).Msg("open drm")
76		return nil, err
77	}
78	return b, nil
79}
80
81// wantFromConfig turns output.<name> and render.* entries into connector choices.
82func wantFromConfig(cfg ports.Config) drm.Want {
83	r := cfg.Render
84	want := drm.Want{Disabled: map[string]bool{}, Modes: map[string][3]float64{}, NoScanout: !r.DirectScanout, NoTearing: !r.Tearing, NoVRR: !r.VRR}
85	for _, o := range cfg.Outputs {
86		if o.Off {
87			want.Disabled[o.Name] = true
88			continue
89		}
90		if o.Mode != "" {
91			// Validated at config load.
92			w, h, hz, _ := config.ParseMode(o.Mode)
93			want.Modes[o.Name] = [3]float64{float64(w), float64(h), hz}
94		}
95	}
96	return want
97}
98
99// close releases the cards and the seat so the TTY comes back. Outputs are
100// closed by their goroutines first.
101func (b *drmBackend) close() {
102	for _, c := range b.cards {
103		b.seat.CloseDevice(c.fd)
104	}
105	b.seat.Close()
106}
107
108// runOutputs drives every output of every card: one goroutine per output,
109// one flip reader per card, and a udev watcher that rescans connectors on
110// hotplug. Core learns about outputs through events. It returns when ctx
111// ends, after every output is closed.
112func (b *drmBackend) runOutputs(ctx context.Context, want func() drm.Want, events chan<- ports.OutputEvent, scenes <-chan []ports.Scene, contents <-chan ports.SurfaceContent, cursorChanges <-chan ports.CursorChange, presented chan<- ports.OutputPresented, formats chan<- ports.OutputFormats, curs *cursors, newRenderer func(w, h int) (ports.Renderer, error), log zerowrap.Logger) error {
113	ctx, cancel := context.WithCancel(ctx)
114	defer cancel()
115	var readers sync.WaitGroup
116	readerErr := make(chan error, len(b.cards))
117	for _, c := range b.cards {
118		c.SetFormats(formats)
119		readers.Go(func() {
120			if err := safe("drm events", func() error { return c.ReadEvents(ctx) }); err != nil {
121				readerErr <- fmt.Errorf("%s: %w", c.Path(), err)
122			}
123		})
124	}
125	defer readers.Wait()
126	hotplug := make(chan struct{}, 1)
127	go func() {
128		if err := safe("hotplug", func() error { return drm.WatchHotplug(ctx, hotplug) }); err != nil {
129			log.Warn().Err(err).Msg("hotplug watch disabled")
130		}
131	}()
132	set := newOutputSet()
133	cards := map[string]*drmCard{}
134	send := func(ev ports.OutputEvent) {
135		select {
136		case events <- ev:
137		case <-ctx.Done():
138		}
139	}
140	// stopping holds outputs asked to stop: true when they restart with a
141	// new mode (core keeps the screen), false when they are gone.
142	stopping := map[string]bool{}
143	scan := func() {
144		for _, c := range b.cards {
145			w := want()
146			w.Device = c.Device()
147			c.SetWant(w)
148			added, removed, replaced, err := c.Scan()
149			if err != nil {
150				log.Warn().Err(err).Msg("scan connectors")
151				continue
152			}
153			stop := func(name string, restart bool) {
154				if r := set.outs[name]; r != nil && cards[name] == c {
155					if _, ok := stopping[name]; !ok {
156						stopping[name] = restart
157						r.stop()
158					}
159				}
160			}
161			for _, name := range removed {
162				log.Info().Str("connector", name).Msg("output unplugged")
163				stop(name, false)
164			}
165			for _, name := range replaced {
166				log.Info().Str("connector", name).Msg("output mode change")
167				stop(name, true)
168			}
169			for _, o := range added {
170				name := o.Info().Name
171				if other := cards[name]; other != nil && other != c {
172					// Core and clients know outputs by connector name.
173					log.Warn().Str("connector", name).Str("card", c.Path()).Msg("connector name already used by another card; ignored")
174					o.Close()
175					c.Release(name)
176					continue
177				}
178				cards[name] = c
179				if cur := o.Cursor(); cur != nil {
180					curs.set(name, cur)
181				}
182				active := b.seat.Subscribe()
183				set.start(ctx, name, func(octx context.Context, sc <-chan ports.Scene, cc <-chan ports.SurfaceContent, cu <-chan ports.CursorChange) error {
184					defer b.seat.Unsubscribe(active)
185					return safe("output "+name, func() error {
186						defer o.Close()
187						return o.Run(octx, newRenderer, loadCursor, active, sc, cc, cu, presented)
188					})
189				})
190				send(ports.OutputAdded{Info: o.Info()})
191			}
192		}
193	}
194	scan()
195	if len(set.outs) == 0 {
196		return errors.Join(errors.New("no connected display"), set.wait())
197	}
198	for {
199		select {
200		case <-ctx.Done():
201			return set.wait()
202		case err := <-readerErr:
203			// No more flips on that card: its outputs would freeze.
204			return errors.Join(err, set.wait())
205		case <-hotplug:
206			log.Info().Msg("hotplug")
207			scan()
208		case s := <-scenes:
209			set.scenes(s)
210		case c := <-contents:
211			set.content(c)
212		case c := <-cursorChanges:
213			set.setCursor(c)
214		case name := <-set.stopped:
215			err := set.finish(name)
216			if ctx.Err() != nil {
217				return errors.Join(err, set.wait())
218			}
219			restart := stopping[name]
220			delete(stopping, name)
221			curs.set(name, nil)
222			cards[name].Release(name)
223			delete(cards, name)
224			if err != nil {
225				// A broken output (e.g. its renderer) must not take the
226				// session down. It stays off until the next hotplug, so a
227				// lasting failure does not loop.
228				log.Error().Err(err).Str("connector", name).Msg("output stopped")
229				restart = false
230			}
231			if restart {
232				// Same connector, new mode: core keeps its screen and
233				// workspaces; OutputAdded updates the size.
234				scan()
235				if set.outs[name] != nil {
236					continue
237				}
238			}
239			send(ports.OutputRemoved{Name: name})
240		}
241	}
242}

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 23

Tags 10

↑↓ moveenter openesc close