v0.2.1 c196c81

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
81	uid := uint32(os.Getuid())
82	sockPath, err := control.DefaultPath()
83	if err != nil {
84		return err
85	}
86	controlCh := make(chan ports.ControlRequest, 8)
87	srv, err := control.Listen(sockPath, uid, controlCh, o.Log)
88	if err != nil {
89		return fmt.Errorf("control socket: %w", err)
90	}
91
92	cfgCh := make(chan ports.ConfigChanged, 4)
93	failCh := make(chan error, 4)
94	wlEvents := make(chan ports.WaylandEvent, 64)
95	wlCmds := make(chan ports.WaylandCommand, 16)
96	sysEvents := make(chan ports.SystemEvent, 16)
97	sysCmds := make(chan ports.SystemCommand, 16)
98	visEvents := make(chan ports.VisualEvent, 16)
99
100	launcher := &visualproc.Launcher{Executable: o.Executable, Args: []string{"visual"}, Log: o.Log}
101	visual := visualproc.NewManager(ctx, launcher, visEvents, o.Log)
102	owner, err := NewOwner(Options{Config: cfg, Visual: visual, Log: o.Log, Gate: true, ExpectSystem: true},
103		Inputs{Config: cfgCh, Failures: failCh, Wayland: wlEvents, System: sysEvents, Visual: visEvents, Control: controlCh},
104		Outputs{Wayland: wlCmds, System: sysCmds})
105	if err != nil {
106		return errors.Join(err, srv.Close())
107	}
108	started = true
109
110	var wg sync.WaitGroup
111	var mu sync.Mutex
112	var first error
113	fail := func(name string, err error) {
114		if err == nil || ctx.Err() != nil && errors.Is(err, context.Canceled) {
115			return
116		}
117		mu.Lock()
118		if first == nil {
119			first = fmt.Errorf("%s: %w", name, err)
120		}
121		mu.Unlock()
122		cancel()
123	}
124	spawn := func(name string, fn func() error) {
125		wg.Add(1)
126		go func() { defer wg.Done(); fail(name, fn()) }()
127	}
128	spawn("config watch", func() error { return config.Watch(ctx, path, cfgCh, failCh, nil) })
129	spawn("wayland", func() error { return wl.Run(ctx, wlCmds, wlEvents) })
130	spawn("logind", func() error { return sys.Run(ctx, sysCmds, sysEvents) })
131	spawn("control", func() error { return srv.Serve(ctx) })
132
133	ownerErr := owner.Run(ctx)
134	cancel()
135	wg.Wait()
136	log.Info().Msg("daemon stopped")
137	var unsup *UnsupportedError
138	if errors.As(ownerErr, &unsup) {
139		return ownerErr
140	}
141	mu.Lock()
142	defer mu.Unlock()
143	if ownerErr != nil {
144		return ownerErr
145	}
146	return first
147}
148
149func dialWayland() (net.Conn, error) {
150	name := os.Getenv("WAYLAND_DISPLAY")
151	if name == "" {
152		return nil, errors.New("WAYLAND_DISPLAY is not set")
153	}
154	if !filepath.IsAbs(name) {
155		dir := os.Getenv("XDG_RUNTIME_DIR")
156		if dir == "" {
157			return nil, errors.New("XDG_RUNTIME_DIR is not set")
158		}
159		name = filepath.Join(dir, name)
160	}
161	c, err := net.Dial("unix", name)
162	if err != nil {
163		return nil, fmt.Errorf("connect compositor: %w", err)
164	}
165	return c, nil
166}

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