____ _____ _______ ____ _ ___ _ _ _____ ____ ____
| _ \ / _ \ \ / / ____| _ \| | |_ _| \ | | ____| _ \/ ___|
| |_) | | | \ \ /\ / /| _| | |_) | | | || \| | _| | |_) \___ \
| __/| |_| |\ V V / | |___| _ <| |___ | || |\ | |___| _ < ___) |
|_| \___/ \_/\_/ |_____|_| \_\_____|___|_| \_|_____|_| \_\____/
Read the Docs · Engineering Report
[SIGNAL // POWERLINE WITHOUT THE PYTHON IMPORT COST]
// jacking your prompt off the python interpreter — same segments, same theme grammar, native exec speed //
> SYSTEM OVERVIEW
powerliners is a Rust port of powerline-status — the canonical Python-driven statusline/prompt renderer used in tmux, zsh, bash, vim, ipython, and shell continuation lines. The Python implementation pays a ~50–150 ms interpreter-startup tax on every render (every prompt redraw, every tmux refresh). powerliners is a single static binary: zero-import, zero-GC, sub-millisecond render.
Drop-in compatible with the existing powerline/config JSON theme + segment files so users can keep their themes unchanged.
> WHY A PORT?
[x] python startup is the killer — ~100 ms per render on the default tmux+powerline setup
[x] tmux refreshes the statusline every interval, and per-window — startup cost compounds
[x] zsh's prompt redraws after every keystroke when `precmd` hooks fire
[x] a 100 ms latency tax on every keystroke-induced redraw turns interactive shells into slideshows
[x] rust gives us: a static binary, microsecond startup, zero runtime deps, cross-arch builds
[x] preserve the exact powerline theme grammar — users keep their .json themes verbatim
> TARGETS
[x] tmux statusline / continuation lines
[x] zsh prompt (PS1 / RPROMPT)
[x] bash prompt (PS1 / PROMPT_COMMAND)
[x] vim statusline
[x] ipython / python REPL prompt (via shell hook, not embedded)
> STATUS
[port progress] 134 / 137 upstream .py files at DONE tier (97.8%)
[remaining] 3 lipstick-only py files — zero-fn class shells, see Status
[partial/sparse] 0 / 0 — no degraded files
[lib tests] 2473 passing, 0 failing, 0 ignored
[parity tests] 462 against live upstream Python — every assertion runs the
Python interpreter on the upstream powerline source and
compares byte/value identical with the Rust port
[port bugs fixed] 11 surfaced by the parity harness and corrected in the
Rust port (see git log for the full list)
[drift gate] green — every ported fn name matches docs/powerline_py_functions.txt
[citation rule] every Rust body line annotated // py:NNN against the upstream source line
The port is structurally complete at the function level. Citation-density
tier classifier (scripts/gen_port_checklist.py) requires // py:NNN
citation density >= 0.5 plus a /// Port of <py_fn>() doccomment per
Python function for DONE classification. All upstream Python files with
function bodies are at DONE.
The 3 remaining NEAR files are Python lipstick — there is no behavior left to port:
renderers/shell/readline.py(14 LOC) — a class shell with two string constants (escape_hl_start = '\x01',escape_hl_end = '\x02') and one module-level alias.py_fn_total == 0.renderers/shell/zsh.py(16 LOC) — same shape, different escape constants.py_fn_total == 0.bindings/i3/powerline-i3.py(52 LOC) — onerenderfunction which is already 100% ported (seedocs/port_report.html); the classifier still tags it NEAR because the file structure trips its class-only branch.
These exist in upstream because Python's renderer registry needs a
class per shell flavour to subclass ShellRenderer; the Rust port
holds the same escape-marker constants directly on the equivalent
renderer struct without ceremony. The classifier's py_fn_total == 0
denominator simply can't promote 0/0 → DONE — a classifier amendment
would resolve it cosmetically, but there is no real port gap. The
remaining behavioral surface (every Python function with a body) is
at DONE.
What's wired end-to-end
| Binary | Mirrors | What it does |
|---|---|---|
powerline |
client/powerline.c |
Native Rust client — forwards argv + cwd + env to the daemon over a Unix socket via the upstream wire format, falls back to powerline-render exec if the daemon is unreachable |
powerline-config |
scripts/powerline-config |
tmux / shell known-function dispatch |
powerline-lint |
scripts/powerline-lint |
argparse + full check pipeline (markedjson loader + Spec checks + orchestrator integration) |
powerline-render |
scripts/powerline-render |
argparse + ext lookup + full direct-render path through the Powerline orchestrator (used as daemon-less fallback) |
powerline-daemon |
scripts/powerline-daemon |
UNIX-socket bind + daemonize + pidfile lock + accept loop + EOF shutdown + end-to-end statusline rendering against a real ~/.config/powerline/themes/... JSON tree |
End-to-end render
powerline-daemon produces real #[fg=…,bg=…]… tmux markup from a
user's powerline config root via the wire format compatible with the
upstream Python powerline C client. The render path covers:
- Config cascade load (
_find_config_files+load_json_config+mergedicts) - Colorscheme alias chasing + cterm color resolution (
Colorscheme::get_highlighting) - Segment preparation via
gen_segment_getterreturning aTheme.segmentstable - Segment dispatch through
process_segment/process_segment_lister - Renderer loop (
do_render/_render_segments/_render_length) with hard/soft divider insertion and per-side outer padding - TmuxRenderer
#[…]markup emission withterm_truecolorcterm path
54 segment adapters wired in src/bin/shared/render_runtime.rs (55
ADAPTERS keys including the bare exec alias; shared
between powerline-daemon and powerline-render): battery, branch,
clementine, cmus, containers, cpu_load_percent, cwd, date,
dbus_player, disk_io, disk_usage, disk_usage_percent,
email_imap_alert, environment, exec, external_ip, fuzzy_time,
gpu_usage_percent, gpu_vram, hostname, internal_ip, itunes,
jobnum, kubecontext, last_pipe_status, last_status, mem_swap,
mem_swap_percentage, mem_usage, mem_usage_percent, mocp, mpd,
network_load, process_count, rhythmbox, spotify, stash,
system_load, thermal, uptime, user, virtualenv, weather,
plus git_status, ci_status, aws.context, gcp.context,
fusevm.jit_cache, and the rkyv_cache / version adapters for
zshrs, stryke, and awkrs.
Point it at a config root via POWERLINE_CONFIG_PATHS:
POWERLINE_CONFIG_PATHS=/.config/powerline
The Python powerline C client already installed via pip talks to it
unchanged — same argc\0arg\0arg\0cwd\0KEY=VAL\0...\0\0 wire format
and same EOF\0\0 shutdown sentinel.
Regenerate the per-file tier table from the live source via:
Regenerate the function-coverage report via:
> MIGRATION TUTORIAL
Drop-in replacement for the Python powerline-daemon. The C client
shipped with powerline-status (installed via pip install powerline-status) talks to our daemon unchanged — same wire format,
same EOF\0\0 shutdown, same socket path (/tmp/powerline-ipc-$UID
on macOS / BSD, abstract \0powerline-ipc-$UID on Linux).
Step 1: Install or build
Fastest path — Homebrew tap (auto-bumped by each release):
# installs powerline, powerline-daemon, powerline-config, powerline-render, powerline-lint
Or build from source (entire 5-binary suite):
Release binaries land at target/release/{powerline,powerline-daemon,powerline-config,powerline-render,powerline-lint}.
Step 2: Verify parity against your config
Before swapping anything, run the daemon against a copy of your real config root and confirm the rendered tmux markup matches what you currently see:
# Spawn our daemon on a throwaway socket
POWERLINE_CONFIG_PATHS=/.config/powerline \
&
# Render the right side via the wire protocol
# Compare against the Python upstream (powerline-status must be installed)
If the two outputs match byte-for-byte for your common segments,
proceed. If they don't, file an issue with the divergence — the
daemon_parity suite covers 45 byte-for-byte scenarios but real
configs hit combinations we haven't asserted on.
Step 3: Stop the Python daemon
Step 4: Replace the binary in $PATH
The simplest approach is symlinking the Rust binary into a directory
that comes before powerline-status's ~/.local/bin (or wherever
pip put it) in your $PATH:
# verify the new resolution
The Python C client at ~/.local/bin/powerline (or powerline-render
as fallback) does NOT need to be replaced — it speaks the same wire
protocol to whichever daemon is bound to the socket.
Step 5: Restart tmux
Your existing ~/.tmux.conf invocations work unchanged. The
canonical lines:
run-shell "powerline-config tmux setup"
run-shell -b "powerline-daemon -q &>/dev/null || exit 0"
powerline-config tmux setup is install-method-agnostic as of
0.2.3 — the 8 tmux conf files (powerline-base.conf plus 7
version-specific variants) are embedded into the binary via
include_str! and extracted to $XDG_CACHE_HOME/powerliners/tmux/
(default ~/.cache/powerliners/tmux/) on first call. Works
identically for cargo install, brew install, and manual cp
into $PATH — no compile-time path baking required.
Kill and reattach tmux to confirm:
The status bar should look identical. The powerline-daemon process
in ps aux should now be a Rust-port invocation rather than the
Python shebang.
Step 6: Confirm
# expected: /usr/local/bin/powerline-daemon -q
# expected: lrwxr-xr-x ... -> .../target/release/powerline-daemon
Rollback
Re-resolve powerline-daemon to the Python script:
Everything is byte-compatible — no config edits, no .tmux.conf edits,
no shell-rc edits.
> VIM SETUP
Vim statusline + tabline rendering ships in the same powerline binary
— no +python3 requirement, no pip install powerline-status. The
bundled powerline.vim plugin is embedded in the binary via
include_str! and extracted to ~/.cache/powerliners/vim/powerline.vim
on first source.
Works on vim 7.4+, vim 9, and neovim — the plugin uses vim8-compatible
syntax only (no vim9script lock-in).
Step 1: Source the plugin from .vimrc
The one-line install that extracts and sources the plugin on every vim launch:
if executable('powerline-config')
execute 'source' trim(system('powerline-config vim source-path'))
endif
powerline-config vim source-path extracts the bundled plugin and
prints its path; the :execute 'source' … then sources it. Caches in
$XDG_CACHE_HOME/powerliners/vim/ (default ~/.cache/powerliners/vim/)
so subsequent vim launches re-source from the already-extracted file.
Or the manual two-step (pin the path, skip the per-launch fork):
# one-time extraction
# /Users/<you>/.cache/powerliners/vim/powerline.vim
" .vimrc
set runtimepath+=~/.cache/powerliners/vim
source ~/.cache/powerliners/vim/powerline.vim
Step 2: Pre-start the daemon (recommended)
The plugin shells out via system('powerline vim left …') on every
statusline refresh. With the daemon running, that's a microsecond
Unix-socket round-trip; without it, every refresh fork-execs
powerline-render which is ~30 ms cold per call. Start the daemon at
shell login (zsh .zshrc or bash .bash_profile):
The -q flag double-forks and detaches; vim sees the socket
immediately. No per-vim daemon — one process per UID handles tmux,
shell prompts, and every running vim simultaneously.
Step 3: Verify
Open a fresh vim session and check:
:echo &statusline
" expected: a %#Pl_…# markup string ending in segment chunks
:PowerlinersRefresh
" manually re-runs the refresh; useful for debugging
:messages
" no errors should appear; if 'powerline: write() to daemon failed'
" shows up, the daemon isn't reachable — re-run `powerline-daemon -q`
The plugin sets laststatus=2 automatically so the statusline shows
even in single-window sessions.
What the plugin wires up
Triggered autocmds (augroup powerliners):
| Event | When it fires |
|---|---|
VimEnter |
initial render on launch |
WinEnter / BufWinEnter / BufEnter / TabEnter |
refresh on context switch |
ModeChanged (vim ≥ 8.2.2871) |
mode transitions (normal → insert etc) |
CursorMoved / CursorMovedI (legacy fallback) |
every cursor move on ancient vim |
Per-request keys sent to the renderer (matches upstream's
powerline.bindings.vim so theme JSON written for Python upstream
renders identically here):
mode— current vim mode (n/i/v/R/ …)bufnr—bufnr('%')winnr—winnr()buf_name—expand('%:p')(omitted when empty)
The bundled powerline.vim shows the actual wiring at the source
path you printed above.
Override the binary name
If you've installed under a non-default name or want to test a build
from target/release/, set the global before sourcing the plugin:
let g:powerliners_binary = expand('~/code/powerliners/target/release/powerline')
if executable(g:powerliners_binary)
execute 'source' trim(system(g:powerliners_binary . '-config vim source-path'))
endif
Customize the theme
The vim statusline pulls from
~/.config/powerline/themes/vim/default.json plus
~/.config/powerline/colorschemes/vim/default.json. The bundled
defaults live under src/ported/config_files/themes/vim/ and
src/ported/config_files/colorschemes/vim/ for reference; copy any
of them into ~/.config/powerline/ and edit.
Same JSON shape as upstream powerline — segments listed under
segments.left / segments.right, theme inheritance via
extends, per-mode highlight overrides via mode_translations.
Troubleshooting
| Symptom | Cause / fix |
|---|---|
| empty statusline | set laststatus=2 got stomped by something else in .vimrc; re-set after sourcing |
| garbled escape codes | terminal doesn't support truecolor; renderer falls back to cterm but vim must be in a 256-color tty ($TERM=xterm-256color) |
| colors don't match terminal | colorscheme JSON missing your custom palette — copy colorschemes/vim/default.json into ~/.config/powerline/colorschemes/vim/ |
powerline: write() to daemon failed |
daemon isn't running; run powerline-daemon -q |
| refresh stutters / flickers | the legacy CursorMoved fallback is firing on every keystroke (vim < 8.2.2871) — upgrade vim or pin a let g:powerliners_no_cursormoved = 1 patch |
E121: Undefined variable: g:powerliners_binary before source line |
g:powerliners_binary is set BY the plugin; reference it only inside autocmds that fire after sourcing |
Known divergences from Python upstream
These are the only areas where output may differ. Each is documented under the test suite's "inherent divergence" notes:
- Live-data segments (
cpu_load_percent,network_load, ...) are sampled per-render via subprocess probes in our daemon; the Python upstream usespsutilwith a different sampling cadence. Numeric values may differ by a sampling-window's worth of data; the markup framing is identical. - Threaded segment caching: Python's
ThreadedSegmentpolls in a background thread and renders the last-known value; our daemon samples on-demand. Latency profile differs (we may block briefly when network/disk segments hit); output content matches. - psutil-only features: Python upstream errors loudly when
psutilis missing and skips affected segments. Our daemon resolves the same data via OS subprocess probes (top,vm_stat,netstat,pmset,uptime) and renders successfully.
For everything else — markup, escaping (# → ##[], control chars
via translate_np), dividers (hard/soft/multi-char/empty/single-char),
colorscheme resolution (alias chains, fallback groups, gradients,
cterm/truecolor encoding with falsy-hex fallback), attrs (bold +
italics + underline bit-packed), outer_padding, spaces, left/right
side handling, empty sides, before/after wrapping, Unicode contents
— the byte stream is identical.
> CUSTOM SEGMENTS
Upstream Python powerline lets you drop a .py file in
~/.config/powerline/segments/ and reference it by dotted-path in
theme JSON; __import__ makes it callable. The Rust binary has no
dynamic import, so the dispatch layer in
src/extensions/exec_segment.rs (src/bin/shared/render_runtime.rs
adapter wiring) provides two equivalent surfaces that fall through
to a subprocess.
Pattern A — explicit exec adapter
Reference the built-in exec adapter directly in theme JSON. Use
when you want the script path, args, and format string spelled out
inline:
Pattern B — dotted-path filesystem dispatch
Reference your segment by dotted path and drop the script under
<config_path>/segments/. The daemon resolves
myseg.cpu_temp →
<config_path>/segments/myseg/cpu_temp.{sh,py,rb,pl,lua,js,executable}
(first hit wins; order documented in
src/extensions/exec_segment.rs::SCRIPT_EXTENSIONS):
The dotted-path resolution honors the daemon's full config-path
cascade (POWERLINE_CONFIG_PATHS, --config-path, ~/.config/),
so segments dropped in any config root that's on the cascade
become available.
Output protocol
Both patterns parse the script's stdout on the first non-whitespace byte:
| Stdout starts with | Treated as | Wrapped how |
|---|---|---|
[ (valid JSON array) |
Verbatim segment list | Used directly — full control over highlight_groups, gradient_level, divider_highlight_group, multi-chunk output |
| anything else | Plain text | [{"contents": <trimmed-stdout>, "highlight_groups": [...]}] — format template applies (%s → contents, %% → literal %) |
Plain-text scripts:
#!/bin/sh
JSON scripts (control gradient color, attach divider group, emit multiple chunks):
#!/usr/bin/env python3
Highlight-group caveat (Rust-port divergence)
The current Rust port's gen_segment_getter (at
src/ported/segment.rs:1011-1023) uses the segment's function_name
as the only highlight group for "type": "function" segments — it
ignores the theme's "highlight_groups" override. Upstream Python
respects the override.
Practical impact: name your colorscheme group to match the function
name ("exec" for pattern A, "cpu_temp" for pattern B), e.g.
"groups":
Pattern B sidesteps this nicely because the dotted path's trailing
component (the function name) is also a natural colorscheme group
name. Pattern A requires either a literal "exec" group, or pinning
the highlight via a JSON-array stdout that emits explicit
highlight_groups inline.
Bundled extensions
src/extensions/ ships several net-new segments above what upstream
powerline-status offers, dispatched as standard built-ins (no
filesystem lookup needed):
| Dotted path | What |
|---|---|
powerlinemem.mem_usage.mem_usage |
1:1 port of mKaloer/powerline_mem_segment's USED/TOTAL formatted bytes |
powerlinemem.mem_usage.mem_usage_percent |
Percentage variant |
powerlinemem.mem_usage.mem_swap |
Swap USED/TOTAL |
powerlinemem.mem_usage.mem_swap_percentage |
Swap percent |
powerliners.disk.disk_usage |
Filesystem USED/TOTAL for any mount |
powerliners.disk.disk_usage_percent |
Filesystem percent-used |
powerliners.disk.disk_io |
Live read/write throughput for any device |
powerliners.gpu.gpu_usage_percent |
Vendor-dispatched GPU compute percent (nvidia-smi → rocm-smi → intel_gpu_top → ioreg fallback) |
powerliners.gpu.gpu_vram |
GPU VRAM USED/TOTAL via same dispatch chain |
powerliners.thermal.thermal |
CPU/GPU temp + fan RPM (/sys/class/hwmon on Linux, powermetrics/istats on macOS) |
powerliners.vcs.git_status |
p10k-style single-chunk VCS segment — branch glyph + name + count badges for unstaged/untracked/staged/conflicts/ahead/behind/stashed from one git status --porcelain=v2 --branch fork; detached HEAD falls back to short SHA / tag; omitted outside a git work tree |
powerliners.docker.containers |
Docker / OCI container counts ({running}/{total}/{images}/{stopped} tokens; probes via docker ps, falls through silently when the daemon is unreachable) |
powerliners.k8s.kubecontext |
Current kubectl context + active namespace (honors $KUBECONFIG cascade and in-context kubectl config set-context --namespace overrides; hide_default arg suppresses the namespace when it equals the configured default) |
powerliners.proc.process_count |
POSIX process tally via ps -eo stat= ({total}/{running}/{sleeping}/{zombie}/{dwait}/{stopped} tokens; warn_zombie flips the highlight group when defunct processes are present) |
powerliners.github.ci_status |
Current branch's HEAD check-runs via gh api repos/:o/:r/commits/:sha/check-runs, cached on disk by SHA (ttl_secs default 30). Tokens: {icon}/{state}/{passed}/{failed}/{running}/{total}. Highlight groups split into github_ci_success / github_ci_failure / github_ci_pending. Falls through silently outside a GitHub work tree or when gh is missing |
powerliners.aws.context |
Active AWS profile + region. Pure-fs probe of $AWS_PROFILE/$AWS_REGION/$AWS_DEFAULT_REGION then ~/.aws/config (honors the [profile NAME] quirk). Tokens: {icon}/{profile}/{region}. hide_default_profile drops {profile} + adjacent separator when profile is default |
powerliners.gcp.context |
Active gcloud configuration's project + account. Pure-fs probe of ~/.config/gcloud/active_config + configurations/config_<NAME> ([core] section). Env overrides $CLOUDSDK_ACTIVE_CONFIG_NAME / $CLOUDSDK_CORE_PROJECT / $CLOUDSDK_CORE_ACCOUNT win. Tokens: {icon}/{project}/{account}/{config}. hide_account strips the account fragment |
powerliners.fusevm.jit_cache |
fusevm Cranelift JIT cache stats (entry count + bytes) for the zshrs/stryke runtime. Recursively walks path (theme arg) or $FUSEVM_JIT_CACHE / $XDG_CACHE_HOME/fusevm-jit / ~/.cache/fusevm-jit. Tokens: {icon}/{entries}/{size} (du -sh block allocation, human K/M/G)/{bytes} (raw disk bytes)/{logical_size}/{logical_bytes} (stat-style content sum). show_when_empty controls whether a missing/cold cache still renders |
powerliners.zshrs.rkyv_cache |
Single-file stat of the zshrs authoritative rkyv archive at $ZSHRS_RKYV_CACHE / $ZSHRS_HOME/scripts.rkyv / $XDG_DATA_HOME/zshrs/scripts.rkyv / ~/.zshrs/scripts.rkyv. Same {size} / {bytes} / {logical_*} token surface as fusevm.jit_cache. Disk-bytes default matches du -sh |
powerliners.stryke.rkyv_cache |
Single-file stat of ~/.stryke/scripts.rkyv (stryke's authoritative bytecode store, Cranelift-JIT'd via the shared fusevm runtime). Same token surface |
powerliners.awkrs.rkyv_cache |
Single-file stat of ~/.awkrs/scripts.rkyv. Same token surface |
powerliners.zshrs.version |
Latest installed zshrs version (parsed from <bin> --version). In-process TTL cache (default 300 s) so the daemon doesn't fork on every prompt tick. Tokens: {icon}/{version} |
powerliners.stryke.version |
Same for stryke (handles the This is stryke vX.Y.Z — ... prefix shape). |
powerliners.awkrs.version |
Same for awkrs. |
powerliners.exec.exec |
The explicit exec adapter (also resolves via bare "function": "exec") |
These each live in src/extensions/<module>.rs and are wired into
the daemon's ADAPTERS table — adding more follows the same pattern
(no new fn-name rules apply per docs/PORT.md's src/extensions/
carve-out).
> Cache-size segments — shared resolution chain
The four cache-size segments (fusevm.jit_cache, zshrs.rkyv_cache,
stryke.rkyv_cache, awkrs.rkyv_cache) share a uniform 4-level
resolution chain and an identical {size} / {bytes} / {logical_size} /
{logical_bytes} token surface so the same theme JSON works across all
four. Each is a pure filesystem probe — no subprocess, no daemon RPC.
For the rkyv segments, <NAME> ∈ {ZSHRS, STRYKE, AWKRS}:
$<NAME>_RKYV_CACHE— explicit override, used verbatim$<NAME>_HOME/scripts.rkyv$XDG_DATA_HOME/<name>/scripts.rkyv— used only when the file exists~/.<name>/scripts.rkyv— final fallback
fusevm.jit_cache uses the analogous 3-level chain
($FUSEVM_JIT_CACHE → $XDG_CACHE_HOME/fusevm-jit →
~/.cache/fusevm-jit) but recursively walks a cache directory rather
than stat-ing a single archive, and also tracks an {entries} token
for the recursive file count. Symlinks count as a single entry and are
never followed (avoids infinite loops across re-symlinked cache dirs).
The resolution chain is unit-tested via a pure-functional
default_path_with(get_env, path_exists) seam in
src/extensions/{zshrs,stryke,awkrs}_rkyv.rs and a matching
default_root_with(get_env) seam in src/extensions/fusevm_jit.rs —
no env-var mutation in tests, no thread-safety hazard, every
precedence level pinned.
Missing-archive behavior: returns None by default (no chunk renders).
Set "show_when_empty": true to render zeroed stats (0B/0) instead.
Highlight-group chain (3 levels): <segment>_rkyv_cache →
<segment> → information:regular. The trailing information:regular
is a neutral fallback so the chunk renders in any colorscheme.
> LICENSE
MIT. Theme JSON files in powerline/config/themes/ remain under their upstream licenses.