detached c4a6bed

feat(cli): add -display and NEFERBAR_DISPLAY to pick the Wayland socket, and clear connect errors

commit
c4a6bed4cdeebf100b3d848aec8a90d941547e2a
author
brice · · 2026-10-05 14:15 UTC
parent
ec57f2f
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
4 files · +55 -19
M README.md +3 -1
@@ -15,7 +15,9 @@ go build -o neferbar ./cmd/neferbar
1515 ./neferbar -config examples/config.toml
1616 ```
1717
18-Flags: `-config <file>`, `-pprof <loopback addr>`, `-memstats <interval>`.
18+Flags: `-config <file>`, `-display <socket>`, `-pprof <loopback addr>`, `-memstats <interval>`.
19+
20+The Wayland socket comes from `-display`, then `$NEFERBAR_DISPLAY`, then `$WAYLAND_DISPLAY`, then `wayland-0`. A relative name lives in `$XDG_RUNTIME_DIR`; an absolute path is used as is.
1921
2022 ## Configuration
2123
@@ -15,7 +15,9 @@ go build -o neferbar ./cmd/neferbar
15 ./neferbar -config examples/config.toml15 ./neferbar -config examples/config.toml
16 ```16 ```
17 17
18-Flags: `-config <file>`, `-pprof <loopback addr>`, `-memstats <interval>`.18+Flags: `-config <file>`, `-display <socket>`, `-pprof <loopback addr>`, `-memstats <interval>`.
19+
20+The Wayland socket comes from `-display`, then `$NEFERBAR_DISPLAY`, then `$WAYLAND_DISPLAY`, then `wayland-0`. A relative name lives in `$XDG_RUNTIME_DIR`; an absolute path is used as is.
19 21
20 ## Configuration22 ## Configuration
21 23
M cmd/neferbar/main.go +6 -8
@@ -21,6 +21,7 @@ import (
2121
2222 func main() {
2323 cfgPath := flag.String("config", "", "config file (default: $XDG_CONFIG_HOME/neferbar/config.toml)")
24+ display := flag.String("display", os.Getenv("NEFERBAR_DISPLAY"), "Wayland socket name or absolute path (default: $NEFERBAR_DISPLAY, then $WAYLAND_DISPLAY)")
2425 pprofAddr := flag.String("pprof", "", "serve pprof on this loopback address, e.g. localhost:6060")
2526 memStats := flag.Duration("memstats", 0, "log allocation counters at this interval (0: off)")
2627 flag.Parse()
@@ -28,13 +29,13 @@ func main() {
2829 log := slog.New(slog.NewTextHandler(os.Stderr, nil))
2930 ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
3031 defer stop()
31- if err := run(ctx, log, *cfgPath, *pprofAddr, *memStats); err != nil {
32- log.Error("neferbar", "err", err)
32+ if err := run(ctx, log, *cfgPath, *display, *pprofAddr, *memStats); err != nil {
33+ fmt.Fprintf(os.Stderr, "neferbar: %v\n", err)
3334 os.Exit(1)
3435 }
3536 }
3637
37-func run(ctx context.Context, log *slog.Logger, cfgPath, pprofAddr string, memStats time.Duration) error {
38+func run(ctx context.Context, log *slog.Logger, cfgPath, display, pprofAddr string, memStats time.Duration) error {
3839 explicit := cfgPath != ""
3940 if !explicit {
4041 cfgPath = config.DefaultPath()
@@ -48,17 +49,14 @@ func run(ctx context.Context, log *slog.Logger, cfgPath, pprofAddr string, memSt
4849 return err
4950 }
5051 }
51- b, err := bar.New(cfg, log)
52+ b, err := bar.New(cfg, log, display)
5253 if err != nil {
5354 return err
5455 }
5556 if memStats > 0 {
5657 go logMemStats(ctx, log, memStats)
5758 }
58- err = b.Run(ctx)
59- log.Info("stats", "presented", b.Stats.Presented, "rebuilds", b.Stats.Rebuilds,
60- "recreated", b.Stats.Recreated, "no_slot", b.Stats.NoSlot, "dirty", b.Stats.Dirty, "module_wakes", b.Stats.ModuleWake)
61- return err
59+ return b.Run(ctx)
6260 }
6361
6462 // servePprof serves the pprof handlers on a loopback address only.
@@ -21,6 +21,7 @@ import (
21 21
22 func main() {22 func main() {
23 cfgPath := flag.String("config", "", "config file (default: $XDG_CONFIG_HOME/neferbar/config.toml)")23 cfgPath := flag.String("config", "", "config file (default: $XDG_CONFIG_HOME/neferbar/config.toml)")
24+ display := flag.String("display", os.Getenv("NEFERBAR_DISPLAY"), "Wayland socket name or absolute path (default: $NEFERBAR_DISPLAY, then $WAYLAND_DISPLAY)")
24 pprofAddr := flag.String("pprof", "", "serve pprof on this loopback address, e.g. localhost:6060")25 pprofAddr := flag.String("pprof", "", "serve pprof on this loopback address, e.g. localhost:6060")
25 memStats := flag.Duration("memstats", 0, "log allocation counters at this interval (0: off)")26 memStats := flag.Duration("memstats", 0, "log allocation counters at this interval (0: off)")
26 flag.Parse()27 flag.Parse()
@@ -28,13 +29,13 @@ func main() {
28 log := slog.New(slog.NewTextHandler(os.Stderr, nil))29 log := slog.New(slog.NewTextHandler(os.Stderr, nil))
29 ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)30 ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
30 defer stop()31 defer stop()
31- if err := run(ctx, log, *cfgPath, *pprofAddr, *memStats); err != nil {32+ if err := run(ctx, log, *cfgPath, *display, *pprofAddr, *memStats); err != nil {
32- log.Error("neferbar", "err", err)33+ fmt.Fprintf(os.Stderr, "neferbar: %v\n", err)
33 os.Exit(1)34 os.Exit(1)
34 }35 }
35 }36 }
36 37
37-func run(ctx context.Context, log *slog.Logger, cfgPath, pprofAddr string, memStats time.Duration) error {38+func run(ctx context.Context, log *slog.Logger, cfgPath, display, pprofAddr string, memStats time.Duration) error {
38 explicit := cfgPath != ""39 explicit := cfgPath != ""
39 if !explicit {40 if !explicit {
40 cfgPath = config.DefaultPath()41 cfgPath = config.DefaultPath()
@@ -48,17 +49,14 @@ func run(ctx context.Context, log *slog.Logger, cfgPath, pprofAddr string, memSt
48 return err49 return err
49 }50 }
50 }51 }
51- b, err := bar.New(cfg, log)52+ b, err := bar.New(cfg, log, display)
52 if err != nil {53 if err != nil {
53 return err54 return err
54 }55 }
55 if memStats > 0 {56 if memStats > 0 {
56 go logMemStats(ctx, log, memStats)57 go logMemStats(ctx, log, memStats)
57 }58 }
58- err = b.Run(ctx)59+ return b.Run(ctx)
59- log.Info("stats", "presented", b.Stats.Presented, "rebuilds", b.Stats.Rebuilds,
60- "recreated", b.Stats.Recreated, "no_slot", b.Stats.NoSlot, "dirty", b.Stats.Dirty, "module_wakes", b.Stats.ModuleWake)
61- return err
62 }60 }
63 61
64 // servePprof serves the pprof handlers on a loopback address only.62 // servePprof serves the pprof handlers on a loopback address only.
M internal/bar/bar.go +44 -8
@@ -13,7 +13,9 @@ import (
1313 "fmt"
1414 "log/slog"
1515 "math"
16+ "os"
1617 "path/filepath"
18+ "syscall"
1719
1820 "github.com/bnema/neferclient"
1921
@@ -40,9 +42,10 @@ type Stats struct {
4042
4143 // Bar is the running application.
4244 type Bar struct {
43- cfg config.Config
44- log *slog.Logger
45- fg, bg [3]uint8
45+ cfg config.Config
46+ log *slog.Logger
47+ display string
48+ fg, bg [3]uint8
4649
4750 conn *neferclient.Conn
4851 surf *neferclient.Surface
@@ -76,8 +79,9 @@ type Bar struct {
7679 }
7780
7881 // New builds a bar from a validated config.
79-func New(cfg config.Config, log *slog.Logger) (*Bar, error) {
80- b := &Bar{cfg: cfg, log: log, modCh: make(chan struct{}, 1)}
82+// display names the Wayland socket; empty uses $WAYLAND_DISPLAY.
83+func New(cfg config.Config, log *slog.Logger, display string) (*Bar, error) {
84+ b := &Bar{cfg: cfg, log: log, display: display, modCh: make(chan struct{}, 1)}
8185 var err error
8286 if b.fg, err = config.ParseColor(cfg.Bar.Foreground); err != nil {
8387 return nil, err
@@ -104,11 +108,15 @@ func New(cfg config.Config, log *slog.Logger) (*Bar, error) {
104108 // Run connects to the compositor and serves until ctx ends or the surface is
105109 // closed. It returns the first fatal error.
106110 func (b *Bar) Run(ctx context.Context) (err error) {
107- b.conn, err = neferclient.Connect(ctx, "")
111+ b.conn, err = neferclient.Connect(ctx, b.display)
108112 if err != nil {
109- return fmt.Errorf("connect: %w", err)
113+ return connectError(b.display, err)
110114 }
111- defer func() { err = errors.Join(err, b.shutdown()) }()
115+ defer func() {
116+ err = errors.Join(err, b.shutdown())
117+ b.log.Info("stats", "presented", b.Stats.Presented, "rebuilds", b.Stats.Rebuilds,
118+ "recreated", b.Stats.Recreated, "no_slot", b.Stats.NoSlot, "dirty", b.Stats.Dirty, "module_wakes", b.Stats.ModuleWake)
119+ }()
112120 if err = b.createSurface(estimateHeight(b.cfg.Bar.Size * b.cfg.Bar.Scale)); err != nil {
113121 return err
114122 }
@@ -138,6 +146,34 @@ func (b *Bar) Run(ctx context.Context) (err error) {
138146 return nil
139147 }
140148
149+// socketPath mirrors how neferclient resolves the socket, for error messages.
150+func socketPath(display string) string {
151+ if display == "" {
152+ display = os.Getenv("WAYLAND_DISPLAY")
153+ }
154+ if display == "" {
155+ display = "wayland-0"
156+ }
157+ if filepath.IsAbs(display) {
158+ return display
159+ }
160+ return filepath.Join(os.Getenv("XDG_RUNTIME_DIR"), display)
161+}
162+
163+// connectError turns a failed dial into one actionable sentence.
164+func connectError(display string, err error) error {
165+ path := socketPath(display)
166+ switch {
167+ case errors.Is(err, os.ErrNotExist):
168+ return fmt.Errorf("no Wayland socket at %s: is the compositor running? Set WAYLAND_DISPLAY, NEFERBAR_DISPLAY or -display", path)
169+ case errors.Is(err, syscall.ECONNREFUSED):
170+ return fmt.Errorf("the Wayland socket %s exists but refuses connections: stale socket or compositor not ready", path)
171+ case errors.Is(err, os.ErrPermission):
172+ return fmt.Errorf("not allowed to open the Wayland socket %s", path)
173+ }
174+ return err
175+}
176+
141177 // estimateHeight is the first request, before the real cell height is known.
142178 func estimateHeight(px float64) int32 { return int32(math.Ceil(px * 1.5)) }
143179
@@ -13,7 +13,9 @@ import (
13 "fmt"13 "fmt"
14 "log/slog"14 "log/slog"
15 "math"15 "math"
16+ "os"
16 "path/filepath"17 "path/filepath"
18+ "syscall"
17 19
18 "github.com/bnema/neferclient"20 "github.com/bnema/neferclient"
19 21
@@ -40,9 +42,10 @@ type Stats struct {
40 42
41 // Bar is the running application.43 // Bar is the running application.
42 type Bar struct {44 type Bar struct {
43- cfg config.Config45+ cfg config.Config
44- log *slog.Logger46+ log *slog.Logger
45- fg, bg [3]uint847+ display string
48+ fg, bg [3]uint8
46 49
47 conn *neferclient.Conn50 conn *neferclient.Conn
48 surf *neferclient.Surface51 surf *neferclient.Surface
@@ -76,8 +79,9 @@ type Bar struct {
76 }79 }
77 80
78 // New builds a bar from a validated config.81 // New builds a bar from a validated config.
79-func New(cfg config.Config, log *slog.Logger) (*Bar, error) {82+// display names the Wayland socket; empty uses $WAYLAND_DISPLAY.
80- b := &Bar{cfg: cfg, log: log, modCh: make(chan struct{}, 1)}83+func New(cfg config.Config, log *slog.Logger, display string) (*Bar, error) {
84+ b := &Bar{cfg: cfg, log: log, display: display, modCh: make(chan struct{}, 1)}
81 var err error85 var err error
82 if b.fg, err = config.ParseColor(cfg.Bar.Foreground); err != nil {86 if b.fg, err = config.ParseColor(cfg.Bar.Foreground); err != nil {
83 return nil, err87 return nil, err
@@ -104,11 +108,15 @@ func New(cfg config.Config, log *slog.Logger) (*Bar, error) {
104 // Run connects to the compositor and serves until ctx ends or the surface is108 // Run connects to the compositor and serves until ctx ends or the surface is
105 // closed. It returns the first fatal error.109 // closed. It returns the first fatal error.
106 func (b *Bar) Run(ctx context.Context) (err error) {110 func (b *Bar) Run(ctx context.Context) (err error) {
107- b.conn, err = neferclient.Connect(ctx, "")111+ b.conn, err = neferclient.Connect(ctx, b.display)
108 if err != nil {112 if err != nil {
109- return fmt.Errorf("connect: %w", err)113+ return connectError(b.display, err)
110 }114 }
111- defer func() { err = errors.Join(err, b.shutdown()) }()115+ defer func() {
116+ err = errors.Join(err, b.shutdown())
117+ b.log.Info("stats", "presented", b.Stats.Presented, "rebuilds", b.Stats.Rebuilds,
118+ "recreated", b.Stats.Recreated, "no_slot", b.Stats.NoSlot, "dirty", b.Stats.Dirty, "module_wakes", b.Stats.ModuleWake)
119+ }()
112 if err = b.createSurface(estimateHeight(b.cfg.Bar.Size * b.cfg.Bar.Scale)); err != nil {120 if err = b.createSurface(estimateHeight(b.cfg.Bar.Size * b.cfg.Bar.Scale)); err != nil {
113 return err121 return err
114 }122 }
@@ -138,6 +146,34 @@ func (b *Bar) Run(ctx context.Context) (err error) {
138 return nil146 return nil
139 }147 }
140 148
149+// socketPath mirrors how neferclient resolves the socket, for error messages.
150+func socketPath(display string) string {
151+ if display == "" {
152+ display = os.Getenv("WAYLAND_DISPLAY")
153+ }
154+ if display == "" {
155+ display = "wayland-0"
156+ }
157+ if filepath.IsAbs(display) {
158+ return display
159+ }
160+ return filepath.Join(os.Getenv("XDG_RUNTIME_DIR"), display)
161+}
162+
163+// connectError turns a failed dial into one actionable sentence.
164+func connectError(display string, err error) error {
165+ path := socketPath(display)
166+ switch {
167+ case errors.Is(err, os.ErrNotExist):
168+ return fmt.Errorf("no Wayland socket at %s: is the compositor running? Set WAYLAND_DISPLAY, NEFERBAR_DISPLAY or -display", path)
169+ case errors.Is(err, syscall.ECONNREFUSED):
170+ return fmt.Errorf("the Wayland socket %s exists but refuses connections: stale socket or compositor not ready", path)
171+ case errors.Is(err, os.ErrPermission):
172+ return fmt.Errorf("not allowed to open the Wayland socket %s", path)
173+ }
174+ return err
175+}
176+
141 // estimateHeight is the first request, before the real cell height is known.177 // estimateHeight is the first request, before the real cell height is known.
142 func estimateHeight(px float64) int32 { return int32(math.Ceil(px * 1.5)) }178 func estimateHeight(px float64) int32 { return int32(math.Ceil(px * 1.5)) }
143 179
M scripts/headless.sh +2 -2
@@ -2,7 +2,7 @@
22 # Run neferbar against a nested headless NeferWL with a clean environment.
33 # usage: scripts/headless.sh <config> <seconds> [neferbar args...]
44 # env: SIZE=WxH (output size), SCALE=1.5 (output scale), OUT=dir (screenshots),
5-# NEFERBAR=binary, LOGLINES=n (compositor log lines to show).
5+# DISPLAY_VAR=NEFERBAR_DISPLAY (pass the socket that way), NEFERBAR=binary, LOGLINES=n (compositor log lines to show).
66 set -eu
77 cfg=$1; secs=$2; shift 2
88 out=${OUT:-/tmp/neferbar-shots}
@@ -24,6 +24,6 @@ for _ in $(seq 100); do
2424 done
2525 [ -n "${sock:-}" ] || { echo "no compositor socket"; cat "$root/neferwl.log"; exit 1; }
2626 set +e
27-timeout "$secs" "${clean[@]}" WAYLAND_DISPLAY="$sock" "${NEFERBAR:-/tmp/neferbar}" -config "$cfg" "$@"
27+timeout "$secs" "${clean[@]}" "${DISPLAY_VAR:-WAYLAND_DISPLAY}=$sock" "${NEFERBAR:-/tmp/neferbar}" -config "$cfg" "$@"
2828 echo "neferbar exit: $?"
2929 echo "--- compositor log"; grep -v "not a conformant" "$root/neferwl.log" | tail -"${LOGLINES:-8}"; cp "$root/neferwl.log" /tmp/neferwl-last.log
@@ -2,7 +2,7 @@
2 # Run neferbar against a nested headless NeferWL with a clean environment.2 # Run neferbar against a nested headless NeferWL with a clean environment.
3 # usage: scripts/headless.sh <config> <seconds> [neferbar args...]3 # usage: scripts/headless.sh <config> <seconds> [neferbar args...]
4 # env: SIZE=WxH (output size), SCALE=1.5 (output scale), OUT=dir (screenshots),4 # env: SIZE=WxH (output size), SCALE=1.5 (output scale), OUT=dir (screenshots),
5-# NEFERBAR=binary, LOGLINES=n (compositor log lines to show).5+# DISPLAY_VAR=NEFERBAR_DISPLAY (pass the socket that way), NEFERBAR=binary, LOGLINES=n (compositor log lines to show).
6 set -eu6 set -eu
7 cfg=$1; secs=$2; shift 27 cfg=$1; secs=$2; shift 2
8 out=${OUT:-/tmp/neferbar-shots}8 out=${OUT:-/tmp/neferbar-shots}
@@ -24,6 +24,6 @@ for _ in $(seq 100); do
24 done24 done
25 [ -n "${sock:-}" ] || { echo "no compositor socket"; cat "$root/neferwl.log"; exit 1; }25 [ -n "${sock:-}" ] || { echo "no compositor socket"; cat "$root/neferwl.log"; exit 1; }
26 set +e26 set +e
27-timeout "$secs" "${clean[@]}" WAYLAND_DISPLAY="$sock" "${NEFERBAR:-/tmp/neferbar}" -config "$cfg" "$@"27+timeout "$secs" "${clean[@]}" "${DISPLAY_VAR:-WAYLAND_DISPLAY}=$sock" "${NEFERBAR:-/tmp/neferbar}" -config "$cfg" "$@"
28 echo "neferbar exit: $?"28 echo "neferbar exit: $?"
29 echo "--- compositor log"; grep -v "not a conformant" "$root/neferwl.log" | tail -"${LOGLINES:-8}"; cp "$root/neferwl.log" /tmp/neferwl-last.log29 echo "--- compositor log"; grep -v "not a conformant" "$root/neferwl.log" | tail -"${LOGLINES:-8}"; cp "$root/neferwl.log" /tmp/neferwl-last.log

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 5

Tags 4

↑↓ moveenter openesc close