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 Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· arch-gpg Verified for the committer email
6 files · +145 -1
M README.md +65 -1
| @@ -1,3 +1,67 @@ | |||
| 1 | 1 | # 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. | |
| @@ -1,3 +1,67 @@ | |||||
| 1 | # neferbar | 1 | # 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 | |||