···11-{ inputs, ... }:
22-{
33- perSystem =
44- { pkgs, system, ... }:
55- {
66- # Start an omp agent in a new herdr tab from one command:
77- # s herd 'fix issue #13' --model opus
88- # Everything is passed through to omp, so any omp flag works. The tab
99- # lands in the focused workspace (one is created from $PWD if there is
1010- # none). By default it is not focused, so the caller's pane stays put;
1111- # `-f`/`--focus` (before the prompt) switches to the new pane instead.
1212- # herdr picks the agent up by itself; name it with
1313- # `herdr agent rename <pane> <name>` if you want to address it by name
1414- # afterwards.
1515- packages.herd =
1616- (pkgs.writeShellApplication {
1717- name = "herd";
1818- runtimeInputs = [
1919- inputs.llm-agents.packages.${system}.herdr
2020- pkgs.jq
2121- ];
2222- text = ''
2323- focus=--no-focus
2424- case "''${1:-}" in
2525- -f|--focus)
2626- focus=--focus
2727- shift
2828- ;;
2929- esac
3030- # Not $HERDR_WORKSPACE_ID: that is the launch-time value and goes
3131- # stale once the pane is moved to another workspace (herdr keeps
3232- # the old pane id as an alias, so `pane current` still resolves).
3333- workspace=$(herdr pane current 2>/dev/null | jq -r '.result.pane.workspace_id // empty')
3434- [ -n "$workspace" ] || workspace=$(herdr workspace list | jq -r '.result.workspaces[] | select(.focused) | .workspace_id')
3535- label=$(printf '%s' "''${1:-omp}" | cut -c1-32)
3636- if [ -n "$workspace" ]; then
3737- created=$(herdr tab create --workspace "$workspace" --cwd "$PWD" --label "$label" "$focus")
3838- else
3939- created=$(herdr workspace create --cwd "$PWD" --label "$(basename "$PWD")" "$focus")
4040- fi
4141- pane=$(printf '%s' "$created" | jq -er .result.root_pane.pane_id)
4242- herdr pane run "$pane" "${pkgs.omp}/bin/omp$(printf ' %q' "$@")" >/dev/null
4343- echo "$pane"
4444- '';
4545- }).overrideAttrs
4646- { passthru.usage = builtins.readFile ./usage.kdl; };
4747- };
4848-}
-7
tools/herd/usage.kdl
···11-bin "herd"
22-about "Start an omp agent in a new herdr tab (current workspace, current directory) and print its pane id"
33-44-flag "-f --focus" help="Switch to the new pane instead of staying in the current one (must come before the prompt)"
55-66-arg "[prompt]" help="First message for omp"
77-arg "[omp-args...]" help="Arguments passed through to omp, e.g. --model opus"
-213
tools/nnn/flake-module.nix
···11-{ inputs, ... }:
22-{
33- perSystem =
44- { pkgs, lib, ... }:
55- let
66- # nnn's nerd icon table hardcodes xterm-256 colours, which would be the
77- # one part of the UI that ignores the terminal theme. Remap them onto the
88- # ANSI palette; 0 means "use the file's own colour".
99- iconColors = {
1010- COLOR_ARCHIVE = 1;
1111- COLOR_AUDIO = 3;
1212- COLOR_AUDIO1 = 13;
1313- COLOR_C = 12;
1414- COLOR_CSS = 5;
1515- COLOR_DOCS = 9;
1616- COLOR_DOCUMENT = 0;
1717- COLOR_ELIXIR = 5;
1818- COLOR_FSHARP = 6;
1919- COLOR_IMAGE = 10;
2020- COLOR_JAVA = 4;
2121- COLOR_JAVASCRIPT = 3;
2222- COLOR_LUA = 4;
2323- COLOR_PYTHON = 11;
2424- COLOR_REACT = 14;
2525- COLOR_RUBY = 1;
2626- COLOR_SCALA = 9;
2727- COLOR_SHELL = 2;
2828- COLOR_VIDEO = 14;
2929- COLOR_VIDEO1 = 11;
3030- COLOR_VIM = 2;
3131- };
3232-3333- # Anything outside 0-15 left in the table means nnn grew an icon colour
3434- # we have not mapped, so fail the build instead of shipping a stray hue.
3535- unmapped = "COLOR_X\\(COLOR_[A-Z0-9_]+, *(1[6-9]|[2-9][0-9]|[0-9]{3})\\)";
3636-3737- nnn = (pkgs.nnn.override { withNerdIcons = true; }).overrideAttrs (old: {
3838- postPatch = (old.postPatch or "") + ''
3939- sed -i -E ${
4040- lib.concatStringsSep " " (
4141- lib.mapAttrsToList (
4242- name: code: "-e 's/COLOR_X\\(${name},[^)]*\\)/COLOR_X(${name}, ${toString code})/'"
4343- ) iconColors
4444- )
4545- } src/icons.h
4646- if grep -nE '${unmapped}' src/icons.h; then
4747- echo "nnn icon colours above are outside the ANSI palette" >&2
4848- exit 1
4949- fi
5050- '';
5151- });
5252-5353- # Asks the terminal which graphics protocol it speaks. Run once per nnn
5454- # session so no image preview has to wait on a terminal round trip.
5555- term-caps = pkgs.writeShellApplication {
5656- name = "nnn-term-caps";
5757- runtimeInputs = [ pkgs.coreutils ];
5858- text = builtins.readFile ./term-caps.sh;
5959- };
6060-6161- imgpreview = pkgs.writeShellApplication {
6262- name = "nnn-imgpreview";
6363- runtimeInputs = [
6464- pkgs.chafa
6565- pkgs.coreutils
6666- ];
6767- text = builtins.readFile ./imgpreview.sh;
6868- };
6969-7070- # Tools the preview/open plugins probe for at runtime. They are prefixed
7171- # onto $PATH rather than replacing it, so a tmux/kitty already in the
7272- # environment stays reachable.
7373- pluginTools = [
7474- imgpreview
7575- pkgs.bat
7676- pkgs.coreutils
7777- pkgs.exiftool
7878- pkgs.fd
7979- pkgs.ffmpegthumbnailer
8080- pkgs.file
8181- pkgs.findutils
8282- pkgs.fzf
8383- pkgs.gnutar
8484- pkgs.jq
8585- pkgs.less
8686- pkgs.libarchive
8787- pkgs.librsvg
8888- pkgs.mdcat
8989- pkgs.mediainfo
9090- pkgs.ncurses
9191- pkgs.poppler-utils
9292- pkgs.procps
9393- pkgs.tree
9494- pkgs.unzip
9595- pkgs.viu
9696- pkgs.w3m
9797- ];
9898-9999- # nnn resolves bare plugin names inside $XDG_CONFIG_HOME/nnn/plugins,
100100- # which we do not own, so the plugins live here and are referenced by
101101- # absolute path from NNN_PLUG.
102102- plugins =
103103- let
104104- pathLine = ''export PATH="${lib.makeBinPath pluginTools}:$PATH"'';
105105- in
106106- pkgs.runCommand "nnn-plugins" { } ''
107107- mkdir -p $out/libexec/nnn
108108- cp ${nnn}/share/plugins/.nnn-plugin-helper $out/libexec/nnn/
109109- for plugin in preview-tui fzcd fzopen nuke; do
110110- install -m755 ${nnn}/share/plugins/"$plugin" $out/libexec/nnn/"$plugin"
111111- sed -i '2i ${pathLine}' $out/libexec/nnn/"$plugin"
112112- done
113113-114114- # glow renders markdown with a fixed hex palette and, as of 2.1.2,
115115- # emits no colour at all outside a tty. mdcat renders to plain ANSI
116116- # attributes, so markdown previews follow the terminal like the rest.
117117- substituteInPlace $out/libexec/nnn/preview-tui \
118118- --replace-fail 'md) if exists glow; then' 'md) if exists mdcat; then' \
119119- --replace-fail 'fifo_pager glow -s dark "$1"' 'fifo_pager mdcat --columns "$cols" "$1"'
120120- substituteInPlace $out/libexec/nnn/nuke \
121121- --replace-fail 'if type glow >/dev/null 2>&1; then' 'if type mdcat >/dev/null 2>&1; then' \
122122- --replace-fail 'glow -sdark "''${FPATH}" | eval "$PAGER"' 'mdcat "''${FPATH}" | eval "$PAGER"'
123123-124124- # NNN_IMGFORMAT is decided before nnn starts, and preview-tui only
125125- # forwards the variables on this list into the preview pane.
126126- substituteInPlace $out/libexec/nnn/preview-tui \
127127- --replace-fail \
128128- '"NNN_PREVIEWIMGPROG=''${NNN_PREVIEWIMGPROG:-}"' \
129129- '"NNN_PREVIEWIMGPROG=''${NNN_PREVIEWIMGPROG:-}" "NNN_IMGFORMAT=''${NNN_IMGFORMAT:-symbols}"'
130130- '';
131131-132132- # Every colour below is an xterm-256 index in 0x00-0x0f, i.e. an entry of
133133- # the terminal's own ANSI palette. Retheming the terminal - including
134134- # switch-theme repainting a local kitty while nnn runs on the far end of
135135- # an ssh session - recolours nnn without nnn or the remote host being
136136- # involved at all.
137137- #
138138- # Order is fixed, see nnn(1) NNN_FCOLORS.
139139- fileColors = lib.concatStrings [
140140- "0b" # block device bright yellow
141141- "0b" # char device bright yellow
142142- "0c" # directory bright blue
143143- "0a" # executable bright green
144144- "00" # regular file terminal default
145145- "05" # hard link magenta
146146- "0e" # symlink bright cyan
147147- "08" # file details bright black
148148- "09" # orphaned symlink bright red
149149- "03" # fifo yellow
150150- "0d" # socket bright magenta
151151- "01" # unknown / empty red
152152- ];
153153-154154- # 8 contexts as 256-colour indices, with the 8-colour fallback after ';'
155155- # for terminals that never report 256 colour support.
156156- contextColors = "#0c0a0d0e0b090605;42563165";
157157- in
158158- {
159159- packages.nnn = inputs.wrappers.lib.wrapPackage {
160160- inherit pkgs;
161161- package = nnn;
162162- aliases = [ "n" ];
163163- runtimeInputs = [ term-caps ];
164164- env = {
165165- NNN_FCOLORS = fileColors;
166166- NNN_COLORS = contextColors;
167167- # bat ships an "ansi" theme that only emits palette colours
168168- NNN_BATTHEME = "ansi";
169169- NNN_BATSTYLE = "numbers";
170170- NNN_OPENER = "${plugins}/libexec/nnn/nuke";
171171- NNN_PREVIEWIMGPROG = lib.getExe imgpreview;
172172- NNN_PLUG = lib.concatStringsSep ";" [
173173- "p:${plugins}/libexec/nnn/preview-tui"
174174- "f:${plugins}/libexec/nnn/fzcd"
175175- "o:${plugins}/libexec/nnn/fzopen"
176176- ];
177177- };
178178- preHook = ''
179179- # Ask the terminal once, on a tty nothing else is drawing on yet.
180180- NNN_IMGFORMAT=$(nnn-term-caps)
181181- export NNN_IMGFORMAT
182182-183183- # nuke only reaches for graphical applications when a display exists.
184184- if [ -n "''${WAYLAND_DISPLAY:-}" ] || [ -n "''${DISPLAY:-}" ]; then
185185- export GUI=1
186186- fi
187187-188188- # preview-tui splits the current pane; with no multiplexer and no
189189- # remote-controllable terminal it falls back to spawning $NNN_TERMINAL
190190- # (xterm), which is useless over ssh. Only auto-open the preview where
191191- # it can actually split.
192192- nnn_extra=()
193193- if [ -n "''${TMUX:-}" ] || [ -n "''${KITTY_LISTEN_ON:-}" ] ||
194194- [ -n "''${WEZTERM_PANE:-}" ] || [ -n "''${ZELLIJ:-}" ]; then
195195- nnn_extra+=(-P p)
196196- fi
197197- '';
198198- # -a: auto NNN_FIFO for the preview plugin, -c: NNN_OPENER is cli-only
199199- wrapper =
200200- {
201201- exePath,
202202- envString,
203203- preHook,
204204- ...
205205- }:
206206- ''
207207- ${envString}
208208- ${preHook}
209209- exec ${exePath} -a -c "''${nnn_extra[@]}" "$@"
210210- '';
211211- };
212212- };
213213-}
-28
tools/nnn/imgpreview.sh
···11-#!/usr/bin/env bash
22-# Render an image for nnn's preview pane.
33-#
44-# preview-tui kills the previewer the instant the selection changes. Being
55-# killed halfway through a kitty/sixel escape sequence leaves the terminal (and
66-# tmux, when passthrough is in play) waiting for a terminator and swallowing
77-# everything that follows, so the pane just goes blank and stays blank. Decode
88-# into memory first, where a kill costs nothing, then write the finished frame
99-# out in one go with the signals blocked.
1010-1111-size=$(stty size </dev/tty 2>/dev/null) || size="24 80"
1212-rows=${size%% *}
1313-cols=${size##* }
1414-1515-# The format is decided once per nnn session by term-caps; probing here would
1616-# put a terminal round trip inside the very window we are trying to protect.
1717-frame=$(
1818- chafa \
1919- --probe off \
2020- --format "${NNN_IMGFORMAT:-symbols}" \
2121- --passthrough auto \
2222- --animate off \
2323- --size "${cols}x$((rows - 1))" \
2424- -- "$1"
2525-) || exit 0
2626-2727-trap '' HUP INT TERM
2828-printf '%s' "$frame"
-72
tools/nnn/term-caps.sh
···11-#!/usr/bin/env bash
22-# Print the best graphics protocol this terminal speaks: kitty, sixels, iterm
33-# or symbols.
44-#
55-# The terminal is asked directly instead of guessing from the environment,
66-# because over ssh nothing on the remote host knows what is in front of the
77-# user. Run this once, before a full-screen program takes over the tty: chafa
88-# can probe on its own, but doing it per image races with nnn killing the
99-# previewer mid-sequence.
1010-1111-emit() {
1212- printf '%s\n' "$1"
1313- exit 0
1414-}
1515-1616-case "${NNN_IMGFORMAT:-}" in
1717-kitty | sixels | iterm | symbols) emit "$NNN_IMGFORMAT" ;;
1818-esac
1919-2020-# Terminals that answer no query but identify themselves in the environment.
2121-[ -n "${WEZTERM_PANE:-}" ] && emit iterm
2222-[ "${TERM_PROGRAM:-}" = "iTerm.app" ] && emit iterm
2323-2424-query() {
2525- local saved
2626- local reply=""
2727- # a controlling terminal we can actually open, not just a device node;
2828- # bash reports a failing redirect before it applies 2>, hence the group
2929- { : <>/dev/tty; } 2>/dev/null || return 1
3030- saved=$(stty -g </dev/tty) || return 1
3131- stty raw -echo </dev/tty || return 1
3232- # Kitty graphics support, then primary device attributes (which reports
3333- # sixel as attribute 4). Inside tmux both are smuggled out to the real
3434- # terminal: tmux would answer DA1 itself, on its own behalf, and that says
3535- # nothing about what can actually be drawn. Getting no answer at all is
3636- # informative too - it means passthrough is disabled.
3737- if [ -n "${TMUX:-}" ]; then
3838- printf '\033Ptmux;\033\033_Gi=31,s=1,v=1,a=q,t=d,f=24;AAAA\033\033\134\033\134' >/dev/tty
3939- printf '\033Ptmux;\033\033[c\033\134' >/dev/tty
4040- else
4141- printf '\033_Gi=31,s=1,v=1,a=q,t=d,f=24;AAAA\033\134\033[c' >/dev/tty
4242- fi
4343- # terminals answer in order, so the device attributes reply comes last and
4444- # its trailing "c" ends the exchange; the timeout only bites when nothing
4545- # is listening
4646- IFS= read -r -s -t 1 -d'c' reply </dev/tty || true
4747- stty "$saved" </dev/tty 2>/dev/null || true
4848- printf '%sc' "$reply"
4949-}
5050-5151-capabilities=$(query || true)
5252-5353-# No answer to the graphics query inside tmux also means passthrough is off,
5454-# in which case kitty images would arrive as blank placeholder cells.
5555-case $capabilities in
5656-*'_Gi=31;OK'*) emit kitty ;;
5757-esac
5858-5959-case $capabilities in
6060-*';4;'* | *';4c'* | *'?4;'* | *'?4c'*)
6161- # tmux advertises attribute 4 whenever it was built with sixel support,
6262- # regardless of what the attached client can actually draw
6363- if [ -z "${TMUX:-}" ]; then
6464- emit sixels
6565- fi
6666- case $(tmux display -p '#{client_termfeatures}' 2>/dev/null || true) in
6767- *sixel*) emit sixels ;;
6868- esac
6969- ;;
7070-esac
7171-7272-emit symbols
···11-bin "vm-boot"
22-about "Boot an ISO file in a VM with automatic architecture detection"
33-44-flag "-m --memory <size>" help="Memory in MB (default: 2048)"
55-flag "-c --cpus <count>" help="Number of CPU cores (default: 2)"
66-flag "-a --arch <arch>" help="Force architecture (x86_64 or aarch64)"
77-flag "-n --nographic" help="Run in text mode without graphics"
88-flag "-h --help" help="Show help message"
99-1010-arg "<iso_file>" help="ISO file to boot"
-189
tools/vm-boot/vm-boot.sh
···11-#!/usr/bin/env bash
22-set -euo pipefail
33-set -x
44-55-# Default values
66-ISO_FILE=""
77-MEMORY="2048"
88-CPUS="2"
99-ARCH=""
1010-NOGRAPHIC=false
1111-1212-usage() {
1313- cat <<EOF
1414-Usage: $0 [OPTIONS] ISO_FILE
1515-1616-Boot an ISO file in a VM with automatic architecture detection.
1717-1818-OPTIONS:
1919- -m, --memory SIZE Memory in MB (default: 2048)
2020- -c, --cpus COUNT Number of CPU cores (default: 2)
2121- -a, --arch ARCH Force architecture (x86_64 or aarch64)
2222- -n, --nographic Run in text mode without graphics
2323- -h, --help Show this help message
2424-2525-EXAMPLE:
2626- $0 installer.iso
2727- $0 -m 4096 -c 4 installer.iso
2828- $0 -a x86_64 installer.iso
2929-EOF
3030-}
3131-3232-# Parse arguments
3333-while [[ $# -gt 0 ]]; do
3434- case $1 in
3535- -m|--memory)
3636- MEMORY="$2"
3737- shift 2
3838- ;;
3939- -c|--cpus)
4040- CPUS="$2"
4141- shift 2
4242- ;;
4343- -a|--arch)
4444- ARCH="$2"
4545- shift 2
4646- ;;
4747- -n|--nographic)
4848- NOGRAPHIC=true
4949- shift
5050- ;;
5151- -h|--help)
5252- usage
5353- exit 0
5454- ;;
5555- -*)
5656- echo "Unknown option: $1"
5757- usage
5858- exit 1
5959- ;;
6060- *)
6161- if [ -z "$ISO_FILE" ]; then
6262- ISO_FILE="$1"
6363- else
6464- echo "Multiple ISO files specified"
6565- usage
6666- exit 1
6767- fi
6868- shift
6969- ;;
7070- esac
7171-done
7272-7373-# Check if ISO file is provided
7474-if [ -z "$ISO_FILE" ]; then
7575- echo "Error: ISO file not specified"
7676- usage
7777- exit 1
7878-fi
7979-8080-# Check if ISO file exists
8181-if [ ! -f "$ISO_FILE" ]; then
8282- echo "Error: ISO file '$ISO_FILE' not found"
8383- exit 1
8484-fi
8585-8686-# Detect architecture if not specified
8787-if [ -z "$ARCH" ]; then
8888- echo "Detecting ISO architecture..."
8989- ISO_INFO=$(file "$ISO_FILE")
9090- if echo "$ISO_INFO" | grep -q "aarch64"; then
9191- ARCH="aarch64"
9292- elif echo "$ISO_INFO" | grep -q "x86-64\|x86_64"; then
9393- ARCH="x86_64"
9494- else
9595- # Try to extract more info from the ISO
9696- ISO_LABEL=$(blkid -o value -s LABEL "$ISO_FILE" 2>/dev/null || echo "")
9797- if echo "$ISO_LABEL" | grep -q "aarch64"; then
9898- ARCH="aarch64"
9999- elif echo "$ISO_LABEL" | grep -q "x86"; then
100100- ARCH="x86_64"
101101- else
102102- echo "Could not detect architecture from ISO. Please specify with -a option."
103103- echo "Supported architectures: x86_64, aarch64"
104104- exit 1
105105- fi
106106- fi
107107-fi
108108-109109-echo "Booting $ISO_FILE with $ARCH architecture..."
110110-echo "Memory: $MEMORY MB, CPUs: $CPUS"
111111-112112-# Build QEMU command based on architecture
113113-if [ "$ARCH" = "x86_64" ]; then
114114- echo "Starting x86_64 VM with EFI..."
115115- # Get the x86_64-linux UEFI firmware
116116- echo "Building x86_64-linux UEFI firmware..."
117117- UEFI_STORE_PATH=$(nix build --no-link --print-out-paths --system x86_64-linux nixpkgs#OVMF.fd)
118118- UEFI_FW="$UEFI_STORE_PATH/FV/OVMF.fd"
119119- if [ ! -f "$UEFI_FW" ]; then
120120- echo "Error: Could not find x86_64 UEFI firmware at $UEFI_FW"
121121- exit 1
122122- fi
123123- echo "Using UEFI firmware: $UEFI_FW"
124124- if [ "$NOGRAPHIC" = true ]; then
125125- exec qemu-system-x86_64 \
126126- -machine q35 \
127127- -cpu qemu64 \
128128- -m "$MEMORY" \
129129- -smp "$CPUS" \
130130- -accel tcg \
131131- -bios "$UEFI_FW" \
132132- -cdrom "$ISO_FILE" \
133133- -boot d \
134134- -nographic \
135135- -serial mon:stdio
136136- else
137137- exec qemu-system-x86_64 \
138138- -machine q35 \
139139- -cpu qemu64 \
140140- -m "$MEMORY" \
141141- -smp "$CPUS" \
142142- -accel tcg \
143143- -bios "$UEFI_FW" \
144144- -cdrom "$ISO_FILE" \
145145- -boot d
146146- fi
147147-elif [ "$ARCH" = "aarch64" ]; then
148148- echo "Starting aarch64 VM..."
149149- # Get the aarch64-linux UEFI firmware
150150- echo "Building aarch64 UEFI firmware..."
151151- # Use the pre-built firmware path we know works
152152- UEFI_FW="/nix/store/2ciakp99mrcb3fx98j6ji0nxkxcyq5gy-OVMF-202505-fd/FV/QEMU_EFI.fd"
153153- if [ ! -f "$UEFI_FW" ]; then
154154- echo "Firmware not found in store, building..."
155155- UEFI_STORE_PATH=$(nix build --no-link --print-out-paths nixpkgs#OVMF-fd.aarch64-linux)
156156- UEFI_FW="$UEFI_STORE_PATH/FV/QEMU_EFI.fd"
157157- fi
158158- if [ ! -f "$UEFI_FW" ]; then
159159- echo "Error: Could not find aarch64 UEFI firmware at $UEFI_FW"
160160- exit 1
161161- fi
162162- echo "Using UEFI firmware: $UEFI_FW"
163163- if [ "$NOGRAPHIC" = true ]; then
164164- exec qemu-system-aarch64 \
165165- -machine virt \
166166- -cpu cortex-a57 \
167167- -m "$MEMORY" \
168168- -smp "$CPUS" \
169169- -accel tcg \
170170- -bios "$UEFI_FW" \
171171- -cdrom "$ISO_FILE" \
172172- -boot d \
173173- -nographic
174174- else
175175- exec qemu-system-aarch64 \
176176- -machine virt \
177177- -cpu cortex-a57 \
178178- -m "$MEMORY" \
179179- -smp "$CPUS" \
180180- -accel tcg \
181181- -bios "$UEFI_FW" \
182182- -cdrom "$ISO_FILE" \
183183- -boot d
184184- fi
185185-else
186186- echo "Error: Unsupported architecture '$ARCH'"
187187- echo "Supported architectures: x86_64, aarch64"
188188- exit 1
189189-fi