feat/data-dnd e05cb15

powerkey.go RawHistoryFolder

1// Package powerkey takes logind's inhibitor locks for the power, suspend,
2// hibernate and reboot keys, so pressing one no longer powers the machine
3// off or to sleep by itself: the key reaches the compositor as a key event
4// (XF86PowerOff, XF86Sleep, ...) and only a bind acts on it.
5package powerkey
6
7import (
8	"context"
9	"errors"
10	"fmt"
11	"os"
12	"time"
13
14	"github.com/bnema/neferwl/internal/adapters/busretry"
15	"github.com/bnema/zerowrap"
16	"github.com/godbus/dbus/v5"
17)
18
19const (
20	logind     = "org.freedesktop.login1"
21	logindPath = "/org/freedesktop/login1"
22	manager    = "org.freedesktop.login1.Manager"
23	// busDaemon is the sender of the bus's own signals. Any client can send
24	// a NameOwnerChanged to us directly, so only this sender is trusted.
25	busDaemon = "org.freedesktop.DBus"
26	// what lists the logind actions NeferWL takes over.
27	what = "handle-power-key:handle-suspend-key:handle-hibernate-key:handle-reboot-key"
28	// callTimeout bounds the Inhibit call.
29	callTimeout = 3 * time.Second
30)
31
32// Serve holds the locks until ctx ends. address is the bus to use, the
33// system bus when empty. When the bus or logind goes, it takes the locks
34// again after retry, doubling up to a minute. Meanwhile the old lock stays
35// open: logind keeps an inhibitor across its own restart for as long as a
36// client holds the descriptor, so the keys stay blocked in the gap, and the
37// old lock is closed only once a new one is in place.
38func Serve(ctx context.Context, address string, retry time.Duration, log zerowrap.Logger) {
39	var lock *os.File
40	defer func() {
41		if lock != nil {
42			_ = lock.Close()
43		}
44	}()
45	busretry.Run(ctx, retry, "logind power key lock", log, func(ctx context.Context) error {
46		return hold(ctx, address, log, &lock)
47	})
48}
49
50// hold takes the locks, replaces *lock with them, and waits until ctx ends
51// (nil) or the bus or logind goes (an error).
52func hold(ctx context.Context, address string, log zerowrap.Logger, lock **os.File) error {
53	conn, err := connect(ctx, address)
54	if err != nil {
55		return err
56	}
57	defer func() { _ = conn.Close() }()
58	signals := make(chan *dbus.Signal, 16)
59	conn.Signal(signals)
60	// Ask for logind's departure before the lock exists: a restart between
61	// the two would go unseen.
62	if err := conn.AddMatchSignalContext(ctx, dbus.WithMatchSender(busDaemon), dbus.WithMatchInterface(busDaemon), dbus.WithMatchMember("NameOwnerChanged"), dbus.WithMatchArg(0, logind)); err != nil {
63		return fmt.Errorf("watch %s: %w", logind, err)
64	}
65	callCtx, cancel := context.WithTimeout(ctx, callTimeout)
66	defer cancel()
67	var fd dbus.UnixFD
68	if err := conn.Object(logind, logindPath).CallWithContext(callCtx, manager+".Inhibit", 0, what, "neferwl", "power keys are bound in the compositor", "block").Store(&fd); err != nil {
69		return fmt.Errorf("inhibit %s: %w", what, err)
70	}
71	// logind releases a lock when its descriptor closes.
72	old := *lock
73	*lock = os.NewFile(uintptr(fd), "logind-inhibit")
74	if old != nil {
75		_ = old.Close()
76	}
77	log.Info().Str("what", what).Msg("holding logind key locks")
78	for {
79		select {
80		case <-ctx.Done():
81			return nil
82		case sig, ok := <-signals:
83			if !ok {
84				if ctx.Err() != nil {
85					return nil // WithContext closed the connection
86				}
87				return errors.New("bus connection closed")
88			}
89			if lost(sig) {
90				return errors.New(logind + " left the bus")
91			}
92		}
93	}
94}
95
96func connect(ctx context.Context, address string) (*dbus.Conn, error) {
97	var conn *dbus.Conn
98	var err error
99	if address == "" {
100		conn, err = dbus.SystemBusPrivate(dbus.WithContext(ctx))
101	} else {
102		conn, err = dbus.Dial(address, dbus.WithContext(ctx))
103	}
104	if err != nil {
105		return nil, fmt.Errorf("bus: %w", err)
106	}
107	if err = conn.Auth(nil); err == nil {
108		err = conn.Hello()
109	}
110	if err != nil {
111		_ = conn.Close()
112		return nil, fmt.Errorf("bus: %w", err)
113	}
114	return conn, nil
115}
116
117// lost reports whether sig is the bus saying logind has no owner any more.
118func lost(sig *dbus.Signal) bool {
119	if sig.Sender != busDaemon || sig.Name != busDaemon+".NameOwnerChanged" || len(sig.Body) != 3 {
120		return false
121	}
122	name, ok1 := sig.Body[0].(string)
123	owner, ok2 := sig.Body[2].(string)
124	return ok1 && ok2 && name == logind && owner == ""
125}

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 25

Tags 10

↑↓ moveenter openesc close