detached ec57f2f

docs(readme): document usage and add example modules and headless test script

commit
ec57f2fbed492ea329bbf47e9d073a3f10957803
author
brice · · 2026-10-05 14:08 UTC
parent
6525562
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
6 files · +145 -1
M README.md +65 -1
@@ -1,3 +1,67 @@
11 # neferbar
22
3-A one-row, terminal-style status bar for Wayland (layer-shell). Modules are scripts that print ANSI.
3+A one-cell-high, terminal-style status bar for Wayland (wlr-layer-shell). Modules are scripts that print ANSI text; the bar draws it with a monospace Nerd Font through raw Vulkan. It is read-only, follows the monitor's fractional scale, and renders only when a module changes, so an animated module can reach the compositor's frame rate and an idle bar costs nothing.
4+
5+## Requirements
6+
7+- A compositor with `zwlr_layer_shell_v1`, `zwp_linux_dmabuf_v1` v4 and `wp_linux_drm_syncobj_v1` (and, for fractional scale, `wp_fractional_scale_v1` with `wp_viewporter`). GNOME is not supported.
8+- Linux 6.6 or newer, a Vulkan 1.3 driver with DMA-BUF export, and a Nerd Font installed (`fc-match` must find it).
9+- Go 1.27.
10+
11+## Build and run
12+
13+```sh
14+go build -o neferbar ./cmd/neferbar
15+./neferbar -config examples/config.toml
16+```
17+
18+Flags: `-config <file>`, `-pprof <loopback addr>`, `-memstats <interval>`.
19+
20+## Configuration
21+
22+`$XDG_CONFIG_HOME/neferbar/config.toml`:
23+
24+```toml
25+[bar]
26+font = "JetBrainsMono Nerd Font Mono"
27+size = 14 # logical pixels
28+scale = 1.0 # multiplier on top of the monitor scale
29+output = "" # wl_output name; empty lets the compositor choose
30+background = "#1e1e2e"
31+foreground = "#cdd6f4"
32+
33+[[module]]
34+name = "clock"
35+zone = "center" # left | center | right
36+exec = "clock.sh"
37+```
38+
39+## Modules
40+
41+A module is a long-running `/bin/sh -c` command. Its stdout is ANSI text (SGR colors, bold, inverse, dim). A frame ends at a newline or a form feed, and each frame replaces the previous one. Only the latest frame matters: a script that prints faster than the bar draws overwrites its pending frame.
42+
43+- Animate by printing frames quickly, ending each with `\f`. See `examples/modules/rainbow.sh`.
44+- A module that exits is restarted with backoff and shows `[name!]` while it is down. Its stderr goes to the bar's log.
45+- Left and right zones keep their text; the center is cut first when they would overlap.
46+
47+## Height and scale
48+
49+The bar is exactly one cell high: `cell = ceil(size × scale × monitor scale)` physical pixels, and the layer surface height is `ceil(cell / monitor scale)` logical pixels. When the monitor scale changes the font is rebuilt, and the surface is recreated if its height changes.
50+
51+## Profiling
52+
53+`-pprof localhost:6060` serves the pprof handlers on a loopback address and records every allocation. `-memstats 5s` logs allocation counters. The tests assert zero allocations per frame for the draw path, the layout and the module hand-off.
54+
55+## Testing
56+
57+```sh
58+go test ./...
59+scripts/headless.sh examples/config.toml 5 # nested headless NeferWL, screenshots in /tmp/neferbar-shots
60+SCALE=1.5 SIZE=1200x200 scripts/headless.sh examples/config.toml 5
61+```
62+
63+The GPU tests skip when no render node or Vulkan device is available.
64+
65+## License
66+
67+MIT.
@@ -1,3 +1,67 @@
1 # neferbar1 # neferbar
2 2
3-A one-row, terminal-style status bar for Wayland (layer-shell). Modules are scripts that print ANSI.3+A one-cell-high, terminal-style status bar for Wayland (wlr-layer-shell). Modules are scripts that print ANSI text; the bar draws it with a monospace Nerd Font through raw Vulkan. It is read-only, follows the monitor's fractional scale, and renders only when a module changes, so an animated module can reach the compositor's frame rate and an idle bar costs nothing.
4+
5+## Requirements
6+
7+- A compositor with `zwlr_layer_shell_v1`, `zwp_linux_dmabuf_v1` v4 and `wp_linux_drm_syncobj_v1` (and, for fractional scale, `wp_fractional_scale_v1` with `wp_viewporter`). GNOME is not supported.
8+- Linux 6.6 or newer, a Vulkan 1.3 driver with DMA-BUF export, and a Nerd Font installed (`fc-match` must find it).
9+- Go 1.27.
10+
11+## Build and run
12+
13+```sh
14+go build -o neferbar ./cmd/neferbar
15+./neferbar -config examples/config.toml
16+```
17+
18+Flags: `-config <file>`, `-pprof <loopback addr>`, `-memstats <interval>`.
19+
20+## Configuration
21+
22+`$XDG_CONFIG_HOME/neferbar/config.toml`:
23+
24+```toml
25+[bar]
26+font = "JetBrainsMono Nerd Font Mono"
27+size = 14 # logical pixels
28+scale = 1.0 # multiplier on top of the monitor scale
29+output = "" # wl_output name; empty lets the compositor choose
30+background = "#1e1e2e"
31+foreground = "#cdd6f4"
32+
33+[[module]]
34+name = "clock"
35+zone = "center" # left | center | right
36+exec = "clock.sh"
37+```
38+
39+## Modules
40+
41+A module is a long-running `/bin/sh -c` command. Its stdout is ANSI text (SGR colors, bold, inverse, dim). A frame ends at a newline or a form feed, and each frame replaces the previous one. Only the latest frame matters: a script that prints faster than the bar draws overwrites its pending frame.
42+
43+- Animate by printing frames quickly, ending each with `\f`. See `examples/modules/rainbow.sh`.
44+- A module that exits is restarted with backoff and shows `[name!]` while it is down. Its stderr goes to the bar's log.
45+- Left and right zones keep their text; the center is cut first when they would overlap.
46+
47+## Height and scale
48+
49+The bar is exactly one cell high: `cell = ceil(size × scale × monitor scale)` physical pixels, and the layer surface height is `ceil(cell / monitor scale)` logical pixels. When the monitor scale changes the font is rebuilt, and the surface is recreated if its height changes.
50+
51+## Profiling
52+
53+`-pprof localhost:6060` serves the pprof handlers on a loopback address and records every allocation. `-memstats 5s` logs allocation counters. The tests assert zero allocations per frame for the draw path, the layout and the module hand-off.
54+
55+## Testing
56+
57+```sh
58+go test ./...
59+scripts/headless.sh examples/config.toml 5 # nested headless NeferWL, screenshots in /tmp/neferbar-shots
60+SCALE=1.5 SIZE=1200x200 scripts/headless.sh examples/config.toml 5
61+```
62+
63+The GPU tests skip when no render node or Vulkan device is available.
64+
65+## License
66+
67+MIT.
A examples/config.toml +21 -0
@@ -0,0 +1,21 @@
1+[bar]
2+font = "JetBrainsMono Nerd Font Mono"
3+size = 14
4+scale = 1.0
5+background = "#1e1e2e"
6+foreground = "#cdd6f4"
7+
8+[[module]]
9+name = "static"
10+zone = "left"
11+exec = "examples/modules/static.sh"
12+
13+[[module]]
14+name = "clock"
15+zone = "center"
16+exec = "examples/modules/clock.sh"
17+
18+[[module]]
19+name = "rainbow"
20+zone = "right"
21+exec = "examples/modules/rainbow.sh 60 30"
@@ -0,0 +1,21 @@
1+[bar]
2+font = "JetBrainsMono Nerd Font Mono"
3+size = 14
4+scale = 1.0
5+background = "#1e1e2e"
6+foreground = "#cdd6f4"
7+
8+[[module]]
9+name = "static"
10+zone = "left"
11+exec = "examples/modules/static.sh"
12+
13+[[module]]
14+name = "clock"
15+zone = "center"
16+exec = "examples/modules/clock.sh"
17+
18+[[module]]
19+name = "rainbow"
20+zone = "right"
21+exec = "examples/modules/rainbow.sh 60 30"
A examples/modules/clock.sh +6 -0
@@ -0,0 +1,6 @@
1+#!/bin/sh
2+# One frame per second: the time, with a Nerd Font clock icon.
3+while :; do
4+ printf '\033[1;38;2;137;180;250m\xef\x80\x97 \033[0m%s\n' "$(date +%H:%M:%S)"
5+ sleep 1
6+done
@@ -0,0 +1,6 @@
1+#!/bin/sh
2+# One frame per second: the time, with a Nerd Font clock icon.
3+while :; do
4+ printf '\033[1;38;2;137;180;250m\xef\x80\x97 \033[0m%s\n' "$(date +%H:%M:%S)"
5+ sleep 1
6+done
A examples/modules/rainbow.sh +20 -0
@@ -0,0 +1,20 @@
1+#!/bin/sh
2+# 60 fps animation: a scrolling rainbow. Each frame ends with a form feed.
3+# usage: rainbow.sh [fps] [width]
4+fps=${1:-60}
5+width=${2:-40}
6+exec awk -v fps="$fps" -v w="$width" 'BEGIN {
7+ pi = 3.14159265; step = 1.0 / fps; t = 0
8+ while (1) {
9+ line = ""
10+ for (i = 0; i < w; i++) {
11+ p = (i * 0.25) - (t * 6)
12+ r = int(127 + 127 * sin(p)); g = int(127 + 127 * sin(p + 2 * pi / 3)); b = int(127 + 127 * sin(p + 4 * pi / 3))
13+ line = line sprintf("\033[48;2;%d;%d;%dm \033[0m", r, g, b)
14+ }
15+ printf "%s\f", line
16+ fflush()
17+ system("sleep " step)
18+ t += step
19+ }
20+}'
@@ -0,0 +1,20 @@
1+#!/bin/sh
2+# 60 fps animation: a scrolling rainbow. Each frame ends with a form feed.
3+# usage: rainbow.sh [fps] [width]
4+fps=${1:-60}
5+width=${2:-40}
6+exec awk -v fps="$fps" -v w="$width" 'BEGIN {
7+ pi = 3.14159265; step = 1.0 / fps; t = 0
8+ while (1) {
9+ line = ""
10+ for (i = 0; i < w; i++) {
11+ p = (i * 0.25) - (t * 6)
12+ r = int(127 + 127 * sin(p)); g = int(127 + 127 * sin(p + 2 * pi / 3)); b = int(127 + 127 * sin(p + 4 * pi / 3))
13+ line = line sprintf("\033[48;2;%d;%d;%dm \033[0m", r, g, b)
14+ }
15+ printf "%s\f", line
16+ fflush()
17+ system("sleep " step)
18+ t += step
19+ }
20+}'
A examples/modules/static.sh +4 -0
@@ -0,0 +1,4 @@
1+#!/bin/sh
2+# One frame, then idle: proves the bar costs nothing when nothing changes.
3+printf '\033[32mneferbar\033[0m \xef\x83\xa7 ready\n'
4+exec sleep 100000
@@ -0,0 +1,4 @@
1+#!/bin/sh
2+# One frame, then idle: proves the bar costs nothing when nothing changes.
3+printf '\033[32mneferbar\033[0m \xef\x83\xa7 ready\n'
4+exec sleep 100000
A scripts/headless.sh +29 -0
@@ -0,0 +1,29 @@
1+#!/bin/bash
2+# Run neferbar against a nested headless NeferWL with a clean environment.
3+# usage: scripts/headless.sh <config> <seconds> [neferbar args...]
4+# env: SIZE=WxH (output size), SCALE=1.5 (output scale), OUT=dir (screenshots),
5+# NEFERBAR=binary, LOGLINES=n (compositor log lines to show).
6+set -eu
7+cfg=$1; secs=$2; shift 2
8+out=${OUT:-/tmp/neferbar-shots}
9+root=$(mktemp -d)
10+mkdir -p "$root"/{run,config/neferwl,data,state} "$out"
11+chmod 700 "$root/run"
12+rm -f "$out"/*.png
13+if [ -n "${SCALE:-}" ]; then
14+ printf 'output.HEADLESS-1.scale = %s\n' "$SCALE" >"$root/config/neferwl/config"
15+fi
16+clean=(env -i PATH="$PATH" HOME="$root" XDG_RUNTIME_DIR="$root/run" XDG_CONFIG_HOME="$root/config" XDG_DATA_HOME="$root/data" XDG_STATE_HOME="$root/state")
17+"${clean[@]}" neferwl --backend=headless --no-terminal --no-xwayland --size "${SIZE:-800x200}" --timeout "$((secs + 6))s" --screenshot "$out" >"$root/neferwl.log" 2>&1 &
18+comp=$!
19+trap 'kill $comp 2>/dev/null || true; wait $comp 2>/dev/null || true' EXIT
20+for _ in $(seq 100); do
21+ sock=$(ls "$root"/run/wayland-* 2>/dev/null | grep -v lock | head -1 || true)
22+ [ -n "$sock" ] && break
23+ sleep 0.1
24+done
25+[ -n "${sock:-}" ] || { echo "no compositor socket"; cat "$root/neferwl.log"; exit 1; }
26+set +e
27+timeout "$secs" "${clean[@]}" WAYLAND_DISPLAY="$sock" "${NEFERBAR:-/tmp/neferbar}" -config "$cfg" "$@"
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.log
@@ -0,0 +1,29 @@
1+#!/bin/bash
2+# Run neferbar against a nested headless NeferWL with a clean environment.
3+# usage: scripts/headless.sh <config> <seconds> [neferbar args...]
4+# env: SIZE=WxH (output size), SCALE=1.5 (output scale), OUT=dir (screenshots),
5+# NEFERBAR=binary, LOGLINES=n (compositor log lines to show).
6+set -eu
7+cfg=$1; secs=$2; shift 2
8+out=${OUT:-/tmp/neferbar-shots}
9+root=$(mktemp -d)
10+mkdir -p "$root"/{run,config/neferwl,data,state} "$out"
11+chmod 700 "$root/run"
12+rm -f "$out"/*.png
13+if [ -n "${SCALE:-}" ]; then
14+ printf 'output.HEADLESS-1.scale = %s\n' "$SCALE" >"$root/config/neferwl/config"
15+fi
16+clean=(env -i PATH="$PATH" HOME="$root" XDG_RUNTIME_DIR="$root/run" XDG_CONFIG_HOME="$root/config" XDG_DATA_HOME="$root/data" XDG_STATE_HOME="$root/state")
17+"${clean[@]}" neferwl --backend=headless --no-terminal --no-xwayland --size "${SIZE:-800x200}" --timeout "$((secs + 6))s" --screenshot "$out" >"$root/neferwl.log" 2>&1 &
18+comp=$!
19+trap 'kill $comp 2>/dev/null || true; wait $comp 2>/dev/null || true' EXIT
20+for _ in $(seq 100); do
21+ sock=$(ls "$root"/run/wayland-* 2>/dev/null | grep -v lock | head -1 || true)
22+ [ -n "$sock" ] && break
23+ sleep 0.1
24+done
25+[ -n "${sock:-}" ] || { echo "no compositor socket"; cat "$root/neferwl.log"; exit 1; }
26+set +e
27+timeout "$secs" "${clean[@]}" WAYLAND_DISPLAY="$sock" "${NEFERBAR:-/tmp/neferbar}" -config "$cfg" "$@"
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.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