main c21f374

daemon.go RawHistoryFolder

1package app
2
3import (
4	"context"
5	"errors"
6	"fmt"
7	"net"
8	"os"
9	"path/filepath"
10	"sync"
11
12	"github.com/bnema/neferafk/internal/adapters/config"
13	"github.com/bnema/neferafk/internal/adapters/control"
14	"github.com/bnema/neferafk/internal/adapters/logind"
15	"github.com/bnema/neferafk/internal/adapters/visualproc"
16	"github.com/bnema/neferafk/internal/adapters/wayland"
17	"github.com/bnema/neferafk/internal/ports"
18	"github.com/bnema/zerowrap"
19)
20
21// DaemonOptions configures RunDaemon.
22type DaemonOptions struct {
23	ConfigPath string // empty: config.DefaultPath()
24	// Executable is re-executed as `<Executable> visual` for the visual process.
25	Executable string
26	Log        zerowrap.Logger
27}
28
29// RunDaemon wires the real adapters around one Owner. It returns nil on a
30// requested shutdown, *UnsupportedError for missing capabilities, or the first
31// fatal adapter error. Nothing in the daemon initialises Vulkan or fonts.
32func RunDaemon(ctx context.Context, o DaemonOptions) (result error) {
33	log := o.Log.WithField("component", "daemon")
34	path := o.ConfigPath
35	if path == "" {
36		path = config.DefaultPath()
37	}
38	cfg, err := config.Load(path)
39	switch {
40	case errors.Is(err, os.ErrNotExist):
41		cfg = ports.Defaults()
42	case err != nil:
43		return fmt.Errorf("config: %w", err)
44	}
45	ctx, cancel := context.WithCancel(ctx)
46	defer cancel()
47
48	wlReq, sysReq := Requirements(cfg)
49	conn, err := dialWayland()
50	if err != nil {
51		return &UnsupportedError{Missing: []string{"wayland: " + err.Error()}}
52	}
53	// Until the adapters own their resources (started), every early return
54	// releases the compositor connection and logind, joining Close errors.
55	started := false
56	var sys *logind.Adapter
57	defer func() {
58		if started {
59			return
60		}
61		if e := conn.Close(); e != nil && !errors.Is(e, net.ErrClosed) {
62			result = errors.Join(result, fmt.Errorf("close wayland connection: %w", e))
63		}
64		if sys != nil {
65			if e := sys.Close(); e != nil {
66				result = errors.Join(result, fmt.Errorf("close logind: %w", e))
67			}
68		}
69	}()
70	// Initial deadlines come from the owner's first ArmIdle (SetDeadlines).
71	wl, err := wayland.New(conn, wayland.Options{Requirements: wlReq, Generation: 1}, o.Log)
72	if err != nil {
73		return fmt.Errorf("wayland: %w", err)
74	}
75	sys, err = logind.Open(ctx, sysReq)
76	if err != nil {
77		sys = nil
78		return &UnsupportedError{Missing: []string{"logind: " + err.Error()}}
79	}
80	if path, source := sys.Session(); path != "" {
81		log.Info().Str("session", path).Str("source", source).Msg("logind session bound")
82	}
83
84	uid := uint32(os.Getuid())
85	sockPath, err := control.DefaultPath()
86	if err != nil {
87		return err
88	}
89	controlCh := make(chan ports.ControlRequest, 8)
90	srv, err := control.Listen(sockPath, uid, controlCh, o.Log)
91	if err != nil {
92		return fmt.Errorf("control socket: %w", err)
93	}
94
95	cfgCh := make(chan ports.ConfigChanged, 4)
96	failCh := make(chan error, 4)
97	wlEvents := make(chan ports.WaylandEvent, 64)
98	wlCmds := make(chan ports.WaylandCommand, 16)
99	sysEvents := make(chan ports.SystemEvent, 16)
100	sysCmds := make(chan ports.SystemCommand, 16)
101	visEvents := make(chan ports.VisualEvent, 16)
102
103	launcher := &visualproc.Launcher{Executable: o.Executable, Args: []string{"visual"}, Log: o.Log}
104	visual := visualproc.NewManager(ctx, launcher, visEvents, o.Log)
105	owner, err := NewOwner(Options{Config: cfg, Visual: visual, Log: o.Log, Gate: true, ExpectSystem: true},
106		Inputs{Config: cfgCh, Failures: failCh, Wayland: wlEvents, System: sysEvents, Visual: visEvents, Control: controlCh},
107		Outputs{Wayland: wlCmds, System: sysCmds})
108	if err != nil {
109		return errors.Join(err, srv.Close())
110	}
111	started = true
112
113	var wg sync.WaitGroup
114	var mu sync.Mutex
115	var first error
116	fail := func(name string, err error) {
117		if err == nil || ctx.Err() != nil && errors.Is(err, context.Canceled) {
118			return
119		}
120		mu.Lock()
121		if first == nil {
122			first = fmt.Errorf("%s: %w", name, err)
123		}
124		mu.Unlock()
125		cancel()
126	}
127	spawn := func(name string, fn func() error) {
128		wg.Add(1)
129		go func() { defer wg.Done(); fail(name, fn()) }()
130	}
131	spawn("config watch", func() error { return config.Watch(ctx, path, cfgCh, failCh, nil) })
132	spawn("wayland", func() error { return wl.Run(ctx, wlCmds, wlEvents) })
133	spawn("logind", func() error { return sys.Run(ctx, sysCmds, sysEvents) })
134	spawn("control", func() error { return srv.Serve(ctx) })
135
136	ownerErr := owner.Run(ctx)
137	cancel()
138	wg.Wait()
139	log.Info().Msg("daemon stopped")
140	var unsup *UnsupportedError
141	if errors.As(ownerErr, &unsup) {
142		return ownerErr
143	}
144	mu.Lock()
145	defer mu.Unlock()
146	if ownerErr != nil {
147		return ownerErr
148	}
149	return first
150}
151
152func dialWayland() (net.Conn, error) {
153	name := os.Getenv("WAYLAND_DISPLAY")
154	if name == "" {
155		return nil, errors.New("WAYLAND_DISPLAY is not set")
156	}
157	if !filepath.IsAbs(name) {
158		dir := os.Getenv("XDG_RUNTIME_DIR")
159		if dir == "" {
160			return nil, errors.New("XDG_RUNTIME_DIR is not set")
161		}
162		name = filepath.Join(dir, name)
163	}
164	c, err := net.Dial("unix", name)
165	if err != nil {
166		return nil, fmt.Errorf("connect compositor: %w", err)
167	}
168	return c, nil
169}

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 2

Tags 9

↑↓ moveenter openesc close