Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

kanri docs

kanri is a vim-modal system powertool for Linux — network, process, memory, disk, and hardware, watched and controlled from one TUI and one scriptable CLI.

Source lives at git.kanri.management/jaghack/kanri.

Install

kanri is a single static-ish binary, no runtime beyond what’s listed per method below. Pick your OS.

Status: apt, NixOS, and building from source all work today, against the real v0.1.1 tag. Neither AUR nor COPR is published yet (see those sections) — both packages are ready, submission just needs an aur.archlinux.org account and a Fedora Account System account respectively.

One line, any distro

curl -fsSL https://kanri.management/install.sh | sh

Reads /etc/os-release and runs the same commands documented in each section below for your distro — nothing it does isn’t also written out plainly somewhere on this page. Prints every command before running it. Atomic/immutable Fedora images (Bazzite, Silverblue, Kinoite, other uBlue-style builds) get routed to Nix automatically instead of rpm-ostree, since layering a dev toolchain onto those isn’t the right call — see the Fedora section below for why.

If you’d rather not pipe a script into a shell — a completely reasonable thing to not want to do — every section below is the same thing, by hand.

Arch Linux

yay -S kanri

(Coming soon — not yet published to the AUR. Until then, build from source below, or use the PKGBUILD directly:)

git clone https://git.kanri.management/jaghack/kanri.git
cd kanri/packaging/aur
makepkg -si

Ubuntu / Debian

curl -fsSL https://kanri.management/apt/key.gpg | sudo gpg --dearmor -o /usr/share/keyrings/kanri.gpg
echo "deb [signed-by=/usr/share/keyrings/kanri.gpg] https://kanri.management/apt stable main" | sudo tee /etc/apt/sources.list.d/kanri.list
sudo apt update && sudo apt install kanri

Verified working end-to-end on a real Debian 13 system, including the apt update/apt install steps exactly as written above — not just a built .deb, the actual hosted, signed repository. Only tested on Debian specifically; Ubuntu should work identically but hasn’t been checked.

Fedora (COPR)

sudo dnf copr enable jaghack/kanri
sudo dnf install kanri

(Coming soon — not yet published to COPR; needs a Fedora Account System account this repo/VPS can’t create on its own. Until then, the real, verified spec builds and installs directly:)

git clone https://git.kanri.management/jaghack/kanri.git
cd kanri
mkdir -p ~/rpmbuild/SOURCES ~/rpmbuild/SPECS
git archive --format=tar.gz --prefix=kanri-0.1.1/ v0.1.1 -o ~/rpmbuild/SOURCES/kanri-0.1.1.tar.gz
cp packaging/fedora/kanri.spec ~/rpmbuild/SPECS/
rpmbuild -bb ~/rpmbuild/SPECS/kanri.spec
sudo dnf install ~/rpmbuild/RPMS/x86_64/kanri-0.1.1-1*.rpm

Verified end-to-end against a real rpmbuild -bb (see packaging/fedora/kanri.spec’s own header comment) — no Fedora box was available to test on, but rpmbuild itself doesn’t care what distro runs it, and the resulting RPM’s file layout and binary were confirmed correct by hand. Targets COPR rather than Fedora’s own official repos on purpose: Commons Clause isn’t an OSI/Fedora-approved license, which COPR (a personal-project hosting service, not the distro’s own repo) is fine with and Fedora proper wouldn’t be.

On an atomic/immutable Fedora image (Bazzite, Silverblue, Kinoite, or another uBlue-style build): don’t use either of the above. rpm-ostree install layers packages onto your base image and needs a reboot to take effect — a heavy way to get one CLI tool — and these images frequently don’t ship cargo/clang at all, so rpmbuild won’t even get that far. Use Nix instead (see below) — it installs under its own /nix prefix and never touches the base image, so it works exactly the same here as on any other distro:

NIXPKGS_ALLOW_UNFREE=1 nix run --impure git+https://git.kanri.management/jaghack/kanri.git

NixOS (flake)

kanri’s license (Apache-2.0 + the Commons Clause — see License) isn’t “free” in nixpkgs’ own sense (it restricts selling the software), so Nix needs an explicit opt-in the same way it would for any other restricted-license package:

NIXPKGS_ALLOW_UNFREE=1 nix run --impure git+https://git.kanri.management/jaghack/kanri.git

That runs it once, nothing persisted. To install it for real via environment.systemPackages, don’t just add kanri.packages.${system}.default as an input and set nixpkgs.config.allowUnfreePredicate in your own config — that looks right but doesn’t actually work: kanri’s flake constructs its own pkgs internally (import nixpkgs { inherit system; }), a separate instance from the one your NixOS config’s nixpkgs.config applies to, so your predicate never reaches it and you’ll hit the same “Refusing to evaluate package… unfree license” error regardless. (Confirmed by actually hitting this building kanri into a real host config, not theoretical.) Build it against your own pkgs instead, using the flake only as a source:

{ config, pkgs, lib, ... }:
let
  kanriSrc = builtins.getFlake "git+https://git.kanri.management/jaghack/kanri.git?rev=<commit-of-the-tag-you-want>";
in
{
  environment.systemPackages = [
    (pkgs.rustPlatform.buildRustPackage {
      pname = "kanri";
      version = "0.1.1";
      src = kanriSrc;
      cargoLock.lockFile = "${kanriSrc}/Cargo.lock";
      nativeBuildInputs = with pkgs; [ pkg-config llvmPackages.clang ];
      LIBCLANG_PATH = "${pkgs.llvmPackages.libclang.lib}/lib";
      meta.license = { free = false; };
    })
  ];
  # Then either set this (simplest), or scope allowUnfreePredicate to
  # just "kanri" the way the flake-consumption snippet above tried to —
  # scoping works fine here, since this *is* the pkgs instance it's
  # checked against:
  nixpkgs.config.allowUnfree = true;
}

Use a real commit hash for rev (a mutable ?ref=v0.1.1 forces --impure on every future nixos-rebuild switch), e.g. via git ls-remote https://git.kanri.management/jaghack/kanri.git v0.1.1.

Build from source (any distro, works today)

Needs a Rust toolchain (edition 2024, so a reasonably recent one) and libclang (one dependency’s build script needs it for bindgen). Don’t have either already? rustup gets you cargo — it installs to ~/.cargo/~/.rustup, no root or system package manager involved — but libclang itself does need a system package (clang/llvm or your distro’s equivalent), which rustup won’t provide.

git clone https://git.kanri.management/jaghack/kanri.git
cd kanri
cargo build --release
./target/release/kanri

Or with Nix, no system Rust or libclang install needed at all — the one path that works unmodified even on an atomic/immutable distro (Bazzite, Silverblue, Kinoite, other uBlue-style images) where neither is preinstalled and layering them on isn’t the right call:

nix develop   # or: nix-shell
cargo run

After installing

kanri works with zero config. Some features need one-time, deliberate setup you do yourself (kanri never grants privilege on its own) — see wireguard.md for WireGuard status/control and capture.md for packet capture. Everything else (network devices, processes, memory, disk, hardware, Tailscale) works out of the box.

See config.md for configuration and keybindings.md for the full keybinding reference.

Keybindings

All of kanri’s TUI input is vim-modal: Normal mode for navigation and actions, Command mode (:) for typed commands, Search mode (/) for live filtering. Every chord below is fully remappable — see config.md.

Several chords are contextual: the same key does different things depending which tab (and sub-view, for Network) is active. This is deliberate — dd always means “delete/kill/forget the selected thing,” just applied to whatever “the selected thing” is on the current screen, rather than one bespoke keybind per subsystem.

Global

KeyAction
h j k lMove left / down / up / right
0Move to the start of the current line/list
ggJump to the top
GJump to the bottom
gtNext tab
gTPrevious tab
{n}gtJump to tab number n (1-indexed)
:Open the command line (same grammar as the CLI — see cli.md)
/Open the search/filter line (contextual — see below)
?Toggle the help overlay (auto-generated from your live keymap)
qQuit — or close the help overlay first, if it’s open
Ctrl+CQuit unconditionally, regardless of mode

Contextual

KeyMeaning
ddDelete the selected thing — always goes through a y/N confirm first. Kills the selected process (Process tab), forgets the selected saved connection (Network → Devices).
u“Power up” the selected thing. Activates a saved connection (Network → Devices), brings a WireGuard interface up (Network → WireGuard).
x“Power down” the selected thing. Deactivates a connection (Network → Devices), brings a WireGuard interface down (Network → WireGuard).
[ / ]Cycle the Network tab’s sub-view: Devices → Diagnostics → Sockets → WireGuard → Tailscale → Bandwidth → Scan → Capture.
SpaceContext-dependent “do the thing”: run a subnet scan (Scan), start/stop a live capture (Capture), toggle Tailscale up/down (Tailscale), collapse/expand the selected drive (Disk).
cOpen the WiFi-connect prompt — SSID, then a password (Network → Devices only; Enter with a blank password connects to an open network).
oToggle showing small system/boot/recovery partitions alongside each drive’s main ones (Disk tab).
/Live filter for whichever list is on screen: process command/user/PID, sockets, subnet scan results, per-process bandwidth, WireGuard peers, Tailscale peers, or live captured packets — each field it makes sense to match against, case-insensitive. Empty query clears the filter.

Search filter fields, per view

ViewMatches against
ProcessCommand, username, PID
Network → SocketsProtocol, state, local/peer address, process name, PID
Network → ScanIP, hostname
Network → BandwidthProcess name
Network → WireGuardInterface, endpoint, allowed IPs
Network → TailscaleHostname, any Tailscale IP, OS
Network → CaptureSource, destination, protocol, dissected info

Remapping

~/.config/kanri/config.toml:

[keymap.normal]
"j" = "move_down"
"dd" = "delete_selected"

Only the chords you mention change — everything else keeps its default. See config.md for the full list of action names you can bind.

Configuration

kanri works with zero config — every field below has a sensible compiled-in default. Config lives at ~/.config/kanri/config.toml (respects $XDG_CONFIG_HOME), or pass --config <path> to use a different file. A missing config file is not an error.

Full example

# Show a Japanese companion label under every tab/section label.
# Off by default — a terminal powertool needs every column of width.
jp_labels = false

# How often live-ticking views (Overview, Process, Memory, Disk,
# Hardware) refresh, in milliseconds.
refresh_interval_ms = 1000

[theme]
# Overrides the default theme source (~/.config/quickshell/theme.json).
# Point this at any JSON file with the same shape to use kanri outside
# that theming pipeline. Every field in that file is optional — kanri
# falls back to a compiled-in dark palette for anything missing.
override_path = "/path/to/theme.json"

[thresholds]
# Color-coding cutoffs (0-100) shared by every gauge/table that shows a
# percentage-like value — CPU%, memory/disk usage, sensor temperatures.
warn_at = 70.0
crit_at = 90.0

[keymap.normal]
# Chord -> action name. Only the chords listed here change; every other
# chord keeps its compiled-in default. See "Action names" below for the
# full list of what you can bind. See keybindings.md for what each one
# does and which are contextual per-tab.
"j" = "move_down"
"k" = "move_up"
"dd" = "delete_selected"

theme

kanri reads the same live theme file a matugen-based rice pipeline generates, and hot-reloads it on change — no restart needed. Every field in that JSON is optional; kanri falls back to a built-in dark palette (Catppuccin-Mocha-adjacent) for anything absent, including when no theme file exists at all. Recognized keys (all optional): base, surface, overlay, textPrimary, textSecondary, textMuted, accentNetwork, accentSystem, success, warning, error — each a "#rrggbb" hex string.

thresholds

Applies everywhere a value is shown as a percentage-like gauge or colored number: below warn_at is “success” colored, between warn_at and crit_at is “warning,” at or above crit_at is “error.”

keymap.normal

An overlay, not a replacement — set only the chords you want to change. A chord can be any sequence of keys kanri’s input layer recognizes (a single character like "j", or a short sequence like "gg"). An unknown action name is logged and skipped at startup, not a fatal error.

Action names

The right-hand side of a keymap.normal entry — every name below is a valid target:

move_down       move_up         move_left        move_right
move_line_start jump_top        jump_bottom
next_tab        prev_tab
enter_command   enter_search    show_help        quit
delete_selected up_selected     down_selected
next_sub_view   prev_sub_view   trigger
connect_wifi    toggle_extra

See keybindings.md for what each one does, including the ones that are contextual per-tab (e.g. trigger starts a subnet scan on the Scan sub-view, but toggles Tailscale on the Tailscale sub-view).

CLI reference

Run kanri with no arguments for the TUI. Any subcommand runs it in CLI mode instead: prints once, exits, scriptable. --json works on every subcommand and switches to machine-readable output.

Every subcommand here is the exact same code path the TUI uses — there is one implementation per action, not a separate CLI and TUI version of each. The TUI’s : command line even parses with this same grammar, so anything you can run from a shell you can also run without leaving the TUI.

kanri [OPTIONS] [COMMAND]

Options:
      --json             Emit machine-readable JSON instead of human-readable text
      --config <CONFIG>  Path to an alternate config.toml (defaults to the XDG config dir)

net — network devices and connections (NetworkManager)

CommandDoes
net listList devices and saved connections
net show <device>Full detail for one device, by interface name
net up <name>Activate a saved connection profile
net down <name>Deactivate a saved connection profile, if active
net disconnect <device>Disconnect whatever’s active on a device
net forget <name>Permanently delete a saved connection profile
net connect-wifi <ssid> [--password <pw>]Create and activate a new WiFi connection
net wg-up <interface>Bring a WireGuard interface up via wg-quick — needs a matching sudo rule for that exact interface name (see wireguard.md)
net wg-down <interface>Bring a WireGuard interface down via wg-quick

proc — processes and CPU usage

CommandDoes
proc list [--sort cpu|mem|pid|name] [--filter <text>]List running processes (default sort: cpu)
proc kill <pid> [--signal term|kill]Send a signal (default: term)
proc renice <pid> <value>Change nice value (-20 highest priority, 19 lowest)

ts — Tailscale status and control

CommandDoes
ts statusThis device + tailnet peers
ts upConnect to the tailnet — no elevated privilege needed on a typical setup
ts downDisconnect. Doesn’t touch your normal internet route — Tailscale is an overlay network, not your default route
ts exit-node [node]Set the active exit node by hostname or IP; omit node to clear it. Routes all of this device’s traffic through the chosen node — treat this one with real caution, unlike a plain up/down toggle

One-shot system info

CommandDoes
overviewCPU, memory, network throughput, disks — one snapshot
memMemory and swap usage
diskDisk usage by mount point
hwHost/CPU/kernel info and live sensor temperatures

--json shape

Every command’s --json output is the same data the human-readable text is built from — nothing is dropped or summarized differently between the two. Field names are plain snake_case, matching the Rust struct they come from. Pipe through jq as usual; kanri handles a closed pipe (kanri disk --json | jq '.[0]') the same way df/free do, not with a broken-pipe panic.

What’s TUI-only, for now

net show’s sub-views beyond the device/connection list — Diagnostics, Sockets, subnet Scan, per-process Bandwidth, WireGuard status (write control via wg-up/wg-down is scriptable, reading status isn’t yet), and live packet Capture — don’t have CLI equivalents yet. They’re real, working features, just reachable from the TUI only so far.

WireGuard

kanri talks to WireGuard through wg/wg-quick directly — not a reimplementation, the real tools. Reading status (wg show) and bringing an interface up/down (wg-quick up/down) both need root, since wg-quick creates network interfaces and can touch routing tables. kanri never has standing root itself; it shells out through sudo -n (non-interactive — fails fast and cleanly if it isn’t authorized, rather than hanging on a password prompt) for each call.

That means you configure the sudo access, scoped as narrowly as you want it. kanri ships no default grant.

Read-only status

# /etc/sudoers.d/kanri-wireguard
your_username ALL=(root) NOPASSWD: /path/to/wg show all dump

Resolve the real path first — command -v wg often returns a symlink (e.g. /run/current-system/sw/bin/wg on NixOS), and some sudo configurations match the literal command string rather than resolving symlinks themselves:

readlink -f "$(command -v wg)"

Use that path in the sudoers rule.

Up/down control

Same idea, one rule per interface name you actually want kanri to control — resist the temptation to wildcard this:

your_username ALL=(root) NOPASSWD: /path/to/wg-quick up wg0
your_username ALL=(root) NOPASSWD: /path/to/wg-quick down wg0

A wildcard (wg-quick up *) technically works, but it means anything that can run as your user can bring up or tear down any WireGuard config on the box, not just ones you meant to expose to kanri. Scoping per-interface costs one extra line per tunnel and closes that off entirely.

wg show’s own private-key exposure

wg show all dump includes each interface’s private key in cleartext in its output. If this machine has other, less-trusted local accounts, scope the sudo rule to trusted users only — don’t hand out blanket wg access to every account on a shared box.

NixOS example

security.sudo.extraRules = [
  {
    users = [ "your_username" ];
    commands = [
      { command = "${pkgs.wireguard-tools}/bin/wg show all dump"; options = [ "NOPASSWD" ]; }
      { command = "${pkgs.wireguard-tools}/bin/wg-quick up wg0"; options = [ "NOPASSWD" ]; }
      { command = "${pkgs.wireguard-tools}/bin/wg-quick down wg0"; options = [ "NOPASSWD" ]; }
    ];
  }
];

${pkgs.wireguard-tools}/bin/wg is already the real, fully-resolved store path — no readlink -f dance needed on NixOS specifically.

Nothing configured?

kanri’s WireGuard sub-view (and net wg-up/net wg-down) still work without any of this — wg show just reports no interfaces, and sudo -n wg-quick up ... fails with a clear “a password is required” style error instead of hanging.

Packet capture

The Capture sub-view (Network tab) shells out to tshark for real protocol dissection — not raw frames, actual decoded source/destination/ protocol/info columns, the same as opening Wireshark itself. Capturing packets needs CAP_NET_RAW/CAP_NET_ADMIN, which a normal user doesn’t have by default. kanri doesn’t grant this itself; you configure it once, the same way you would for Wireshark/tcpdump on any system.

The standard way

Most distros already have a mechanism for this via Wireshark’s own packaging:

# Debian/Ubuntu
sudo dpkg-reconfigure wireshark-common   # answer "yes" to the non-superuser prompt
sudo usermod -aG wireshark $USER

# Arch
sudo setcap cap_net_raw,cap_net_admin=eip $(readlink -f $(command -v dumpcap))
sudo usermod -aG wireshark $USER          # if you'd rather scope it to a group

Log out and back in (or reboot) afterward — group membership is fixed for your whole session at login, not per-process. A new terminal window inside an already-running desktop session does not pick up a newly-added group; only a fresh login does.

NixOS example

programs.wireshark = {
  enable = true;
  # dumpcap.enable defaults to true: wraps dumpcap with the capability,
  # restricted to the `wireshark` group (not world-executable).
};
users.users.your_username.extraGroups = [ "wireshark" ];

Why kanri doesn’t just grant this itself

Packet capture can read cleartext credentials off the wire for anyone sharing the same network segment — materially more sensitive than, say, per-process bandwidth counts. That’s worth a deliberate, visible choice on your part, not something a TUI silently arranges the first time you press a key.

Nothing configured?

The Capture sub-view still works without any of this — tshark fails fast (well under a second) with a clear permission error instead of hanging, and kanri shows the whole error, not just its first line.