seams.go RawHistoryFolder

1package screencast
2
3import (
4	"context"
5	"errors"
6	"fmt"
7	"os"
8	"os/exec"
9	"runtime"
10	"syscall"
11	"time"
12
13	"github.com/bnema/neferwl/internal/adapters/peerexe"
14	"github.com/bnema/purego-pipewire/pipewire"
15	"github.com/bnema/zerobus"
16)
17
18// bus is the part of a zerobus connection the service uses. Only Send and
19// ReadMessage are used after startup: a connection has one encoder and one
20// receive buffer, so Call and Handle are off limits.
21type bus interface {
22	NewReply(m *zerobus.Message, sig string) *zerobus.Encoder
23	NewError(m *zerobus.Message, name, sig string) *zerobus.Encoder
24	NewSignal(path, iface, member, sig string) *zerobus.Encoder
25	NewSignalTo(dest, path, iface, member, sig string) *zerobus.Encoder
26	NewMethodCall(dest, path, iface, member, sig string) (*zerobus.Encoder, uint32)
27	Send() (uint32, error)
28	ReadMessage() (*zerobus.Message, error)
29	Buffered() bool
30	SyscallConn() (syscall.RawConn, error)
31	AddMatch(rule string) error
32	RequestName(name string, flags uint32) (uint32, error)
33	Close() error
34}
35
36// videoSource is the part of a pipewire.VideoSource the producer uses. All
37// calls run on the loop thread.
38type videoSource interface {
39	NodeID() uint32
40	State() int32
41	Dequeue() *pipewire.VideoBuffer
42	Queue(b *pipewire.VideoBuffer, seq uint64, pts time.Duration, corrupted bool)
43	Release(b *pipewire.VideoBuffer)
44	Trigger()
45	Resize(w, h int) error
46	Destroy()
47}
48
49// videoCore is a PipeWire connection driven by the service's own poll loop.
50type videoCore interface {
51	// Fd is the pollable descriptor of the loop.
52	Fd() int
53	// Iterate dispatches pending events; handler callbacks run inside it.
54	Iterate()
55	NewVideoSource(cfg pipewire.VideoConfig, h pipewire.VideoHandler) (videoSource, error)
56	Close()
57}
58
59// peerResolver proves which executable a D-Bus peer runs: Open pins the
60// process behind a pid, Exe reads the executable through that pin.
61type peerResolver interface {
62	Open(pid int) (*os.File, error)
63	Exe(pidfd *os.File) (exe string, pid int, err error)
64}
65
66// runner runs nefercap with args.
67type runner interface {
68	Run(ctx context.Context, args []string) error
69}
70
71// dialer connects the session bus (address empty) or the bus at address.
72type dialer interface {
73	Dial(address string) (bus, error)
74}
75
76type zerobusDialer struct{}
77
78func (zerobusDialer) Dial(address string) (bus, error) {
79	if address == "" {
80		return zerobus.SessionBus()
81	}
82	return zerobus.Dial(address)
83}
84
85// pipewireOpener connects to the PipeWire daemon. Open runs on the goroutine
86// that stays locked to its OS thread: the loop belongs to that thread.
87type pipewireOpener interface {
88	Open() (videoCore, error)
89}
90
91type realPipeWire struct{}
92
93func (realPipeWire) Open() (videoCore, error) { return openPipeWire() }
94
95// pidfdPeers is peerResolver over the pidfd proof of package peerexe.
96type pidfdPeers struct{}
97
98func (pidfdPeers) Open(pid int) (*os.File, error) { return peerexe.Open(pid) }
99
100func (pidfdPeers) Exe(pidfd *os.File) (string, int, error) {
101	exe, pid, err := peerexe.ExeOfPidfd(int(pidfd.Fd()))
102	runtime.KeepAlive(pidfd)
103	return exe, pid, err
104}
105
106// nefercap is the program that takes the screenshots.
107const nefercap = "nefercap"
108
109// execRunner runs nefercap in the session's environment (WAYLAND_DISPLAY
110// names this compositor).
111type execRunner struct{ env []string }
112
113func (r execRunner) Run(ctx context.Context, args []string) error {
114	bin, err := exec.LookPath(nefercap)
115	if err != nil {
116		return err
117	}
118	cmd := exec.CommandContext(ctx, bin, args...)
119	// A cancelled run is joined by the owner: never wait long for pipes
120	// that a grandchild keeps open.
121	cmd.WaitDelay = 2 * time.Second
122	if len(r.env) > 0 {
123		cmd.Env = r.env
124	}
125	out, err := cmd.CombinedOutput()
126	if err != nil {
127		if len(out) > 256 {
128			out = out[len(out)-256:]
129		}
130		return fmt.Errorf("%s: %w: %s", nefercap, err, out)
131	}
132	return nil
133}
134
135// pwLoop is videoCore over a real PipeWire loop, context and core.
136type pwLoop struct {
137	rt   pipewire.Runtime
138	loop pipewire.Loop
139	ctx  pipewire.Context
140	core pipewire.Core
141}
142
143// openPipeWire connects to the PipeWire daemon.
144func openPipeWire() (videoCore, error) {
145	rt, err := pipewire.Init()
146	if err != nil {
147		return nil, err
148	}
149	p := &pwLoop{rt: rt}
150	if p.loop, err = rt.NewLoop(); err != nil {
151		p.Close()
152		return nil, err
153	}
154	if p.ctx, err = rt.NewContext(p.loop); err != nil {
155		p.Close()
156		return nil, err
157	}
158	if p.core, err = p.ctx.Connect(); err != nil {
159		p.Close()
160		return nil, err
161	}
162	return p, nil
163}
164
165func (p *pwLoop) Fd() int { return p.loop.Fd() }
166
167func (p *pwLoop) Iterate() { p.loop.Iterate() }
168
169func (p *pwLoop) NewVideoSource(cfg pipewire.VideoConfig, h pipewire.VideoHandler) (videoSource, error) {
170	vs, err := p.core.NewVideoSource(cfg, h)
171	if err != nil {
172		return nil, err
173	}
174	if vs == nil {
175		return nil, errors.New("pipewire returned no video source")
176	}
177	return vs, nil
178}
179
180// Close releases the connection, innermost first. Destroy the sources first.
181func (p *pwLoop) Close() {
182	if p.core != nil {
183		p.core.Disconnect()
184	}
185	if p.ctx != nil {
186		p.ctx.Destroy()
187	}
188	if p.loop != nil {
189		p.loop.Destroy()
190	}
191	p.rt.Deinit()
192}

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