Compare commits

..

1 Commits

Author SHA1 Message Date
521d9b73a3 feat(zsh): add conditional logic for mac 2026-01-21 23:37:51 +01:00
72 changed files with 648 additions and 4884 deletions

View File

@@ -1,2 +0,0 @@
# typos
f148ab1559cbb4e0e46e05f8d151f74f64adb359

117
.gitignore vendored
View File

@@ -4,17 +4,24 @@
# allowed misc files # allowed misc files
!.gitmodules !.gitmodules
!.gitignore !.gitignore
!.git-blame-ignore-revs
!README.md !README.md
# allow some stuff to be pushed
!alacritty
!.pyenv
# zsh # zsh
!zsh !zsh
zsh/* zsh/*
!zsh/.zshrc !zsh/.zshrc
!zsh/.zshenv !zsh/.zshenv
!zsh/.zprofile !zsh/.zprofile
!zsh/.zsh_plugins.txt
!zsh/scripts.zsh !zsh/scripts.zsh
!zsh/aliases.zsh !zsh/aliases.zsh
!zsh/completions/.keep
!zsh/ssh-menu
!zsh/get-dump
# starship # starship
!starship !starship
@@ -25,118 +32,20 @@ starship/*
!tmux !tmux
tmux/* tmux/*
!tmux/tmux.conf !tmux/tmux.conf
!tmux/scripts !tmux/tmux-new-project
!tmux/tmux-sessionizer
!tmux/tmux-ggclone
!tmux/tmux-ssher
# git # git
!git !git
git/* git/*
!git/config !git/config
!git/ignore !git/ignore
!git/config.d !git/aflabs
git/config.d/*
!git/config.d/aflabs.conf
!git/config.d/flare.conf
!git/hooks
git/hooks/*
!git/hooks/merge-msg
# ghostty # ghostty
!ghostty !ghostty
ghostty/* ghostty/*
!ghostty/config !ghostty/config
!ghostty/themes !ghostty/themes
# sway
!sway
sway/*
!sway/config
!sway/config.d
!sway/host.d
!swaylock
swaylock/*
!swaylock/config
!swayidle
swayidle/*
!swayidle/config
# waybar
!waybar
waybar/*
!waybar/config.jsonc
!waybar/style.css
!waybar/scripts
waybar/scripts/*
!waybar/scripts/ssh-session.sh
!waybar/scripts/vpn-status.sh
!waybar/scripts/notification-status.sh
!waybar/scripts/mic-status.sh
!waybar/scripts/network-status.sh
# eww
!eww
eww/*
!eww/eww.yuck
!eww/eww.scss
!eww/scripts
eww/scripts/*
!eww/scripts/popup.sh
!eww/scripts/system.sh
!eww/scripts/battery.sh
!eww/scripts/volume.sh
!eww/scripts/bluetooth.sh
!eww/scripts/network.sh
!eww/scripts/vpn.sh
!eww/scripts/keyboard.sh
!eww/scripts/media.sh
# sway scripts
!sway/scripts
sway/scripts/*
!sway/scripts/power-menu.sh
!sway/scripts/screenshot.sh
!sway/scripts/ws-to-current.sh
!sway/scripts/power-aware-effects.sh
# bin
!bin
bin/*
!bin/ssh-menu
!bin/git-sign-branch
# swaync
!swaync
swaync/*
!swaync/config.json
!swaync/style.css
# fuzzel
!fuzzel
fuzzel/*
!fuzzel/fuzzel.ini
# mako
!mako
mako/*
!mako/config
# claude
!claude
claude/*
!claude/CLAUDE.md
!claude/settings.json
!claude/statusline-command.sh
!claude/rules
!claude/agents
!claude/skills
claude/skills/*
!claude/skills/lead
claude/skills/lead/*
!claude/skills/lead/SKILL.md
!claude/skills/lead/notes.md
!claude/skills/lead/notes.css
!claude/skills/lead/notes.py
!claude/skills/builder
!claude/skills/ground-rules
!claude/skills/researcher

5
.pyenv/default-packages Normal file
View File

@@ -0,0 +1,5 @@
# Neovim should be able to run anywhere
pynvim
# Upgrade pip while we're at it.
pip

82
alacritty/alacritty.toml Normal file
View File

@@ -0,0 +1,82 @@
[colors]
draw_bold_text_with_bright_colors = true
[colors.bright]
black = "0x45403d"
blue = "0x7daea3"
cyan = "0x89b482"
green = "0xa9b665"
magenta = "0xd3869b"
red = "0xea6962"
white = "0xd4be98"
yellow = "0xd8a657"
[colors.normal]
black = "0x45403d"
blue = "0x7daea3"
cyan = "0x89b482"
green = "0xa9b665"
magenta = "0xd3869b"
red = "0xea6962"
white = "0xd4be98"
yellow = "0xd8a657"
[colors.primary]
background = "0x32302f"
foreground = "0xd4be98"
[cursor]
unfocused_hollow = true
[cursor.style]
blinking = "On"
shape = "Block"
[env]
TERM = "xterm-256color"
[font]
size = 15
[font.bold]
family = "JetBrainsMono Nerd Font"
style = "Bold"
[font.bold_italic]
family = "JetBrainsMono Nerd Font"
style = "Bold Italic"
[font.italic]
family = "JetBrainsMono Nerd Font"
style = "Italic"
[font.normal]
family = "JetBrainsMono Nerd Font"
style = "Regular"
[[keyboard.bindings]]
chars = "\f"
key = "R"
mode = "~Vi|~Search"
mods = "Command"
[[keyboard.bindings]]
action = "ScrollPageUp"
key = "PageUp"
mode = "~Alt"
[[keyboard.bindings]]
action = "ScrollPageDown"
key = "PageDown"
mode = "~Alt"
[scrolling]
history = 10000
[window]
dynamic_title = true
option_as_alt = "Both"
[window.dimensions]
columns = 150
lines = 36

View File

@@ -1,74 +0,0 @@
#!/usr/bin/env bash
# git sign-branch [-n] [--reset-author] [<branch> [<upstream>]]
# re-creates every commit in <upstream>..<branch> with a signature, keeping trees, parents (mapped to
# the new ids), author, committer, dates and message byte for byte; merges are copied, never re-run
# --reset-author: author and committer become this repo's user.name and user.email (the signer); dates stay
# a branch with a merge of a signed tag (mergetag header) is refused, commit-tree can't carry the tag over
set -euo pipefail
dry=0 reset=0
while [[ ${1:-} == -* ]]; do
case $1 in
-n | --dry-run) dry=1 ;;
--reset-author) reset=1 ;;
*) echo "unknown option: $1" >&2; exit 1 ;;
esac
shift
done
if (( reset )); then
me_name=$(git config user.name) me_email=$(git config user.email)
fi
branch=${1:-$(git symbolic-ref --short HEAD)}
upstream=${2:-main}
ref=refs/heads/$branch
old_tip=$(git rev-parse --verify "$ref^{commit}")
git rev-parse --verify -q "$upstream^{commit}" >/dev/null || { echo "no such upstream: $upstream" >&2; exit 1; }
# checked before anything is signed, so a refusal leaves no partial work
tagged=()
while read -r c; do
if [[ -n $(git cat-file commit "$c" | sed -n '1,/^$/{/^mergetag /p}') ]]; then
tagged+=("$(git log -1 --format='%h %s' "$c")")
fi
done < <(git rev-list "$upstream..$old_tip")
if (( ${#tagged[@]} )); then
echo "refusing to sign $branch: commit-tree can't carry over the signed tag these merges embed (mergetag):" >&2
printf ' %s\n' "${tagged[@]}" >&2
exit 1
fi
declare -A new # old id -> new id, only for commits in the range
msg=$(mktemp); trap 'rm -f "$msg"' EXIT
count=0
while read -r c parents; do
args=() changed=0
for p in $parents; do
np=${new[$p]:-$p}; [[ $np != "$p" ]] && changed=1
args+=(-p "$np")
done
# a commit already signed, with the same parents (and, with --reset-author, already ours) stays as it is
mine=1
if (( reset )) && [[ $(git log -1 --format='%an%x1f%ae%x1f%cn%x1f%ce' "$c") != "$me_name"$'\x1f'"$me_email"$'\x1f'"$me_name"$'\x1f'"$me_email" ]]; then mine=0; fi
if (( !changed && mine )) && git verify-commit "$c" >/dev/null 2>&1; then new[$c]=$c; continue; fi
count=$((count + 1))
if (( dry )); then
echo "would sign $(git log -1 --format='%h %s' "$c")"; new[$c]=dry-$c; continue
fi
git cat-file commit "$c" | sed '1,/^$/d' >"$msg" # message bytes after the header block
# a non-utf-8 commit keeps its encoding header; names are read in it so their bytes stay too
enc=$(git cat-file commit "$c" | sed -n '1,/^$/{s/^encoding //p}')
enc=${enc:-UTF-8}
IFS=$'\x1f' read -r an ae ad cn ce cd < <(git log -1 --encoding="$enc" --date=raw --format='%an%x1f%ae%x1f%ad%x1f%cn%x1f%ce%x1f%cd' "$c")
if (( reset )); then an=$me_name ae=$me_email cn=$me_name ce=$me_email; fi
new[$c]=$(GIT_AUTHOR_NAME=$an GIT_AUTHOR_EMAIL=$ae GIT_AUTHOR_DATE=$ad \
GIT_COMMITTER_NAME=$cn GIT_COMMITTER_EMAIL=$ce GIT_COMMITTER_DATE=$cd \
git -c i18n.commitEncoding="$enc" commit-tree -S "${args[@]}" -F "$msg" "$c^{tree}")
done < <(git rev-list --reverse --topo-order --parents "$upstream..$old_tip")
if (( dry )); then echo "$count commit(s) to sign on $branch"; exit 0; fi
new_tip=${new[$old_tip]:-$old_tip}
[[ $(git rev-parse "$new_tip^{tree}") == $(git rev-parse "$old_tip^{tree}") ]] || { echo "tip tree differs, ref left alone" >&2; exit 1; }
# compare-and-swap: fails if the branch moved while we were signing
git update-ref -m "sign-branch: signed $count commit(s)" "$ref" "$new_tip" "$old_tip"
echo "$branch: $old_tip -> $new_tip ($count signed); undo: git update-ref $ref $old_tip"

View File

@@ -1,389 +0,0 @@
#!/usr/bin/env bash
# pick a host from ~/.ssh/config and every file it includes
# ssh-menu connect to the selection
# ssh-menu -s print the selection: "<host>" or "<host>\tluks" (ctrl-u)
# ssh-menu -h show usage
# exported: the preview and the list reload run in child shells spawned by fzf
export SSH_MENU_CONFIG="${SSH_MENU_CONFIG:-$HOME/.ssh/config}"
# reachability results are cached per host and profile so browsing the list
# does not re-probe (and re-log on) the same server; ctrl-r forgets one host
export SSH_MENU_CACHE="${XDG_RUNTIME_DIR:-/tmp}/ssh-menu-${USER:-$(id -un)}"
export SSH_MENU_CACHE_TTL="${SSH_MENU_CACHE_TTL:-3600}"
# seconds to wait for an sshd banner: a plain tcp connection answers within one;
# a ProxyCommand such as an iap tunnel needs a few to come up first
export SSH_MENU_TIMEOUT="${SSH_MENU_TIMEOUT:-1}"
export SSH_MENU_PROXY_TIMEOUT="${SSH_MENU_PROXY_TIMEOUT:-3}"
[[ -d $SSH_MENU_CACHE ]] || mkdir -m 700 "$SSH_MENU_CACHE"
# fail early with one message instead of a cascade of command-not-found errors
# from inside fzf's preview shells
function _ssh_menu_require {
local cmd missing=()
for cmd in "$@"; do
command -v "$cmd" >/dev/null 2>&1 || missing+=("$cmd")
done
if (( ${#missing[@]} > 0 )); then
printf 'ssh-menu: missing required commands: %s\n' "${missing[*]}" >&2
exit 1
fi
}
# config plus every file named on Include lines; globs expanded, relative
# paths resolved against ~/.ssh, unreadable files skipped
function _ssh_menu_files {
local line paths path file
printf '%s\n' "$SSH_MENU_CONFIG"
grep -iE '^[[:space:]]*Include[[:space:]]' "$SSH_MENU_CONFIG" 2>/dev/null \
| while read -r line; do
paths=${line#*[Ii]nclude}
for path in $paths; do
path=${path/#\~/$HOME}
[[ $path != /* ]] && path="$HOME/.ssh/$path"
for file in $path; do
[[ -r $file ]] && printf '%s\n' "$file"
done
done
done
}
# one line per alias: file, alias, state, group, unlock marker
# group is the fragment name; a "# unlock" comment inside a Host block, or on
# the line directly above it, marks the host as having an initrd sshd
function _ssh_menu_hosts {
local file group
while read -r file; do
group=$(basename "$file")
group=${group#ssh-fragment-}
awk -v file="$file" -v group="$group" '
function flush() {
for (i = 0; i < n; i++)
printf "%s\t%s\t%s\t%s\n", file, hosts[i], group, (marks > 0 ? "unlock" : "")
n = 0; marks = 0
}
# unset would compare equal to 0, i.e. "the line before line 1"
BEGIN { marker_line = -1 }
/^[[:space:]]*[Hh]ost[[:space:]]/ {
# a marker on the line directly above belongs to this block; the
# previous block keeps any markers of its own
carry = marker_line == NR - 1
if (carry) marks--
flush()
marks = carry
for (i = 2; i <= NF; i++) if ($i !~ /[*?!]/) hosts[n++] = $i
next
}
/^[[:space:]]*[Mm]atch[[:space:]]/ { flush(); next }
/^[[:space:]]*#[[:space:]]*unlock/ { marks++; marker_line = NR }
END { flush() }
' "$file"
done < <(_ssh_menu_files | awk '!seen[$0]++') \
| sort -t $'\t' -k3,3 -k2,2 \
| _ssh_menu_with_state
}
# $1 host, $2 now (epoch): one word summarising the cached default-profile
# probe, "?" when nothing fresh is cached. reads the cache only, never probes
function _ssh_menu_state {
local host=$1 now=$2 file="$SSH_MENU_CACHE/$1__default" line
[[ -f $file ]] || { echo "?"; return; }
(( now - $(stat -c %Y "$file") < SSH_MENU_CACHE_TTL )) || { echo "?"; return; }
line=$(<"$file")
case $line in
"jump "*" reachable"*) echo hop ;;
"jump "*) echo down ;;
reachable*) echo up ;;
unreachable*) echo down ;;
*) echo - ;;
esac
}
# insert the cached state as the third field: file, alias, state, group, marker.
# fzf keeps each field's trailing delimiter and the last field has none, so the
# state must not be last or --with-nth would glue it to its neighbour
function _ssh_menu_with_state {
local now file host group marker
now=$(date +%s)
while IFS=$'\t' read -r file host group marker; do
printf '%s\t%s\t%s\t%s\t%s\n' "$file" "$host" "$(_ssh_menu_state "$host" "$now")" "$group" "$marker"
done
}
# the raw Host block for an alias, comments included
function _ssh_menu_block {
local file=$1 host=$2
awk -v h="$host" '
/^[[:space:]]*[Hh]ost[[:space:]]/ {
found = 0
for (i = 2; i <= NF; i++) if ($i == h) found = 1
}
/^[[:space:]]*[Mm]atch[[:space:]]/ { found = 0 }
found
' "$file"
}
# an ssh server announces itself before the client sends anything, so a bare
# tcp connection (or a running ProxyCommand) yields the banner without any
# authentication and therefore without touching the agent or the yubikey
# $1 host, $2 port: prints the sshd banner if the port answers with one
function _ssh_menu_tcp_banner {
# shellcheck disable=SC2016 # $1/$2/$3 are the inner bash's positional args
timeout "$SSH_MENU_TIMEOUT" bash -c '
exec 3<>"/dev/tcp/$1/$2" || exit 1
IFS= read -t "$3" -r banner <&3 || exit 1
printf "%s\n" "$banner" | tr -d "\r"
' _ "$1" "$2" "$SSH_MENU_TIMEOUT" 2>/dev/null
}
# $1 expanded ProxyCommand: the remote banner arrives on its stdout. stdin is an
# open, silent pipe (not /dev/null): a tunnel command may exit on stdin EOF
# before the banner arrives. the sleep lives in a process substitution, not the
# pipeline, so it is not waited for: the result returns as soon as the first
# line arrives, or after the proxy timeout
function _ssh_menu_proxy_banner {
timeout "$SSH_MENU_PROXY_TIMEOUT" bash -c "$1" < <(sleep "$((SSH_MENU_PROXY_TIMEOUT + 1))") 2>/dev/null | head -n1 | tr -d '\r'
}
# ssh reading the same config ssh itself would; -F only when overridden, since
# -F also drops /etc/ssh/ssh_config
function _ssh_menu_sshflags {
[[ $SSH_MENU_CONFIG != "$HOME/.ssh/config" ]] && printf '%s\n' -F "$SSH_MENU_CONFIG"
return 0
}
function _ssh_menu_ssh {
local flags=()
mapfile -t flags < <(_ssh_menu_sshflags)
# shellcheck disable=SC2029 # plain argument passthrough, no remote command built here
ssh "${flags[@]}" "$@"
}
function _ssh_menu_resolve {
local host=$1 tag=$2
_ssh_menu_ssh -G ${tag:+-P "$tag"} -- "$host" 2>/dev/null
}
# effective config for one profile plus a (cached) reachability probe
function _ssh_menu_probe {
local host=$1 tag=$2 resolved
if ! resolved=$(_ssh_menu_resolve "$host" "$tag") || [[ -z $resolved ]]; then
echo "ssh -G failed for $host${tag:+ (-P $tag)}"
return
fi
# identityfile only when the profile pins one; ssh -G lists all defaults otherwise
awk '
$1 == "identitiesonly" && $2 == "yes" { pinned = 1 }
$1 ~ /^(hostname|port|user|tag|proxyjump|proxycommand|userknownhostsfile)$/ { print }
$1 == "identityfile" && pinned { print }
' <<<"$resolved"
echo ""
_ssh_menu_reach_cached "$host" "$tag" "$resolved"
}
function _ssh_menu_age {
local s=$1
if (( s < 60 )); then echo "${s}s"
elif (( s < 3600 )); then echo "$(( s / 60 ))m"
else echo "$(( s / 3600 ))h"
fi
}
# $1 host: drop cached results for every profile of that host
function _ssh_menu_forget {
rm -f -- "$SSH_MENU_CACHE/$1__"*
}
# $1 host, $2 unlock marker: forget and re-probe now, so the list can be
# reloaded with the fresh state immediately instead of on the next timer tick
function _ssh_menu_reprobe {
_ssh_menu_forget "$1"
_ssh_menu_probe "$1" "" >/dev/null
[[ $2 == unlock ]] && _ssh_menu_probe "$1" luks >/dev/null
return 0
}
# $1 host, $2 tag, $3 ssh -G output: reachability line, served from cache while fresh
function _ssh_menu_reach_cached {
local host=$1 tag=$2 resolved=$3 file age result
file="$SSH_MENU_CACHE/${host}__${tag:-default}"
if [[ -f $file ]]; then
age=$(( $(date +%s) - $(stat -c %Y "$file") ))
if (( age < SSH_MENU_CACHE_TTL )); then
printf '%s (cached %s ago, ctrl-r re-probes)\n' "$(<"$file")" "$(_ssh_menu_age "$age")"
return
fi
fi
result=$(_ssh_menu_reach "$host" "$tag" "$resolved")
printf '%s\n' "$result" > "$file"
printf '%s\n' "$result"
}
# $1 host, $2 tag, $3 ssh -G output: probe the endpoint, one line of result
function _ssh_menu_reach {
local host=$1 tag=$2 resolved=$3 hostname port user proxycommand proxyjump banner
local hop hopuser hophost hopport hopresolved hopopts sshflags cmd
hostname=$(awk '$1 == "hostname" { print $2 }' <<<"$resolved")
port=$(awk '$1 == "port" { print $2 }' <<<"$resolved")
user=$(awk '$1 == "user" { print $2 }' <<<"$resolved")
proxycommand=$(awk '$1 == "proxycommand" { $1 = ""; sub(/^ /, ""); print }' <<<"$resolved")
proxyjump=$(awk '$1 == "proxyjump" && $2 != "none" { print $2 }' <<<"$resolved")
if [[ -n $proxyjump ]]; then
# the target is only reachable through the hop, and opening a forwarded
# channel needs an authenticated session there. if a ControlMaster to
# the hop is already alive we ride it (no auth, no agent); otherwise
# only the hop's own port is probed
hop=${proxyjump%%,*}
hopuser=""
[[ $hop == *@* ]] && hopuser=${hop%%@*}
hop=${hop##*@}
hopport=""
if [[ $hop == *:* ]]; then
hopport=${hop##*:}
hop=${hop%%:*}
fi
# the ControlPath hash covers user and port, so match the jump's exactly
hopopts=(${hopuser:+-l "$hopuser"} ${hopport:+-p "$hopport"})
if _ssh_menu_ssh -O check "${hopopts[@]}" -- "$hop" >/dev/null 2>&1; then
# timeout runs a binary, not a shell function, hence the explicit flags
mapfile -t sshflags < <(_ssh_menu_sshflags)
banner=$(timeout "$SSH_MENU_TIMEOUT" ssh "${sshflags[@]}" -o BatchMode=yes -o IdentityAgent=none -o ControlMaster=no \
"${hopopts[@]}" -W "$hostname:$port" -- "$hop" < <(sleep "$((SSH_MENU_TIMEOUT + 1))") 2>/dev/null | head -n1 | tr -d '\r')
if [[ $banner == SSH-* ]]; then
echo "reachable via $hop ($hostname:$port, $banner)"
else
echo "unreachable via $hop ($hostname:$port)"
fi
else
hopresolved=$(_ssh_menu_resolve "$hop" "")
hophost=$(awk '$1 == "hostname" { print $2 }' <<<"$hopresolved")
hopport=${hopport:-$(awk '$1 == "port" { print $2 }' <<<"$hopresolved")}
if banner=$(_ssh_menu_tcp_banner "$hophost" "$hopport"); then
echo "jump $hop reachable ($hophost:$hopport, $banner); target not probed: no master (ssh -O check ${hopopts[*]} $hop)"
else
echo "jump $hop unreachable ($hophost:$hopport)"
fi
fi
elif [[ -n $proxycommand ]]; then
# running an ssh-based proxy would authenticate to the hop through the agent
# matches "ssh", "/path/to/ssh", "sh -c 'ssh ..." but not "sshuttle" or "foo-ssh"
if [[ $proxycommand =~ (^|[^[:alnum:]_.-])ssh([[:space:]]|$) ]]; then
echo "proxycommand runs ssh; not probed (would need the agent)"
else
# ProxyCommand accepts exactly %%, %h, %n, %p and %r (ssh_config(5) TOKENS);
# %% first so it cannot form a token
cmd=${proxycommand//%%/$'\x01'}
cmd=${cmd//%h/$hostname}
cmd=${cmd//%p/$port}
cmd=${cmd//%r/$user}
cmd=${cmd//%n/$host}
cmd=${cmd//$'\x01'/%}
banner=$(_ssh_menu_proxy_banner "$cmd")
if [[ $banner == SSH-* ]]; then
echo "reachable via proxycommand ($banner)"
else
echo "unreachable via proxycommand"
fi
fi
elif banner=$(_ssh_menu_tcp_banner "$hostname" "$port"); then
echo "reachable ($hostname:$port, $banner)"
else
echo "unreachable ($hostname:$port)"
fi
}
# $1 file, $2 alias, $3 unlock marker
function _ssh_menu_preview {
local file=$1 host=$2 marker=$3
_ssh_menu_block "$file" "$host"
echo ""
echo "--- ssh -G"
_ssh_menu_probe "$host" ""
if [[ $marker == unlock ]]; then
echo ""
echo "--- ssh -G -P luks"
_ssh_menu_probe "$host" luks
fi
}
export -f _ssh_menu_files _ssh_menu_hosts _ssh_menu_state _ssh_menu_with_state \
_ssh_menu_block _ssh_menu_tcp_banner _ssh_menu_proxy_banner _ssh_menu_sshflags _ssh_menu_ssh _ssh_menu_resolve \
_ssh_menu_age _ssh_menu_forget _ssh_menu_reprobe _ssh_menu_reach_cached _ssh_menu_reach _ssh_menu_probe _ssh_menu_preview
# the state column is passive: it only reflects the cache, which the preview
# fills as hosts are visited. a periodic reload keeps the column current; fzf
# older than 0.73 lacks the every() event, so it is added only when supported
function _ssh_menu_reload_opts {
# exit 0 = option accepted and matched; an unknown event exits 2
if printf 'x\n' | fzf --bind 'every(2):ignore' --filter x >/dev/null 2>&1; then
printf '%s\n' '--bind' 'every(2):reload-sync(_ssh_menu_hosts)' '--track' '--id-nth' '2'
fi
}
# prints "<host>" or "<host>\tluks"
function _ssh_menu_select {
local key host marker reload_opts=()
mapfile -t reload_opts < <(_ssh_menu_reload_opts)
{
read -r key
IFS=$'\t' read -r _ host _ _ marker
} < <(_ssh_menu_hosts \
| SHELL=$(command -v bash) fzf \
--delimiter $'\t' \
--with-nth 2..5 \
--nth 1,3 \
--tabstop 24 \
"${reload_opts[@]}" \
--expect ctrl-u \
--bind 'ctrl-r:execute-silent(_ssh_menu_reprobe {2} {5})+refresh-preview+reload-sync(_ssh_menu_hosts)' \
--header 'ENTER connect, CTRL-U unlock (luks), CTRL-R re-probe' \
--preview '_ssh_menu_preview {1} {2} {5}' \
--preview-label 'host' \
--border-label 'ssh-menu')
[[ -z $host ]] && return 0
if [[ $key == ctrl-u ]]; then
printf '%s\tluks\n' "$host"
else
printf '%s\n' "$host"
fi
}
function _ssh_menu_usage {
cat <<'EOF'
usage: ssh-menu [-s | -h]
(no args) pick a host and ssh into it
-s pick a host and print it ("<host>" or "<host>\tluks")
-h show this help
EOF
}
case "${1:-}" in
-h | --help)
_ssh_menu_usage
;;
-s)
_ssh_menu_require fzf ssh awk timeout
_ssh_menu_select
;;
"")
_ssh_menu_require fzf ssh awk timeout
IFS=$'\t' read -r host profile < <(_ssh_menu_select)
[[ -z $host ]] && exit 0
if [[ $profile == luks ]]; then
exec ssh -P luks "$host"
else
exec ssh "$host"
fi
;;
*)
printf 'ssh-menu: unknown argument: %s\n' "$1" >&2
_ssh_menu_usage >&2
exit 1
;;
esac

View File

@@ -1,39 +0,0 @@
# Global CLAUDE.md
## Output format
- for plans, reports, design docs, and anything benefiting from diagrams, styling, or embedded images, prefer HTML over Markdown, except in `.notes/` (the `lead` skill's notes.md says when)
- for conventional files (README, CONTRIBUTING, LICENSE, CHANGELOG, etc.) use the ecosystem default
## Notes
- never stage `.notes/` from the project repo; the `lead` skill's notes.md has the rest
## Diagnostics
- IMPORTANT: find the root cause before proposing a fix; never present a workaround as a fix (see ~/.config/claude/rules/diagnostics.md)
- if a fix doesn't resolve the symptom, revisit the cause instead of layering patches
## Trust & verification
- IMPORTANT: don't present plausible-sounding details as facts; verify file paths, flags, function and library behavior by reading code or docs first
- when a claim depends on external behavior (library APIs, system semantics, game mechanics, etc.), cite the source you verified against, or mark it as unverified
## Planning vs execution
- IMPORTANT: when asked to "plan", "analyze", "deep-dive", or "research", produce documents only; no Edit/Write (except to the plan file or `.notes/`) or Bash mutations until approved
- if a request is ambiguous between planning and implementing, ask which one
## Scope
- don't start unrelated work on your own; if a diff grows beyond the request, stop and confirm
- in orchestrated work (the `lead` skill), an agent working from a brief stays inside the brief instead: it notes anything outside it as a leftover and keeps going
## Comments
- lowercase, no trailing punctuation
- plain: `# descriptive but non-verbose comment`
- keyword: `# KEYWORD:(@janezicmatej) descriptive but non-verbose comment`
- keywords: WARN, FIX, NOTE, TODO, PERF, TEST, HACK
- keyword comments must always include the `(@janezicmatej)` attribution
## Git
- agents never sign commits, in any form: they are not me. commit with `--no-gpg-sign`; I re-sign branches on my host (`git sign-branch`) before they are pushed
- use the identity git already has; where it has none, commit as `-c user.name=agent -c user.email=agent@sandbox.invalid` (my host signing resets the author)
- commit messages: a conventional-commit subject line of at most 72 characters including the prefix; a body only where the project wants one, wrapped at 72; no trailers
- stage paths explicitly; never `git add .` or `git add -A`
- split multi-step work into a few logical commits I can review; condense fixups before handing a branch back
- every branch, worktree and container you create is yours to remove when the work ends; never remove ones you did not create

View File

@@ -1,10 +0,0 @@
---
name: builder-opus
description: Builds one part of an integrator's unit of work on its own branch. Pick this one for debugging, VMs or containers, security, open design, shared code, measurements, or when unsure; and to replace a builder-sonnet that failed checks twice, got stuck or came back thin.
model: opus
skills:
- ground-rules
- builder
---
You are a builder; follow the preloaded builder instructions and the brief in your prompt.

View File

@@ -1,10 +0,0 @@
---
name: builder-sonnet
description: Builds one part of an integrator's unit of work on its own branch. Pick this one when the part is fully specified, touches few files, follows an existing pattern and has fast checks; otherwise builder-opus.
model: sonnet
skills:
- ground-rules
- builder
---
You are a builder; follow the preloaded builder instructions and the brief in your prompt.

View File

@@ -1,14 +0,0 @@
---
name: explorer
description: Builds one option for an open design question in Matej's projects as something he can inspect (runnable code on a prototype branch, a canvas frame). Use when the lead wants an option to look at before deciding.
skills:
- ground-rules
---
You answer an open question with an option Matej can look at and judge, not with a final implementation. Prototypes may cut corners where it doesn't change what he is judging; say which. If another explorer works the same brief, don't look at its work.
Read `~/.config/claude/CLAUDE.md` and the project's AGENTS.md first. Work on the prototype branch your brief names; never merge, push or sign. Make the option inspectable the way the brief asks (a preview he can run on his host, screenshots, a frame) and leave that in place; stop your own servers and containers.
## Reply
Text, verdict first: what the option is and why, how to look at it (exact commands and URLs), the numbers that matter, what you tried and dropped, its weak spots, what you couldn't verify, what you created. At most about 700 words unless the brief asks for more.

View File

@@ -1,18 +0,0 @@
---
name: integrator
description: Owns one unit of work in Matej's projects - builds it on a branch, alone or with builder agents in parallel, reviews and merges their parts, runs every check, and hands back one branch that already works. Use when the lead delegates implementation from a brief.
skills:
- ground-rules
---
You are the integrator for one unit of work. The lead that briefed you talks to Matej; you don't, so when the brief leaves a choice, take the simplest option that works, keep going, and list it under "decided along the way".
Read `~/.config/claude/CLAUDE.md`, the project's AGENTS.md and the house-rule skills it points to before writing code. From `~/.config/claude/skills/lead/SKILL.md` read only your sections: the integrator under Roles, Models, Tests: who runs what, Ownership and clean-up, and Git.
You own the result: a branch that works, lint and format clean, every suite green the way CI runs it, UI looked at in a browser, reviewed for bugs. Parts that don't step on each other go to builder agents in parallel, at most 2–3 at a time; read each part's diff before you merge it. Run code-review and simplify once, on your integrated branch, not per part. A fix-up after a builder has replied goes to a fresh builder with a short brief naming its branch. Never merge into the main branch, push or sign. Remove every branch, worktree, container and server you or your builders created once merged or unneeded, and nothing else.
Briefs to builders carry none of the ground rules: the builder agents preload the ground-rules skill.
## Reply
Text, verdict first: the branch and its commits with what each delivers, the checks with their numbers, what differs from the brief, decided along the way, open items, what you couldn't verify, what you created and removed. At most about 700 words unless the brief asks for more.

View File

@@ -1,10 +0,0 @@
---
name: researcher-opus
description: Answers a factual or technical question with sources and, where it can, a test in a throwaway sandbox. Pick this one for debugging, VMs or containers, security, open design, measurements, comparisons, core decisions, or when unsure; and to replace a researcher-sonnet that got stuck or came back thin.
model: opus
skills:
- ground-rules
- researcher
---
You are a researcher; follow the preloaded researcher instructions and the brief in your prompt.

View File

@@ -1,10 +0,0 @@
---
name: researcher-sonnet
description: Answers a factual or technical question with sources and, where it can, a test in a throwaway sandbox. Pick this one when the answer is clearly in known sources and any check is a short script; otherwise researcher-opus.
model: sonnet
skills:
- ground-rules
- researcher
---
You are a researcher; follow the preloaded researcher instructions and the brief in your prompt.

View File

@@ -1,8 +0,0 @@
# Diagnostics rules
The long form of the headline rules in `CLAUDE.md`.
- Say what the cause is, with the evidence (file and line, log excerpt, command output), before proposing a fix. For system, boot, network and regression bugs, also say which other cause you ruled out and why.
- A workaround is labelled as one, with what would have to change to make it unnecessary. Don't mask an error with a try/except, a retry or a flag without asking first.
- If the fix doesn't resolve the symptom, go back to the cause; don't stack another patch on top.
- Done means the reported behaviour is now right, checked by re-running what failed. An error that went away is not enough. If the fix touched a process, daemon or background job, check it left nothing behind (processes, sockets, locks, temp files).

View File

@@ -1,33 +0,0 @@
{
"permissions": {
"allow": [
"mcp__todo-mcp__*",
"mcp__nixos__*",
"Bash(git remote get-url origin)"
]
},
"model": "opus[1m]",
"hooks": {},
"statusLine": {
"type": "command",
"command": "bash \"${CLAUDE_CONFIG_DIR:-$HOME/.claude}/statusline-command.sh\""
},
"enabledPlugins": {
"typesafe@typesafe-ai": true
},
"extraKnownMarketplaces": {
"typesafe-ai": {
"source": {
"source": "github",
"repo": "typesafe-ai/skills"
}
}
},
"effortLevel": "high",
"subagentPromptCacheTtl": "1h",
"tui": "default",
"skipDangerousModePermissionPrompt": true,
"theme": "dark",
"voiceEnabled": true,
"mcpServers": {}
}

View File

@@ -1,15 +0,0 @@
---
name: builder
description: Role instructions preloaded by the builder-sonnet and builder-opus agents; not for direct use.
user-invocable: false
---
# Builder
You build one part of a unit of work that an integrator owns, from its brief. Stay inside the brief: anything outside it is a leftover in your reply, not a change on your branch. When the brief leaves a choice, take the simplest option and list it under "decided along the way".
Read `~/.config/claude/CLAUDE.md`, the project's AGENTS.md and the house-rule skills it points to before writing code. Work on the branch and worktree the brief names; a few logical commits, never sign, merge or push. Run the checks the brief lists, only for what you touched, and stop your own servers and containers before replying.
## Reply
Text, verdict first: the branch and commits, what works, the checks with their numbers, open items, what you couldn't verify, what you created and removed. At most about 700 words unless the brief asks for more.

View File

@@ -1,15 +0,0 @@
---
name: ground-rules
description: Ground rules shared by every agent type, preloaded by them; not for direct use.
user-invocable: false
---
# Ground rules
- no `bash -c` or `sh -c` wrappers, also not inside `docker exec` or `docker run`: write the script to a file, run `bash <file>`
- remove by literal path or `"${DIR:?}"/name`; never `$DIR/*`, never the cwd or its parents; worktrees with `git worktree remove`
- never `git stash`: use `git show <rev>:<path>` or a scratch copy
- run project tools (uv, ruff, pnpm) only from your worktree, never the main checkout; tool state (venvs, caches, `node_modules`: `UV_PROJECT_ENVIRONMENT`, `RUFF_CACHE_DIR` and the like) under `$SANDBOX_VM_STATE`, never in a host mount
- if the harness refuses something, stop and report it; never work around it
- find with grep, read line ranges, not whole files; send long output (builds, docker, tests) to a log file and show its tail and the failures
- wait on a long job with Monitor in the same turn; never reply with a job still running

View File

@@ -1,103 +0,0 @@
---
name: lead
description: How Matej likes larger work run and its notes kept - a lead session that talks to him and briefs agents, an integrator that hands back a checked branch, builders, researchers and explorers, and the project's local .notes/ (docs, plans, work handoffs, follow-ups, his parked questions). Use when he asks for orchestrated work ("let the integrator handle it", "spawn agents for this", options to compare), for work spanning several branches or sessions, or whenever writing, handing off, closing out or settling anything in .notes/, or when he asks what is pending.
---
# Running work through agents
Guidelines, not a procedure. What is written here is what isn't knowable from the repo.
## Roles
- **Lead**: the session Matej talks to. Discusses, locks decisions, keeps the notes, briefs agents, checks their replies. It doesn't edit the project, so its context stays free for judgement. One work item per lead session: at a natural break it hands off through the work folder and Matej starts a new session, which starts with `notes.py status`. Its replies state outcomes and decisions and what it took on trust, and point Matej at the work item's page, whose `## Your move` says what he does next; detail stays in the notes.
- **Integrator** (`integrator`): owns a unit of work (a phase, or the whole item when it has one part) from brief to one branch that works. It merges its builders' parts into its own branch, never its branch into the work branch or `main` (see Git).
- Parts that don't step on each other go to builders in parallel, at most 2–3 at a time, each on its own branch and worktree under the work id. What everything else builds on (a shared shell, a schema) goes first, alone. Small or tightly coupled work it does itself.
- It reads each part's diff, fixes what is real or sends it to a fresh builder, condenses the part's history, and merges it with `--no-ff`, sorting out what parallel parts collide on (migrations, generated code).
- On the integrated branch, once: code-review and simplify, every check the project has (see Tests), and a browser look when there is UI.
- It removes its builders' branches, worktrees and containers once merged, and its own when it hands back.
- **Builders** (`builder-sonnet`, `builder-opus`): one part each, from the integrator's brief.
- **Researchers** (`researcher-sonnet`, `researcher-opus`): a verified answer with sources.
- **Explorers** (`explorer`): an option Matej can inspect. Prototypes may cut corners; they never land as they are. A prototype branch ends in `-prototype` (`wNNN/<name>-prototype`), so `notes.py status` doesn't count it as work to merge.
## How many agents
One agent per question. Two independent ones only for security and quality reviews, and for a decision Matej flags as core. Any agent that runs others runs at most 2–3. A sharp brief to one researcher beats a manager with a team.
## Models
The pair types carry their model in their frontmatter; never pass a `model` when spawning them, it overrides the frontmatter. The lead picks researchers, the integrator picks builders, by the type descriptions: Sonnet for fully specified, few files, an existing pattern, fast checks (builders) or a clear answer in known sources with a short scripted check (researchers); Opus for debugging, VMs or containers, security, open design, shared code, measurements, comparisons, core decisions, or when unsure. A Sonnet agent that fails its checks twice, gets stuck or comes back thin is replaced by a new Opus agent, briefed from its branch or reply. Integrator and explorer run on the default model.
## Protocol
- Discuss, lock, relay. Only locked decisions go to agents, in the words agreed. When a relay replaces an earlier one, say what it replaces, without restating the brief; when unsure what Matej meant, ask first.
- Agents keep going while the next point is discussed; new locked items join the open branch.
- Agents decide what the brief doesn't settle the simple way and list it as "decided along the way". Only the lead asks Matej.
- Once an agent has replied, it is done. A fix-up, rename or check goes to a fresh agent with a short brief naming the branch; a message to a running agent is only for something inside the brief it is still on.
## Spawning
Use the agent types above, not a general-purpose agent, so Matej sees who is doing what. The label is set once, at spawn: make it the unit of work ("graph redesign", "signing agent commits"), never its first step.
## A brief carries
The goal and what done looks like; where the spec is, as note ids and sections: brief sections and at most one report section, never whole reports, folders or "everything it points to"; the decisions that touch it; scope and what not to touch; the checks with the test scope for the role; the reply shape when it differs from the agent's; what it may create and must clean up. Agents don't see the lead's memory: what they need from it goes in the brief. The standing rules (shell wrappers, `rm` paths, stash, tool state, refusals, quiet output, waiting on jobs, reply shape) are in the ground-rules skill, preloaded by every agent type; briefs don't restate them.
## Tests: who runs what
- **While building a part**: only the tests for what was touched (new tests, the changed modules' test files, tests that import them), plus lint and format on the changed files. Name the selection in the reply.
- **Before a part is merged**: the integrator runs the whole suite of each project member the part touched; live or slow tests (VMs, external services, browsers) only when the part touches what they cover.
- **Once, before reporting**: the integrator runs every check the project lists, on the integrated branch, as CI does. The reply's numbers come from this run.
- **The lead never re-runs suites.** It checks the cheap facts, and re-runs something only when a claim looks off.
## Before passing a reply on
Check the cheap facts: branch heads, commit authors and subject lengths, nothing staged that shouldn't be, worktrees and containers gone. Say what you checked.
## Tuning this flow
A change to this skill, the agent definitions or the notes conventions is kept with its measured difference: replay a finished task on its frozen commit with the old and the new files and compare tokens and outcome from the transcripts. Each kept change gets a line in the work item's as-built doc.
## Where things go
- Plans, decisions, replies: the work item in `.notes/` (see Notes). Its brief holds the current state: a replaced decision is edited in place, git keeps the old one. Agents can't write there; the lead files each reply with `notes.py file` when the notification arrives, and never retypes it.
- Leftovers: `followups/`, by area, at close-out. `questions/` is Matej's own.
- Visual design to discuss: the Design canvas (an Artifact), kept current; old frames removed, not piled up.
- Prototypes and finished work: branches. The repo's `.git` is on Matej's host, so he can `git worktree add --detach` any branch.
- Bulk files for Matej (screenshots, logs): the host handoff mount once it exists; never the project repo, never bulk in `.notes/`.
## Notes
Plans, decisions, work handoffs, follow-ups and Matej's questions live in the project's `.notes/`. Before writing, closing out or settling anything there, read `notes.md` next to this file: layout, ids, the `notes.py` commands (`~/.config/claude/skills/lead/notes.py`), decide blocks, close-out.
## Ownership and clean-up
- Everything a unit of work creates carries its work id: branches `<wNNN>/<name>` (see Git), worktrees under `$SANDBOX_VM_STATE/wt/<wNNN>/`, containers `<wNNN>-<name>`. Agents remove what they created and nothing else.
- The lead keeps `made.md` in the work item: each branch, worktree, container and handoff folder, with its state (open, merged, prototype); whether a branch is in main, `notes.py status` reads from git.
- Close-out removes the list, then checks nothing under the work id is left.
## Interruptions
Any agent can stop (usage limits, Matej stopping it) and can't be resumed. Keep state on disk: small commits, decisions and replies in the work item. Before re-briefing, look at the repo and hand the new agent what is actually there.
## Git
Agents never sign; Matej re-signs on his host before pushing (CLAUDE.md, Git).
```
main
└─ wNNN/<work> the work branch, off main; Matej merges it into main
├─ wNNN/phase1 an integrator's branch, off wNNN/<work>
│ ├─ wNNN/phase1-api a builder's part, off wNNN/phase1
│ └─ wNNN/phase1-ui
└─ wNNN/phase2 off wNNN/<work> once phase1 is merged into it
```
- Each level branches from the one above and goes back with a merge commit (`merge: <what>`, unsigned), so every phase and part stays one merge in the history.
- `<work>` is a short kebab name, usually the item's slug. Never plain `wNNN`, and phases and parts are siblings under `wNNN/`, never under `wNNN/<work>/`: git can't hold a branch next to branches below it. The lead creates the work branch off `main` when the first phase is briefed.
- One part, no phases: the integrator's branch is `wNNN/<work>` itself.
- Parts: the integrator merges each into its phase branch with `git merge --no-ff`.
- Phases: once the lead has checked a phase's reply, it merges the phase branch into `wNNN/<work>` with `--no-ff` and deletes it, right away; the next phase branches off that merge commit, never off the previous phase's tip. A conflict goes to an integrator; the lead doesn't resolve code.
- A phase that must start before the previous one is merged branches off its tip, then is rebased onto the merge commit (`git rebase --onto <merge> <previous tip>`) before it is merged.
- A work item built on another one not yet in `main` starts from that one's work branch tip; once Matej merges it (his signing rewrites the shas), the lead rebases with `--rebase-merges` onto that merge commit.
- Two branches that conflict (a refactor and a fix pass on the same code) aren't resolved hunk by hunk. An integrator, for each conflicting part, picks the better implementation (more correct, tested, fits the code) and says why; adopts it; rebuilds the other branch's remaining intent as new commits on a fresh branch off the winner; runs every check; and lists what was adopted, rebuilt and dropped. The lead only picks which pairs need it.
- Only Matej merges into `main`.

View File

@@ -1,980 +0,0 @@
/*
* notes.css: the one stylesheet for every page in a project's .notes/
* notes.py copies it to .notes/notes.css; edit it here, in the lead skill, never in a project
*
* gruvbox material (sainnhe/gruvbox-material: medium contrast, material foreground), light or dark
* by the system scheme; jetbrains mono for all text
*
* page skeleton for a doc (docs/dNNN-slug.html; one level deeper for a folder doc):
* <link rel="stylesheet" href="../notes.css">
* <body><main class="page">
* <nav class="back"><a href="../index.html">← notes</a></nav>
* <header>
* <p class="eyebrow">d007 · 2026-10-13</p>
* <h1>Title</h1>
* <p class="status current|stale|superseded">status: …</p>
* <p class="lede">one or two sentences</p>
* </header>
* <section><h2>…</h2> …</section>
* </main></body>
*
* components:
* .callout (+ .good .warn .bad .info) a boxed note, recommendation, open point
* .tag (+ .good .warn .bad .info .muted) a small inline label
* .decide > .option(.recommended .chosen) two options left and right, one column on a phone;
* a markdown ```decide block renders it, see the skill
* .table > table a table that scrolls sideways when wide
* figure.diagram > svg (+ figcaption) an inline svg diagram, see "diagrams" below
* dl.facts > dt + dd label and value pairs in two columns (repro, cause, fix)
* pre.diff > span(.add .del) a unified diff, one span per line
* .legend > span > svg + text a row of notation keys above a diagram
* .swatch (+ .info .good .warn .bad) a small colour square that matches a diagram box
* main.page.wide a wider page, for pages carried by wide diagrams
* .sub, .muted secondary text
* .fix a line prefixed with an arrow
*/
@import url("https://fonts.googleapis.com/css2?family=JetBrains+Mono:ital,wght@0,400;0,500;0,600;0,700;1,400&display=swap");
:root {
color-scheme: light dark;
--radius: 0.375rem;
/* gruvbox material light */
--red: #c14a4a;
--orange: #c35e0a;
--yellow: #b47109;
--green: #6c782e;
--aqua: #4c7a5d;
--blue: #45707a;
--purple: #945e80;
--background: #fbf1c7;
--foreground: #654735;
--card: #f4e8be;
--muted: #eee0b7;
--muted-foreground: #7c6f64;
--border: #ddccab;
--input: #a89984;
--link: var(--blue);
/* text colours and their tints, both follow the scheme's palette */
--good: var(--green);
--warn: var(--yellow);
--bad: var(--red);
--info: var(--blue);
--good-bg: color-mix(in srgb, var(--green) 14%, transparent);
--warn-bg: color-mix(in srgb, var(--yellow) 16%, transparent);
--bad-bg: color-mix(in srgb, var(--red) 13%, transparent);
--info-bg: color-mix(in srgb, var(--blue) 13%, transparent);
--accent: var(--orange);
--accent-bg: color-mix(in srgb, var(--orange) 12%, transparent);
--font-mono: "JetBrains Mono", ui-monospace, SFMono-Regular, Menlo, monospace;
}
@media (prefers-color-scheme: dark) {
:root {
/* gruvbox material dark */
--red: #ea6962;
--orange: #e78a4e;
--yellow: #d8a657;
--green: #a9b665;
--aqua: #89b482;
--blue: #7daea3;
--purple: #d3869b;
--background: #282828;
--foreground: #d4be98;
--card: #32302f;
--muted: #45403d;
--muted-foreground: #a89984;
--border: #45403d;
--input: #5a524c;
--good-bg: color-mix(in srgb, var(--green) 16%, transparent);
--warn-bg: color-mix(in srgb, var(--yellow) 15%, transparent);
--bad-bg: color-mix(in srgb, var(--red) 16%, transparent);
--info-bg: color-mix(in srgb, var(--blue) 16%, transparent);
--accent-bg: color-mix(in srgb, var(--orange) 15%, transparent);
}
}
/* base */
*,
*::before,
*::after {
box-sizing: border-box;
}
html {
scrollbar-gutter: stable;
-webkit-text-size-adjust: 100%;
}
body {
margin: 0;
background: var(--background);
color: var(--foreground);
font: 400 14px/1.65 var(--font-mono);
-webkit-font-smoothing: antialiased;
}
main.page {
max-width: 860px;
margin: 0 auto;
padding: 24px 16px 96px;
}
main.page.wide {
max-width: 1240px;
}
a {
color: var(--link);
text-decoration: none;
}
a:hover {
text-decoration: underline;
}
a:focus-visible {
outline: 2px solid var(--blue);
outline-offset: 2px;
border-radius: 3px;
}
h1,
h2,
h3,
h4 {
font-weight: 600;
line-height: 1.25;
text-wrap: balance;
margin: 0;
}
h1 {
font-size: 26px;
}
h2 {
font-size: 19px;
margin: 44px 0 12px;
padding-top: 24px;
border-top: 1px solid var(--border);
}
h3 {
font-size: 16px;
margin: 28px 0 8px;
}
h4 {
font-size: 15px;
margin: 20px 0 6px;
}
section > h2:first-child {
margin-top: 40px;
}
p,
ul,
ol,
blockquote,
pre,
figure,
.table,
.callout,
.decide,
details {
margin: 0 0 14px;
}
ul,
ol {
padding-left: 22px;
}
li + li {
margin-top: 4px;
}
li > ul,
li > ol {
margin: 4px 0 0;
}
strong,
b {
font-weight: 600;
}
hr {
border: 0;
border-top: 1px solid var(--border);
margin: 32px 0;
}
blockquote {
padding: 2px 0 2px 16px;
border-left: 3px solid var(--border);
color: var(--muted-foreground);
}
img {
max-width: 100%;
height: auto;
border-radius: var(--radius);
border: 1px solid var(--border);
}
code,
kbd,
pre,
.mono {
font-family: var(--font-mono);
font-size: 0.93em;
}
code {
background: var(--muted);
padding: 1px 5px;
border-radius: 4px;
overflow-wrap: break-word;
}
pre {
background: var(--card);
border: 1px solid var(--border);
border-radius: var(--radius);
padding: 12px 14px;
overflow-x: auto;
line-height: 1.5;
}
pre code {
background: none;
padding: 0;
font-size: 1em;
overflow-wrap: normal;
}
/* page chrome */
nav.back {
font-size: 13px;
margin-bottom: 28px;
}
nav.back a {
color: var(--muted-foreground);
}
nav.back a:hover {
color: var(--link);
}
header {
margin-bottom: 8px;
}
header > * + * {
margin-top: 10px;
}
.eyebrow {
font: 500 12px/1.4 var(--font-mono);
letter-spacing: 0.04em;
color: var(--muted-foreground);
margin: 0;
}
.lede {
font-size: 17px;
line-height: 1.6;
margin-top: 14px;
}
.sub,
.muted {
color: var(--muted-foreground);
}
p.status {
display: inline-block;
font-size: 13px;
line-height: 1.5;
padding: 3px 10px;
border-radius: 6px;
background: var(--muted);
color: var(--muted-foreground);
}
p.status.current {
background: var(--good-bg);
color: var(--good);
}
p.status.stale {
background: var(--warn-bg);
color: var(--warn);
}
.fix::before {
content: "→ ";
color: var(--accent);
font-weight: 600;
}
/* components */
.callout {
background: var(--card);
border: 1px solid var(--border);
border-left: 3px solid var(--input);
border-radius: var(--radius);
padding: 10px 14px;
}
.callout > :last-child {
margin-bottom: 0;
}
.callout.good {
background: var(--good-bg);
border-color: transparent;
border-left-color: var(--good);
}
.callout.warn {
background: var(--warn-bg);
border-color: transparent;
border-left-color: var(--warn);
}
.callout.bad {
background: var(--bad-bg);
border-color: transparent;
border-left-color: var(--bad);
}
.callout.info {
background: var(--info-bg);
border-color: transparent;
border-left-color: var(--info);
}
.tag {
display: inline-block;
font: 500 11px/1.5 var(--font-mono);
letter-spacing: 0.02em;
padding: 1px 8px;
border-radius: 99px;
white-space: nowrap;
vertical-align: 1px;
background: var(--muted);
color: var(--muted-foreground);
}
.tag.good {
background: var(--good-bg);
color: var(--good);
}
.tag.warn {
background: var(--warn-bg);
color: var(--warn);
}
.tag.bad {
background: var(--bad-bg);
color: var(--bad);
}
.tag.info {
background: var(--info-bg);
color: var(--info);
}
/* two options side by side, the only side by side a page has */
.decide {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 12px;
}
@media (max-width: 720px) {
.decide {
grid-template-columns: minmax(0, 1fr);
}
}
.decide + .decide-paste {
margin-top: -6px;
font-size: 13px;
}
.option {
display: flex;
flex-direction: column;
align-items: flex-start;
background: var(--card);
border: 1px solid var(--border);
border-radius: var(--radius);
padding: 12px 14px;
}
.option.recommended {
border-color: var(--good);
}
.option.chosen {
border-color: var(--info);
box-shadow: inset 3px 0 0 var(--info);
}
.option > p,
.option > ul {
margin-bottom: 8px;
}
.option > :last-child {
margin-bottom: 0;
}
.option-head .tag {
margin-left: 6px;
}
.option button {
margin-top: auto;
font: 500 13px/1.4 var(--font-mono);
color: var(--foreground);
background: var(--muted);
border: 1px solid var(--input);
border-radius: var(--radius);
padding: 5px 12px;
cursor: pointer;
}
.option button:hover {
border-color: var(--accent);
}
.option button:focus-visible {
outline: 2px solid var(--blue);
outline-offset: 2px;
}
.table {
overflow-x: auto;
}
table {
border-collapse: collapse;
width: 100%;
font-size: 14px;
}
th,
td {
text-align: left;
vertical-align: top;
padding: 7px 10px;
border-bottom: 1px solid var(--border);
}
th {
font-weight: 600;
font-size: 13px;
color: var(--muted-foreground);
}
td > :last-child {
margin-bottom: 0;
}
td .tag {
white-space: normal;
}
td code {
overflow-wrap: anywhere;
}
details {
background: var(--card);
border: 1px solid var(--border);
border-radius: var(--radius);
padding: 8px 14px;
}
summary {
cursor: pointer;
font-weight: 500;
}
details[open] > summary {
margin-bottom: 8px;
}
dl.facts {
display: grid;
grid-template-columns: max-content minmax(0, 1fr);
gap: 6px 16px;
margin: 0 0 14px;
}
dl.facts dt {
color: var(--muted-foreground);
font-weight: 500;
}
dl.facts dd {
margin: 0;
}
@media (max-width: 560px) {
dl.facts {
grid-template-columns: minmax(0, 1fr);
gap: 0;
}
dl.facts dd + dt {
margin-top: 8px;
}
}
/* one column as wide as the longest line, so every line's tint spans it when scrolled */
pre.diff {
display: grid;
grid-template-columns: minmax(max-content, 1fr);
}
pre.diff span {
white-space: pre;
}
pre.diff .add {
background: var(--good-bg);
color: var(--good);
}
pre.diff .del {
background: var(--bad-bg);
color: var(--bad);
}
.legend {
display: flex;
flex-wrap: wrap;
gap: 8px 22px;
margin: 0 0 14px;
font-size: 13px;
color: var(--muted-foreground);
}
.legend > span {
display: inline-flex;
align-items: center;
gap: 8px;
}
.legend svg {
width: 34px;
height: 20px;
flex: none;
}
.swatch {
display: inline-block;
width: 10px;
height: 10px;
border-radius: 3px;
border: 1px solid var(--muted-foreground);
background: var(--card);
vertical-align: 0;
}
.swatch.info {
background: color-mix(in srgb, var(--blue) 14%, var(--card));
border-color: var(--blue);
}
.swatch.good {
background: color-mix(in srgb, var(--green) 14%, var(--card));
border-color: var(--green);
}
.swatch.warn {
background: color-mix(in srgb, var(--yellow) 18%, var(--card));
border-color: var(--yellow);
}
.swatch.bad {
background: color-mix(in srgb, var(--red) 14%, var(--card));
border-color: var(--red);
}
/*
* diagrams: inline svg inside figure.diagram, drawn with these classes so both schemes work
* <g class="node [accent|dashed|good|bad|warn]"><rect …/><text …>Label</text><text class="sub" …>detail</text></g>
* <path class="edge [back|good|bad|warn]" … marker-end="url(#arrow)"/>
* <path class="arrow [back|good|bad|warn]" d="M0 0 L10 5 L0 10 z"/> inside a <marker>
* <text class="edge-label [back|good|bad|warn]" …>label</text>
* <rect class="group" …/> and <text class="group-label" …> a dashed box around nodes
* for drawings with their own text placement (a data flow, a sequence), classes go on the shapes:
* <rect class="box [info|good|warn|bad|accent] [strong] [dashed] [open]"/> a step, process or entity
* <rect class="zone [info|good|warn|bad|accent] [boundary]"/> a tinted dashed area; boundary strokes it red
* <rect class="panel"/> a quiet solid area behind a group of boxes
* <rect class="chip [good|accent]"/> + <text class="chip-text"> a small filled badge or numbered dot
* <path class="line"/> a plain stroke (a data store's rails) <line class="rule"/> a faint dashed guide
* <rect class="fill"/> a borderless card-coloured shape: a data store's body, a backdrop
* <text class="title|sub|mono|heading [info|good|warn|bad]"> text roles; text-anchor stays an attribute
* .edge.dashed an answer or a nudge, not a main flow
* figure.diagram.wide svg at least 1000px wide, scrolls sideways on a phone
* an svg may also use currentColor and var(--good|--bad|--warn|--accent) in fill and stroke attributes
*/
figure {
margin-left: 0;
margin-right: 0;
}
figure.diagram {
background: var(--card);
border: 1px solid var(--border);
border-radius: var(--radius);
padding: 14px;
overflow-x: auto;
color: var(--foreground);
}
figure.diagram svg {
display: block;
width: 100%;
height: auto;
min-width: 640px;
font-family: var(--font-mono);
}
figure.diagram.wide svg {
min-width: 1000px;
}
figure.diagram text {
fill: var(--foreground);
}
figcaption {
color: var(--muted-foreground);
font-size: 13px;
margin-top: 8px;
}
.node rect,
.node circle,
.node ellipse,
.node polygon {
fill: var(--card);
stroke: var(--muted-foreground);
stroke-width: 1;
}
.node.accent rect,
.node.accent circle,
.node.accent polygon {
fill: var(--accent-bg);
stroke: var(--accent);
stroke-width: 1.5;
}
.node.dashed rect,
.node.dashed circle,
.node.dashed polygon {
stroke-dasharray: 5 4;
}
.node.good rect {
fill: var(--good-bg);
stroke: var(--good);
}
.node.bad rect {
fill: var(--bad-bg);
stroke: var(--bad);
}
.node.warn rect {
fill: var(--warn-bg);
stroke: var(--warn);
}
.node text {
fill: var(--foreground);
font: 500 14px var(--font-mono);
text-anchor: middle;
}
.node text.sub {
fill: var(--muted-foreground);
font: 400 11px var(--font-mono);
}
.edge {
fill: none;
stroke: var(--muted-foreground);
stroke-width: 1.4;
}
.arrow {
fill: var(--muted-foreground);
}
.edge-label {
fill: var(--muted-foreground);
font: 400 11px var(--font-mono);
text-anchor: middle;
paint-order: stroke;
stroke: var(--card);
stroke-width: 4px;
stroke-linejoin: round;
}
.edge.back {
stroke: var(--accent);
stroke-dasharray: 5 4;
}
.arrow.back,
.edge-label.back {
fill: var(--accent);
}
.edge.good {
stroke: var(--good);
}
.arrow.good,
.edge-label.good {
fill: var(--good);
}
.edge.bad {
stroke: var(--bad);
}
.arrow.bad,
.edge-label.bad {
fill: var(--bad);
}
.edge.warn {
stroke: var(--warn);
}
.arrow.warn,
.edge-label.warn {
fill: var(--warn);
}
.group {
fill: none;
stroke: var(--muted-foreground);
stroke-opacity: 0.5;
stroke-dasharray: 5 3;
}
.group-label {
fill: var(--muted-foreground);
font: 500 11px var(--font-mono);
}
/* shapes with their own classes, for drawings that place their own text */
svg .box {
fill: var(--card);
stroke: var(--muted-foreground);
stroke-width: 1.2;
}
svg .box.info {
fill: color-mix(in srgb, var(--blue) 14%, var(--card));
stroke: var(--blue);
}
svg .box.good {
fill: color-mix(in srgb, var(--green) 14%, var(--card));
stroke: var(--green);
}
svg .box.warn {
fill: color-mix(in srgb, var(--yellow) 18%, var(--card));
stroke: var(--yellow);
}
svg .box.bad {
fill: color-mix(in srgb, var(--red) 14%, var(--card));
stroke: var(--red);
}
svg .box.accent {
fill: color-mix(in srgb, var(--orange) 14%, var(--card));
stroke: var(--orange);
}
svg .box.strong {
stroke: var(--foreground);
stroke-width: 1.6;
}
svg .box.dashed,
svg .edge.dashed {
stroke-dasharray: 5 4;
}
svg .box.open {
fill: none;
}
svg .zone {
fill: color-mix(in srgb, var(--muted-foreground) 6%, transparent);
stroke: var(--muted-foreground);
stroke-width: 1.5;
stroke-dasharray: 7 5;
}
svg .zone.info {
fill: color-mix(in srgb, var(--blue) 7%, transparent);
}
svg .zone.good {
fill: color-mix(in srgb, var(--green) 7%, transparent);
}
svg .zone.warn {
fill: color-mix(in srgb, var(--yellow) 9%, transparent);
}
svg .zone.bad {
fill: color-mix(in srgb, var(--red) 6%, transparent);
}
svg .zone.accent {
fill: color-mix(in srgb, var(--orange) 7%, transparent);
}
svg .zone.boundary {
stroke: var(--red);
}
svg .panel {
fill: color-mix(in srgb, var(--foreground) 4%, transparent);
stroke: var(--border);
}
svg .chip {
fill: var(--blue);
}
svg .chip.good {
fill: var(--green);
}
svg .chip.accent {
fill: var(--orange);
}
svg text.chip-text {
fill: var(--background);
font: 700 10px var(--font-mono);
}
svg .fill {
fill: var(--card);
stroke: none;
}
svg .line {
fill: none;
stroke: var(--foreground);
stroke-width: 1.6;
}
svg .rule {
stroke: var(--border);
stroke-width: 1.5;
stroke-dasharray: 3 5;
}
svg text.title {
fill: var(--foreground);
font: 600 13px var(--font-mono);
}
svg text.sub {
fill: var(--muted-foreground);
font: 400 11px var(--font-mono);
}
svg text.mono {
fill: var(--muted-foreground);
font: 400 10.5px var(--font-mono);
}
svg text.heading {
fill: var(--muted-foreground);
font: 600 11px var(--font-mono);
letter-spacing: 0.06em;
text-transform: uppercase;
}
svg text.info {
fill: var(--info);
}
svg text.good {
fill: var(--good);
}
svg text.warn {
fill: var(--warn);
}
svg text.bad {
fill: var(--bad);
}
/* index */
.index-head h1 {
font-size: 26px;
}
.index-head p {
margin: 4px 0 0;
color: var(--muted-foreground);
font-size: 13px;
}
.index h2 {
display: flex;
align-items: baseline;
gap: 8px;
font-size: 13px;
font-weight: 600;
letter-spacing: 0.06em;
text-transform: uppercase;
color: var(--muted-foreground);
margin: 36px 0 6px;
padding: 0;
border: 0;
}
.index h2 .count {
font: 500 12px var(--font-mono);
letter-spacing: 0;
}
.rows {
list-style: none;
padding: 0;
margin: 0;
background: var(--card);
border: 1px solid var(--border);
border-radius: var(--radius);
}
.rows li {
display: grid;
grid-template-columns: 52px minmax(0, 1fr) auto;
gap: 12px;
align-items: baseline;
padding: 9px 14px;
margin: 0;
}
.rows li + li {
border-top: 1px solid var(--border);
}
.rows .id {
font: 500 12px var(--font-mono);
color: var(--muted-foreground);
}
.rows .meta {
font-size: 12px;
color: var(--muted-foreground);
white-space: nowrap;
}
.empty {
color: var(--muted-foreground);
font-size: 13px;
margin: 0;
}
@media (max-width: 560px) {
.rows li {
grid-template-columns: 44px minmax(0, 1fr);
}
.rows .meta {
grid-column: 2;
}
}
/* a page rendered from markdown */
.md-meta {
display: flex;
flex-wrap: wrap;
gap: 4px 16px;
font-size: 13px;
color: var(--muted-foreground);
margin: 12px 0 0;
}
.md-meta dt {
display: inline;
font-weight: 500;
}
.md-meta dd {
display: inline;
margin: 0 0 0 4px;
}
.md-meta div {
min-width: 0;
}
.files {
font-size: 13px;
color: var(--muted-foreground);
margin-top: 32px;
padding-top: 16px;
border-top: 1px solid var(--border);
}
/* serve's hub: one row per project */
.rows.hub li {
grid-template-columns: minmax(0, 1fr) auto;
}
@media (max-width: 560px) {
.rows.hub li {
grid-template-columns: minmax(0, 1fr);
}
.rows.hub .meta {
grid-column: 1;
}
}
/* serve's search */
form.search,
input.filter {
margin: 24px 0 0;
}
form.search input,
input.filter {
width: 100%;
font: inherit;
color: var(--foreground);
background: var(--card);
border: 1px solid var(--input);
border-radius: var(--radius);
padding: 9px 14px;
}
form.search input:focus,
input.filter:focus {
outline: 2px solid var(--blue);
outline-offset: 1px;
}
.rows.results li {
grid-template-columns: 52px minmax(0, 1fr);
}
.rows.results .meta {
display: none;
}
.snippet {
display: block;
font-size: 13px;
color: var(--muted-foreground);
margin-top: 2px;
}
mark {
background: var(--warn-bg);
color: inherit;
border-radius: 3px;
padding: 0 2px;
}
/* a work item: its move line under the title in the index, the move box on top of its page */
.rows .title .move {
display: block;
margin-top: 2px;
}
.callout.move h2 {
font-size: 16px;
margin: 0 0 8px;
padding-top: 0;
border-top: 0;
}

View File

@@ -1,107 +0,0 @@
# Notes
Each project keeps its local working notes in `.notes/` at the project root, its own git repo with no remote, never pushed and never committed to the project. Every step that changes it ends with `commit`, which also regenerates the generated files (never committed): `index.html`, `notes.css`, `_pages/` (markdown rendered), `_out/` (`other/out/` made self-contained). Change the look only in `notes.css` next to this file.
Who reads what: Matej reads the work item, starting at its brief's `## Your move`, the pages it links, and his questions; nothing else. Docs, follow-ups and the rest of a work folder are the lead's and the agents', shaped for them. The index lists only work items and questions; everything else is still rendered, so a link from a work item opens.
```
N=~/.config/claude/skills/lead/notes.py
python3 $N init # create .notes/ if missing (idempotent)
python3 $N next d|w|f|q # next free id, counts deleted items too
python3 $N new d <slug> # docs/dNNN-slug.md on the skeleton; --html for a doc with a diagram
python3 $N new w <slug> # work/wNNN-slug/brief.md on the skeleton
python3 $N commit "<subject>" # regenerate, then commit all of .notes/
python3 $N index # regenerate only
python3 $N status # open work items: move, next, decisions waiting, branches in main or not; counts
python3 $N file wNNN <part> <output-file|agent-id> --checked "<what the lead checked>"
# the agent's final reply, verbatim, into work/wNNN-*/report-<part>.md
python3 $N serve # every project's notes on http://127.0.0.1:8765/, decide clicks recorded (the user runs this)
```
All commands take `--root <project>`; the default is the git toplevel of the current dir. Never pick an id by hand: ids are `<letter><NNN>-<slug>` (`d` docs, `w` work, `f` follow-ups, `q` questions, three digits, kebab slug), never reused, and deleted ones live only in git history. Refer to items by id everywhere ("see q014", "per d007"); in markdown an id becomes a link.
```
.notes/
docs/ dNNN-slug.md, or .html / dNNN-slug/index.html for a doc with a diagram and its assets
work/ wNNN-slug/ while in flight, removed at close-out
followups/ fNNN-slug.md, removed when done or moot
questions/ qNNN-slug.md, Matej's own, removed when he settles them
other/in/ placed by the user, read only when pointed at
other/out/ prepared for someone else
```
Markdown for everything, docs included: they are agent-facing. HTML only for a diagram Matej needs, linked from his work item. Screenshot a page (headless browser, light and dark) only when it has a diagram or a decide block.
## Docs
Point-in-time records of a type: `plan` (a proposal to approve), `research` (findings with sources), `review` (what a review found and what was done), `as built` (the lasting design of finished work, written at close-out).
- `new d <slug>`, then fill it in. The markdown skeleton's frontmatter holds `type`, `date` and `status`; the html one (`--html`) links `notes.css`, starts `main.page` with `nav.back`, and uses only the components listed at the top of `notes.css`, no `<style>` or font links. A folder doc's relative paths gain one `../`.
- status: `current`, `partly stale (<what changed>, <date>)` or `superseded by dNNN (<date>)`; in html, `<p class="status current|stale|superseded">status: …</p>` under the title, with `dNNN` a relative link. Replacing a doc means setting the older one's status.
- Once done, only a doc's status changes; a new design gets a new doc.
- At close-out a plan or review doc is deleted once its lasting decisions are in an as-built doc or in the code. "Partly stale" doesn't outlive the next session: fix the doc or delete it.
- `commit "dNNN: <slug>"`
## Work items
One folder per piece of work that spans sessions; a single-session task gets none. It is the handoff: a later session picks the work up from the folder alone.
- `new w <slug>` writes `brief.md`: goal, `## Your move`, done when, scope, prerequisites, and `## Decided` with each locked decision and its date.
- `## Your move` is Matej's entry point: one to three lines with links (a page to read, a decision to click). The page opens with it in a box, with links to the decisions still waiting and the item's branches, in main or not. The lead keeps it current and its chat reply points at the page instead of asking in chat.
- The brief holds the current state, not its history: a replaced decision, fact or next step is edited in place, and git keeps the old one. An agent's brief cites brief sections and at most one report section, never whole reports or folders.
- As the work goes: `plan.md` (the proposal), a brief per phase or part an agent builds, `report-<part>.md` per reply, `made.md` (every branch, worktree, container and folder the work created, with its state; whether a branch is in main, `status` reads from git), research, and `decisions.md` (written by `serve`; the lead copies its entries into `Decided` and leaves them there, and appends a choice Matej gives in chat as `- <date> <key>: <choice> (matej, in chat)`, so the block shows it chosen and `status` stops listing it).
- Replies are filed with `file` when the notification arrives, from its `<output-file>` or the agent id: the lead types only the check line, never the reply. Its transcript's last reply changes if the agent goes on later, so file each reply on its notification.
- `commit "wNNN: <what changed>"`. A folder untouched for weeks with no open branch: tell the user, don't close it.
### Decisions in a page
A fenced block in any markdown file of a work item:
````
```decide d1-policy
a*) fold into the skill
one file, loaded only with the skill
b) a path-scoped rule
keeps two files
```
````
The info string is `decide <key>` (`[a-z0-9-]`); an option starts with `<letter>) <title>`, `*` before the `)` marks the recommended one, and the lines up to the next option are its text. It renders as two options side by side, each with a button. Under `serve` a click appends `- <date> <key>: <choice> (matej, clicked)` to the item's `decisions.md`, commits `wNNN: <key> decided, <choice>`, and the page marks the chosen option; `git -C .notes log -1` shows it when Matej says he decided. Opened without `serve`, the block shows a line for him to paste instead (`w012 d1-policy: a|b`). `serve` accepts only loopback, same-origin posts, and only an option the item's own pages offer for that key.
## Close-out
When the work's branch is merged or the work is dropped, in that same session:
1. lasting design: a new `as built` or `review` doc, or a status line on an existing one; delete the plan and review docs it covers (see Docs)
2. leftovers: follow-ups (below), by area; never questions
3. remove everything `made.md` lists, `rm -r work/wNNN-slug`, `commit "wNNN: closed, <merged as <sha>|dropped>; <dNNN, fNNN touched>"`
## Follow-ups
What finished work found and left undone (defects, rough edges, unverified claims, ideas, to-dos after a deploy), one file per area, not per item.
1. a file for the area exists: append `## From wNNN, <date>` and add `wNNN` to its `origin`
2. otherwise `next f` and write `followups/fNNN-slug.md`: frontmatter `created`, `origin: wNNN`, `status: open`; `# <the area, as a noun phrase>`; then per item what it is, the evidence (`file:line`, a commit, a measurement) and a direction in a line, so a session with no memory of today can act on it
3. `commit "fNNN: <what was added>"`
Settling: check the repo first (items go stale), remove the done or moot items, `rm` an empty file, `commit "fNNN: <items> settled, <done in <sha> | moot because ...>"`. A follow-up area becomes work by a work item that cites it.
## Questions
Matej's own. Park one only when he asks, in his words; never for your own leftovers.
1. `next q`, write `questions/qNNN-slug.md`: frontmatter `created`, `asked-by: matej`, `origin`, `status: open`; `# <his question>`; `## Known` with what is known, sourced, and where to look (you may add sourced facts there later, never new questions)
2. `commit "qNNN: <slug>"`
Settling: check the repo first, fold the outcome into a doc or work item if worth keeping, `rm` the file, `commit "qNNN: settled, <done in <sha> | moot because ... | moved to wNNN>"`.
## Other
`other/out/` holds documents for someone else, written for that reader, and the few small files that go with them; no ids, no status line. HTML there follows the doc skeleton with `../../notes.css`; the eyebrow names the reader and date (`for the team · 2026-10-14`). What gets sent is the generated `_out/<file>`; look at it before handing it over, link it from the work item's `## Your move` (the index doesn't list `other/`), then `commit "other: <file>"`. Bulk output for a person (screenshots, builds, logs) goes to the host handoff mount, not `.notes/`.
## Never
- stage `.notes/` from the project repo (`git add .`, `git add -A`), or reference `.notes/` paths from committed project files
- keep archives or dated copies: delete, git keeps it
- push `.notes/` or add a remote
- put in notes what every session needs (AGENTS.md, CLAUDE.md, memory), what teammates need (the project repo), or secrets

File diff suppressed because it is too large Load Diff

View File

@@ -1,13 +0,0 @@
---
name: researcher
description: Role instructions preloaded by the researcher-sonnet and researcher-opus agents; not for direct use.
user-invocable: false
---
# Researcher
You find out, and you show how you know. Check claims against primary sources (official docs, man pages, source code) and cite them; mark anything you couldn't verify. Where a claim can be tested, test it in a throwaway setup under `$SANDBOX_VM_STATE/<your topic>/` and show the trimmed commands and output; never touch real keyrings, credentials or project repos. Read `~/.config/claude/CLAUDE.md` first for how Matej wants facts handled. Leave your sandbox folder for review.
## Reply
Text, verdict first: the answer as a short recommendation, the key numbers, the evidence per option with sources, open items, what is unverified, where your sandbox folder is. At most about 700 words unless the brief asks for more.

View File

@@ -1,158 +0,0 @@
#!/usr/bin/env bash
# NOTE:(@janezicmatej) layout adapted from github.com/nejcr/grind statusline.js:
# model + context, repo, 5h/7d limits, session id, optional dev-links row
input=$(cat)
# one jq call emits every field as a quoted shell assignment
eval "$(jq -r '
def s: tostring | @sh;
"model=\(.model.display_name // "" | s)",
"used=\(.context_window.total_input_tokens // 0 | s)",
"total=\(.context_window.context_window_size // 0 | s)",
"ctx_pct=\(.context_window.used_percentage // "" | s)",
"cwd=\(.workspace.current_dir // "" | s)",
"project_dir=\(.workspace.project_dir // "" | s)",
"session_id=\(.session_id // "" | s)"
' <<<"$input")"
# claude only sends rate_limits after the first api response, so a fresh session
# would show none until its first prompt; every refresh that has them writes the
# last known limits next to the config, and a fresh session reads those back dimmed
cfg_dir=${CLAUDE_CONFIG_DIR:-$HOME/.claude}
limits_cache=$cfg_dir/statusline-limits.json
read_limits() {
jq -r '
def s: tostring | @sh;
"five_pct=\(.five_hour.used_percentage // "" | s)",
"five_reset=\(.five_hour.resets_at // "" | s)",
"seven_pct=\(.seven_day.used_percentage // "" | s)",
"seven_reset=\(.seven_day.resets_at // "" | s)"
' 2>/dev/null
}
current=$(jq -c '.rate_limits // {} | with_entries(select(.value.used_percentage != null))' <<<"$input")
stale=""
if [[ $current != "{}" ]]; then
eval "$(read_limits <<<"$current")"
# merged, not replaced: a turn carrying one window must not wipe the other;
# written then renamed so a concurrent session never reads it half-written
merged=$(jq -c --argjson cur "$current" '. * $cur' "$limits_cache" 2>/dev/null) || merged=$current
tmp=$limits_cache.$$.tmp
if { printf '%s\n' "$merged" > "$tmp"; } 2>/dev/null; then mv -f "$tmp" "$limits_cache" 2>/dev/null || rm -f "$tmp"; fi
elif [[ -f $limits_cache ]]; then
stale=1
eval "$(read_limits < "$limits_cache")"
fi
R=$'\e[0m' C=$'\e[36m' G=$'\e[32m' Y=$'\e[33m' RE=$'\e[31m' D=$'\e[2m'
# "Claude 3.5 Sonnet" -> "Sonnet 3.5", "Claude Fable 5.1" -> "Fable 5.1"
model=$(sed -E -e 's/^Claude +([0-9.]+) +([A-Za-z0-9_]+)$/\2 \1/; t' -e 's/^Claude +//' <<<"${model:-unknown}")
# 61234 -> 61.2k, half-up rounding to match Math.round
fmt() { awk -v n="$1" 'BEGIN { if (n >= 1000) printf "%.1fk", n / 1000; else printf "%d", n }'; }
round() { awk -v x="$1" 'BEGIN { printf "%d", int(x + 0.5) }'; }
# same scale for context and limits: green under 50, yellow under 80, red above
color_for() {
if (( $1 >= 80 )); then printf %s "$RE"; elif (( $1 >= 50 )); then printf %s "$Y"; else printf %s "$G"; fi
}
# repo: origin url from the host onwards (host/group/repo), else the folder name
repo=""
if url=$(git -C "$cwd" --no-optional-locks remote get-url origin 2>/dev/null); then
repo=$(sed -E \
-e 's#^[^@/]+@([^:/]+):(.+)$#\1/\2#; t done' \
-e 's#^[a-z+]+://([^@/]+@)?([^/:]+)(:[0-9]+)?/(.+)$#\2/\4#I; t done' \
-e 's/.*//' -e ':done' -e 's#/+$##; s/\.git$//' <<<"$url")
fi
if [[ -z $repo ]]; then
top=$(git -C "$cwd" --no-optional-locks rev-parse --show-toplevel 2>/dev/null)
repo=$(basename "${top:-${project_dir:-$cwd}}")
fi
# limits, same shape as the context group: "5h 42% (15:36)" and "7d 81% (5d)"
# the 5h window resets within hours so the clock answers "when", the 7d one is
# far enough out that days are all that matters; a rolled-over window shows a dash
now=$(date +%s)
rl_part() {
local label=$1 pct=$2 reset=${3%.*} mode=$4 p s left
[[ -n $pct ]] || return 0
s=$(( ${reset:-0} - now ))
if [[ -z $reset ]] || (( s <= 0 )); then
printf '%s%s —%s' "$D" "$label" "$R"
return 0
fi
p=$(round "$pct")
case $mode in
clock) left=$(date -d "@$reset" +%H:%M) ;;
days) if (( s < 86400 )); then left="$(( (s + 3599) / 3600 ))h"; else left="$(( (s + 43200) / 86400 ))d"; fi ;;
esac
# last known values from the cache render fully dim to say they may be behind
local col; col=$(color_for "$p"); [[ -n $stale ]] && col=$D
printf '%s%s%s %s%s%%%s %s(%s)%s' "$D" "$label" "$R" "$col" "$p" "$R" "$D" "$left" "$R"
}
groups=()
session="${C}${model}${R} ${G}$(fmt "$used")/$(fmt "$total")${R}"
if [[ -n $ctx_pct ]]; then
p=$(round "$ctx_pct")
session+=" ${D}(${R}$(color_for "$p")${p}%${R}${D})${R}"
fi
groups+=("$session")
[[ -n $repo ]] && groups+=("${C}${repo}${R}")
# limits are only sent once the session has talked to the api
part=$(rl_part 5h "$five_pct" "$five_reset" clock); [[ -n $part ]] && groups+=("$part")
part=$(rl_part 7d "$seven_pct" "$seven_reset" days); [[ -n $part ]] && groups+=("$part")
# full session id so it can be pasted into claude --resume, dim and last
[[ -n $session_id ]] && groups+=("${D}${session_id}${R}")
sep=" ${D}│${R} "
out=""
for g in "${groups[@]}"; do out+="${out:+$sep}$g"; done
printf '%s\n' "$out"
# pinned dev links, opt-in per project: .claude/dev-links.json adds a second row
# a value is either "url" or {url, note}, note being a throwaway local credential
link_file=""
for d in "$project_dir" "$cwd"; do
[[ -n $d && -f $d/.claude/dev-links.json ]] && { link_file=$d/.claude/dev-links.json; break; }
done
[[ -n $link_file ]] || exit 0
pins=$(jq -r '
to_entries[]
| { label: .key,
url: (if (.value | type) == "string" then .value
elif (.value | type) == "object" then .value.url else null end),
note: (if (.value | type) == "object" and (.value.note | type) == "string" then .value.note else "" end) }
| select((.url | type) == "string" and (.url | test("^https?://")))
| [.label, .url, .note] | @tsv
' "$link_file" 2>/dev/null) || exit 0
[[ -n $pins ]] || exit 0
# bare urls rather than osc 8: claude code resolves plain http:// on ctrl+click
# itself; COLUMNS is the only readable width since stdout is captured
cols=${COLUMNS:-120}
[[ $cols =~ ^[0-9]+$ ]] || cols=120
gap=3
row="" width=0
while IFS=$'\t' read -r label url note; do
len=$(( ${#label} + 1 + ${#url} + (${#note} > 0 ? ${#note} + 3 : 0) ))
cost=$(( width ? len + gap : len ))
if (( width && width + cost > cols - 4 )); then
printf '%s\n' "$row"
row="" width=$len
else
width=$(( width + cost ))
fi
cell="${C}${label}${R} ${D}${url}${R}"
[[ -n $note ]] && cell+=" ${Y}${note}${R}"
row+="${row:+${D} · ${R}}$cell"
done <<<"$pins"
[[ -n $row ]] && printf '%s\n' "$row"
exit 0

View File

@@ -1,22 +0,0 @@
[main]
font=JetBrainsMono Nerd Font:size=10
prompt=" "
icons=no
terminal=ghostty -e
width=40
lines=12
layer=overlay
exit-on-keyboard-focus-loss=yes
[colors]
background=252423ff
text=d4be98ff
match=a9b665ff
selection=45403dff
selection-text=d4be98ff
selection-match=a9b665ff
border=45403dff
[border]
width=2
radius=8

View File

@@ -1,8 +1,11 @@
theme = light:gruvbox-material-soft-light,dark:gruvbox-material-soft-dark # theme
# theme = light:gruvbox-material-soft-light,dark:gruvbox-material-soft-dark
theme = gruvbox-material-soft-dark
minimum-contrast = 1.1 minimum-contrast = 1.1
# cursor # cursor
cursor-style = block
cursor-style-blink = true cursor-style-blink = true
# mouse # mouse
@@ -22,10 +25,20 @@ window-theme = dark
window-height = 36 window-height = 36
window-width = 150 window-width = 150
# macos send option as alt
macos-option-as-alt = true
# no warning full close # no warning full close
confirm-close-surface = false confirm-close-surface = false
# quit-after-last-window-closed = true # quit-after-last-window-closed = true
# keybinds
keybind = global:cmd+enter=new_window
# shell integrations for zsh
# https://ghostty.org/docs/features/shell-integration
shell-integration = zsh
# gtk # gtk
gtk-titlebar = false gtk-titlebar = false

View File

@@ -1,4 +1,4 @@
palette = 0=#252423 palette = 0=252423
palette = 1=#ea6962 palette = 1=#ea6962
palette = 2=#a9b665 palette = 2=#a9b665
palette = 3=#d8a657 palette = 3=#d8a657

View File

@@ -1,79 +1,56 @@
[includeIf "gitdir:~/Desktop/git/git.aflabs.org/"]
path = ~/.config/git/config.d/aflabs.conf
[includeIf "gitdir:~/Desktop/git/gitlab.com/flarenetwork"]
path = ~/.config/git/config.d/aflabs.conf
[includeIf "gitdir:~/Desktop/git/gitlab.com/flare-foundation"]
path = ~/.config/git/flare
[user] [user]
name = Matej Janežič name = Matej Janežič
email = janezic.mj@gmail.com email = janezic.mj@gmail.com
signingkey = AF349EECC849D87B790E88FF6318FFB7DB374B7D
[commit]
gpgsign = true
verbose = true
; NOTE:(@janezicmatej) github/gitlab don't support these
; [push]
; gpgsign = true
[core] [core]
autocrlf = input autocrlf = input
editor = nvim editor = nvim
[user]
signingkey = AF349EECC849D87B790E88FF6318FFB7DB374B7D
[commit]
gpgsign = true
; NOTE:(@janezicmatej) github/gitlab don't support these
; [push]
; gpgsign = true
[init] [init]
defaultBranch = main defaultBranch = main
[rebase] [alias]
updateRefs = true blame = "!f() { [ -f .git-blame-ignore-revs ] && git blame --ignore-revs-file .git-blame-ignore-revs \"$@\" || git blame \"$@\"; }; f"
[rerere]
enabled = true
autoUpdate = true
[merge]
tool = nvimdiff
conflictStyle = zdiff3
[mergetool]
keepBackup = false
writeToTemp = true
[mergetool "nvimdiff"]
layout = "LOCAL,BASE,REMOTE / MERGED"
[diff] [diff]
tool = nvimdiff tool = "nvimdiff"
algorithm = histogram
colorMoved = plain [difftool]
mnemonicPrefix = true prompt = true
[difftool "nvimdiff"] [difftool "nvimdiff"]
cmd = nvim -d "$LOCAL" "$REMOTE" cmd = nvim -d "$LOCAL" "$REMOTE"
[alias]
blame-ignore = "config blame.ignoreRevsFile .git-blame-ignore-revs"
# re-sign <upstream>..<branch> as the repo identity, keeping trees and merge topology
sign-branch = "!~/.config/bin/git-sign-branch --reset-author"
# list every commit in <range> without a good signature; exits 1 if any, git's status if the range is bad
check-signed = "!f() { log=$(git log --format='%G? %h %s' \"${1:-main..HEAD}\") || exit; bad=$(printf '%s\\n' \"$log\" | grep -v -e '^[GU] ' -e '^$'); [ -z \"$bad\" ] && echo 'all signed' || { printf '%s\\n' \"$bad\"; exit 1; }; }; f"
[branch]
sort = -committerdate
[tag]
sort = version:refname
[help]
autoCorrect = prompt
[maintenance] [maintenance]
auto = true auto = true
strategy = incremental strategy = incremental
[includeIf "gitdir:~/git/git.aflabs.org/"] [merge]
path = ~/.config/git/config.d/aflabs.conf tool = "nvimdiff"
[includeIf "gitdir:~/git/gitlab.com/flarenetwork/"] [mergetool]
path = ~/.config/git/config.d/flare.conf prompt = true
keepBackup = false
writeToTemp = true
[includeIf "gitdir:~/git/gitlab.com/flare-foundation/"] [mergetool "nvimdiff"]
path = ~/.config/git/config.d/flare.conf layout = "LOCAL,BASE,REMOTE / MERGED"
[hook "merge-msg"]
command = ~/.config/git/hooks/merge-msg
event = prepare-commit-msg

View File

@@ -1,2 +0,0 @@
[user]
email = matej.janezic@aflabs.com

View File

@@ -1,2 +0,0 @@
[user]
email = matej@flare.network

View File

@@ -1,22 +0,0 @@
#!/usr/bin/env bash
# rewrite default merge subject to `merge: <branch with dashes as spaces>`
msg_file="$1"
source="$2"
[[ "$source" == "merge" ]] || exit 0
subject=$(head -n1 "$msg_file")
if [[ "$subject" =~ ^Merge\ remote-tracking\ branch\ \'[^/]+/([^\']+)\' ]]; then
branch="${BASH_REMATCH[1]}"
elif [[ "$subject" =~ ^Merge\ branch\ \'([^\']+)\' ]]; then
branch="${BASH_REMATCH[1]}"
else
exit 0
fi
{
printf 'merge: %s\n' "${branch//-/ }"
tail -n +2 "$msg_file"
} > "$msg_file.tmp" && mv "$msg_file.tmp" "$msg_file"

View File

@@ -1,24 +1,20 @@
# os # mac files
.DS_Store .DS_Store
# editors # i don't like pushing .vscode and .idea folders, i will force add if needed
.vscode .vscode
.idea .idea
.ignore
# ai # pyenv
CLAUDE.local.md
.claude
.notes
# languages
.python-version .python-version
# nix # nix venv hook
.nix-venv .nix-venv
.envrc .dev
.direnv/
# project-local # i use .ignore for nvim
.ignore
# todo files
TODO.md TODO.md
dumps

View File

@@ -1,28 +0,0 @@
# gruvbox-material-soft-dark
font=JetBrainsMono Nerd Font 11
background-color=#252423
text-color=#d4be98
border-color=#45403d
border-size=2
border-radius=8
padding=12
width=450
height=300
margin=8
max-visible=3
default-timeout=10000
ignore-timeout=0
group-by=app-name
max-icon-size=32
icon-path=/usr/share/icons/Adwaita
layer=overlay
anchor=top-right
[urgency=low]
default-timeout=5000
[urgency=critical]
default-timeout=0
border-color=#ea6962

View File

@@ -1,55 +1,44 @@
add_newline = false add_newline = false
format = """\ format = """\
$username\
$hostname\ $hostname\
$directory\ [$directory](cyan)\
$git_branch\ $git_branch\
$git_commit\ $git_commit\
$git_state\ $git_state\
$git_status\ $git_status\
$character\
"""
right_format = """\
$nix_shell\ $nix_shell\
$golang\ $golang\
$lua\ $lua\
$nodejs\ $nodejs\
$python\ $python\
$rust\ $rust\
$dotnet\
$c\
$solidity\
$terraform\
$kubernetes\
$helm\
$cmd_duration\ $cmd_duration\
$line_break\
$character\
""" """
[username] [nix_shell]
show_always = true disabled = false
format = '[$user](bold yellow)' format = '[n(\($name\))](bold blue) '
[hostname]
ssh_only = false
format = '[@$hostname](bold yellow) '
[directory]
style = 'bold cyan'
truncation_length = 1
truncate_to_repo = false
fish_style_pwd_dir_length = 1
[character] [character]
success_symbol = '[>](bold green)' success_symbol = '[>](bold green)'
error_symbol = '[>](bold red)' error_symbol = '[>](bold red)'
[git_branch] [directory]
format = 'on [$branch(:$remote_branch)]($style) ' truncation_length = 1
symbol = '' truncate_to_repo = false
fish_style_pwd_dir_length = 1
[git_commit] [git_commit]
tag_symbol = '' tag_symbol = ''
[git_branch]
symbol = ''
[git_status] [git_status]
format = '[$all_status$ahead_behind]($style)' format = '[$all_status$ahead_behind]($style)'
ahead = '[↑${count}](green) ' ahead = '[↑${count}](green) '
@@ -62,47 +51,13 @@ stashed = '[*${count}](green) '
deleted = '[x${count}](red) ' deleted = '[x${count}](red) '
renamed = '[r${count}](yellow) ' renamed = '[r${count}](yellow) '
[nix_shell]
disabled = false
format = '[nix](bold blue) '
[golang]
symbol = 'go-'
[lua]
symbol = 'lua-'
[nodejs]
symbol = 'node-'
[python] [python]
symbol = 'py-' format = 'via [${symbol}${pyenv_prefix}(${version} )(\($virtualenv\) )]($style)'
format = '[${symbol}(${version})]($style) '
python_binary = 'python' python_binary = 'python'
pyenv_version_name = false pyenv_version_name = false
detect_files = [ '.python-version' ] detect_files = [ '.python-version' ]
[rust] [hostname]
symbol = 'rs-' ssh_only = false
style = 'bold yellow'
[c] format = '[$hostname]($style) on '
symbol = 'c-'
[dotnet]
symbol = 'net-'
[solidity]
symbol = 'sol-'
[terraform]
symbol = 'tf-'
[kubernetes]
disabled = false
symbol = 'k8s-'
[helm]
symbol = 'helm-'
[cmd_duration]
format = '[took $duration]($style) '

View File

@@ -1,3 +0,0 @@
include host.d/"$(hostname)".conf
include config.d/*
include /etc/sway/config.d/*

View File

@@ -1,27 +0,0 @@
set $mod Mod4
# vim style
set $left h
set $down j
set $up k
set $right l
# programs
set $term ghostty
set $menu fuzzel
set $browser google-chrome-stable
set $lockscreen swaylock
# other
set $wobs /tmp/wob.sock
# style
set $border 3
# colors
set $bg0 #32302f
set $bg1 #252423
set $bg_overlay #45403d
set $fg #d4be98
set $red #ea6962
set $gray #928374

View File

@@ -1 +0,0 @@
output * bg ~/.assets/wallpaper.png fill

View File

@@ -1,5 +0,0 @@
input "type:keyboard" {
xkb_options "fkeys:basic_13-24"
repeat_delay 150
repeat_rate 35
}

View File

@@ -1,12 +0,0 @@
font pango:JetBrainsMono Nerd Font Bold 10
default_floating_border normal $border
default_border normal $border
titlebar_border_thickness $border
# class border bground text indicator child_border
client.focused $fg $bg0 $fg $fg $fg
client.focused_inactive $bg1 $bg1 $gray $bg1 $bg1
client.unfocused $bg1 $bg1 $gray $bg1 $bg1
client.urgent $red $red $bg0 $red $red
client.placeholder $bg1 $bg1 $fg $bg1 $bg1

View File

@@ -1,8 +0,0 @@
titlebar_separator disable
shadows enable
shadow_blur_radius 25
shadow_color #0000007F
shadow_offset 0 5
blur enable

View File

@@ -1,133 +0,0 @@
# sway
bindsym $mod+Shift+c reload
bindsym $mod+Shift+e exec ~/.config/sway/scripts/power-menu.sh
floating_modifier $mod normal
# programs
bindsym $mod+Return exec $term
bindsym $mod+d exec $menu
bindsym $mod+u exec $lockscreen
bindsym $mod+Shift+Ctrl+Alt+space exec 1password --quick-access
bindsym Print exec ~/.config/sway/scripts/screenshot.sh
# notifications
bindsym $mod+n exec makoctl dismiss
bindsym $mod+Shift+n exec makoctl dismiss --all
# clipboard
bindsym $mod+c exec cliphist list | fuzzel -d | cliphist decode | wl-copy
# output
bindsym $mod+Shift+o exec ~/.config/sway/scripts/output-scale.sh toggle
# window
bindsym $mod+Shift+q kill
# move focus
bindsym $mod+$left focus left
bindsym $mod+$down focus down
bindsym $mod+$up focus up
bindsym $mod+$right focus right
# move focused window
bindsym $mod+Shift+$left move left
bindsym $mod+Shift+$down move down
bindsym $mod+Shift+$up move up
bindsym $mod+Shift+$right move right
# switch to workspace
bindsym $mod+1 workspace number 1
bindsym $mod+2 workspace number 2
bindsym $mod+3 workspace number 3
bindsym $mod+4 workspace number 4
bindsym $mod+5 workspace number 5
bindsym $mod+6 workspace number 6
bindsym $mod+7 workspace number 7
bindsym $mod+8 workspace number 8
bindsym $mod+9 workspace number 9
bindsym $mod+0 workspace number 10
# move to workspace
bindsym $mod+Shift+1 move container to workspace number 1
bindsym $mod+Shift+2 move container to workspace number 2
bindsym $mod+Shift+3 move container to workspace number 3
bindsym $mod+Shift+4 move container to workspace number 4
bindsym $mod+Shift+5 move container to workspace number 5
bindsym $mod+Shift+6 move container to workspace number 6
bindsym $mod+Shift+7 move container to workspace number 7
bindsym $mod+Shift+8 move container to workspace number 8
bindsym $mod+Shift+9 move container to workspace number 9
bindsym $mod+Shift+0 move container to workspace number 10
bindsym $mod+Ctrl+1 exec ~/.config/sway/scripts/ws-to-current.sh 1
bindsym $mod+Ctrl+2 exec ~/.config/sway/scripts/ws-to-current.sh 2
bindsym $mod+Ctrl+3 exec ~/.config/sway/scripts/ws-to-current.sh 3
bindsym $mod+Ctrl+4 exec ~/.config/sway/scripts/ws-to-current.sh 4
bindsym $mod+Ctrl+5 exec ~/.config/sway/scripts/ws-to-current.sh 5
bindsym $mod+Ctrl+6 exec ~/.config/sway/scripts/ws-to-current.sh 6
bindsym $mod+Ctrl+7 exec ~/.config/sway/scripts/ws-to-current.sh 7
bindsym $mod+Ctrl+8 exec ~/.config/sway/scripts/ws-to-current.sh 8
bindsym $mod+Ctrl+9 exec ~/.config/sway/scripts/ws-to-current.sh 9
bindsym $mod+Ctrl+0 exec ~/.config/sway/scripts/ws-to-current.sh 10
# layout
bindsym $mod+b splith
bindsym $mod+v splitv
bindsym $mod+s layout stacking
bindsym $mod+w layout tabbed
bindsym $mod+e layout toggle split
# fullscreen
bindsym $mod+f fullscreen
# focus
bindsym $mod+Shift+space floating toggle; [floating] border pixel $border; [tiling] border normal $border
bindsym $mod+space focus mode_toggle
bindsym $mod+a focus parent
# scratchpad
bindsym $mod+Shift+minus move scratchpad
bindsym $mod+minus scratchpad show
# xf86-volume
bindsym --locked XF86AudioMute exec pamixer -t && ( pamixer --get-mute && echo 0 > $wobs || pamixer --get-volume > $wobs )
bindsym --locked XF86AudioLowerVolume exec pamixer -d 5 && pamixer --get-volume > $wobs
bindsym --locked XF86AudioRaiseVolume exec pamixer -i 5 && pamixer --get-volume > $wobs
bindsym --locked F16 exec pamixer --default-source -t
# xf86-brightness
bindsym --locked XF86MonBrightnessDown exec brightnessctl set 5%-
bindsym --locked XF86MonBrightnessUp exec brightnessctl set 5%+
# xf86-audio
bindsym --locked XF86AudioNext exec playerctl next
bindsym --locked XF86AudioPause exec playerctl play-pause
bindsym --locked XF86AudioPlay exec playerctl play-pause
bindsym --locked XF86AudioPrev exec playerctl previous
# resize
mode "resize" {
# left will shrink the containers width
# right will grow the containers width
# up will shrink the containers height
# down will grow the containers height
bindsym $left resize shrink width 10px
bindsym $down resize grow height 10px
bindsym $up resize shrink height 10px
bindsym $right resize grow width 10px
# Ditto, with arrow keys
bindsym Left resize shrink width 10px
bindsym Down resize grow height 10px
bindsym Up resize shrink height 10px
bindsym Right resize grow width 10px
# Return to default mode
bindsym Return mode "default"
bindsym Escape mode "default"
}
bindsym $mod+r mode "resize"
mode "passthrough" {
bindsym $mod+Shift+Escape mode "default"
}
bindsym $mod+Shift+Escape mode "passthrough"

View File

@@ -1,26 +0,0 @@
# additional theming
for_window [floating] border pixel $border
for_window [tiling] border normal $border
for_window [app_id="com.mitchellh.ghostty" title="fastfetch"] floating enable, focus, move position 50 50
# workspace assignment
# NOTE:(@janezicmatej) app_ids change casing and separators across packaging; matches are case-insensitive substrings
assign [app_id="(?i)google-chrome"] workspace 2
assign [app_id="(?i)thunderbird"] workspace 3
assign [app_id="(?i)cider"] workspace 4
assign [app_id="(?i)boltlauncher"] workspace 5
assign [class="(?i)net-runelite"] workspace 5
assign [app_id="(?i)discord"] workspace 8
assign [app_id="(?i)slack"] workspace 8
assign [app_id="(?i)telegram"] workspace 8
assign [app_id="(?i)rocket.?chat"] workspace 8
# rules
for_window [app_id="com.gabm.satty"] fullscreen enable
for_window [class="net-runelite.*"] floating enable
no_focus [class="net-runelite.*"]
# idle
for_window [app_id=".*"] inhibit_idle fullscreen
for_window [class=".*"] inhibit_idle fullscreen

View File

@@ -1,3 +0,0 @@
bar {
swaybar_command waybar
}

View File

@@ -1,21 +0,0 @@
# daemon
exec mako
exec nm-applet --indicator
exec swayidle -w
exec wlsunset -l 46.1 -L 14.5
exec pkill -x wob; rm -f $wobs && mkfifo $wobs && tail -f $wobs | wob
exec wl-paste --watch cliphist store
exec ~/.config/sway/scripts/power-aware-effects.sh
exec protonmail-bridge -n
# traditional "start when os starts" programs
exec ghostty --title=fastfetch --window-width=105 --window-height=27 -e bash -c 'nix run nixpkgs#fastfetch; read -rsn1' && ghostty
exec $browser
exec thunderbird
exec cider-2
exec bolt-launcher
exec discord
exec slack
exec Telegram
exec rocketchat-desktop
exec 1password --silent

View File

@@ -1,4 +0,0 @@
# bottom-aligned and adjacent to eDP-1 at scale 1
# NOTE:(@janezicmatej) scripts/output-scale.sh recomputes both positions on scale change
output DP-7 resolution 2560x1440 position 2560 160
output eDP-1 resolution 2560x1600@165Hz position 0 0 scale 1 adaptive_sync on

View File

@@ -1,2 +0,0 @@
output DP-2 resolution 2560x1440@179.952Hz position 0 0
output HDMI-A-2 resolution 2560x1440@74.971Hz position 2560 0

View File

@@ -1,16 +0,0 @@
#!/usr/bin/env bash
# toggle swayfx blur and shadows based on battery state
while true; do
status=$(cat /sys/class/power_supply/BAT1/status 2>/dev/null || cat /sys/class/power_supply/BAT0/status)
if [ "$status" = "Discharging" ]; then
swaymsg blur off
swaymsg shadows off
else
swaymsg blur on
swaymsg shadows on
fi
sleep 30
done

View File

@@ -1,13 +0,0 @@
#!/usr/bin/env bash
# power menu via fuzzel
choice=$(printf "lock\nlogout\nsuspend\nreboot\nshutdown" | fuzzel -d -p "power: ")
case "$choice" in
lock) swaylock ;;
logout) swaymsg exit ;;
suspend) systemctl suspend ;;
reboot) systemctl reboot ;;
shutdown) systemctl poweroff ;;
esac

View File

@@ -1,6 +0,0 @@
#!/usr/bin/env bash
# screenshot focused monitor with satty
output=$(swaymsg -t get_outputs | jq -r '.[] | select(.focused) | .name')
grim -o "$output" - | satty -f - --fullscreen --early-exit --copy-command wl-copy

View File

@@ -1,4 +0,0 @@
#!/bin/sh
# switch to workspace and move it to the currently focused output
output=$(swaymsg -t get_outputs | jq -r '.[] | select(.focused) | .name')
swaymsg "workspace number $1, move workspace to output $output"

View File

@@ -1,4 +0,0 @@
lock 'swaylock -f'
timeout 300 'swaylock -f'
timeout 600 'swaymsg "output * power off"' resume 'swaymsg "output * power on"'
before-sleep 'swaylock -f'

View File

@@ -1,35 +0,0 @@
ignore-empty-password
show-failed-attempts
image=~/.assets/wallpaper.png
clock
datestr=%A, %d/%m/%y
indicator-radius=150
indicator-thickness=7
indicator-idle-visible
indicator-y-position=300
indicator-x-position=1200
# gruvbox-material-soft-dark
inside-color=#32302f
text-color=#d4be98
ring-color=#928374
inside-ver-color=#32302f
text-ver-color=#a9b665
ring-ver-color=#a9b665
inside-clear-color=#32302f
text-clear-color=#d8a657
ring-clear-color=#d8a657
inside-wrong-color=#32302f
text-wrong-color=#ea6962
ring-wrong-color=#ea6962
key-hl-color=#7daea3
bs-hl-color=#ea6962
caps-lock-key-hl-color=#d8a657
caps-lock-bs-hl-color=#ea6962

View File

@@ -1,27 +0,0 @@
#!/usr/bin/env bash
if [[ $# -eq 1 ]]; then
selected=$1
else
selected=$(ggman ls \
| fzf \
--border-label "tmux-sessionizer" \
)
fi
if [[ -z $selected ]]; then
exit 0
fi
selected_name="$(echo "$selected" | shasum -a 256 | head -c 4)_$(basename "$selected" | tr . _ | head -c 10)"
if [[ -z $TMUX ]] && ! tmux list-sessions 2>/dev/null; then
tmux new-session -s "$selected_name" -c "$selected"
exit 0
fi
if ! tmux has-session -t="$selected_name" 2>/dev/null; then
tmux new-session -ds "$selected_name" -c "$selected"
fi
tmux switch-client -t "$selected_name"

View File

@@ -1,47 +0,0 @@
#!/usr/bin/env bash
# open (or switch to) a tmux session running ssh to a host
# tmux-ssher [host [luks]]
if [[ $# -ge 1 ]]; then
host=$1
profile=${2:-}
else
IFS=$'\t' read -r host profile < <(~/.config/bin/ssh-menu -s)
fi
if [[ -z $host ]]; then
exit 0
fi
# session names cannot contain . or :
name=${host//[.:]/_}
# an unlock session is short-lived and must not collide with a live ssh session
if [[ $profile == luks ]]; then
session="unlock_$name"
args=(-P luks "$host")
else
session="ssh_$name"
args=("$host")
fi
# on failure keep the window until enter, so the error stays readable (e.g.
# publickey denied because the yubikey is not inserted); tmux.conf's
# detach-on-destroy then returns to the previous session. bash, not the tmux
# default shell, so read -p behaves
cmd="bash -c 'ssh \"\$@\" || { rc=\$?; echo; read -rp \"ssh exited \$rc, press enter to close \"; }' _$(printf ' %q' "${args[@]}")"
if ! tmux has-session -t="$session" 2>/dev/null; then
tmux new-session -ds "$session" -n "$host" "$cmd"
# tmux.conf's c / " / % bindings read this to open more panes on the same host
if [[ $profile != luks ]]; then
tmux set-option -t "=$session:" @ssh_host "$host"
fi
fi
if [[ -z $TMUX ]]; then
tmux attach-session -t "=$session"
else
tmux switch-client -t "=$session"
fi

View File

@@ -1,5 +1,7 @@
#!/usr/bin/env bash #!/usr/bin/env bash
source "$XDG_CONFIG_HOME/zsh/scripts.zsh"
if [[ $# -eq 1 ]]; then if [[ $# -eq 1 ]]; then
selected=$1 selected=$1
else else
@@ -10,7 +12,7 @@ if [[ -z $selected ]]; then
exit 0 exit 0
fi fi
ggman clone "$selected" ggman clone $selected
echo "" echo ""
read -p "press enter to continue..." read -p "press enter to continue..."

View File

@@ -6,6 +6,8 @@ if [[ -z ${NAME:+x} ]]; then
exit 1 exit 1
fi fi
source "$XDG_CONFIG_HOME/zsh/scripts.zsh"
read -p "name: " project_name read -p "name: " project_name
if [[ -z $project_name ]]; then if [[ -z $project_name ]]; then
@@ -17,16 +19,16 @@ if [[ -d "$GGROOT/no-remote/$project_name" ]]; then
fi fi
project="$GGROOT/no-remote/$project_name" project="$GGROOT/no-remote/$project_name"
git init "$project" git init $project
echo "# $project_name" >>"$project/README.md" echo "# $project_name" >>"$project/README.md"
gitignore_template=$( gitignore_template=$(
find "$GGROOT"/github.com/github/gitignore/*.gitignore -exec basename {} \; \ find "$GGROOT"/github.com/github/gitignore/*.gitignore -exec basename {} \; \
| sed 's/\(.*\)\.gitignore/\1/' \ | sed 's/\(.*\)\.gitignore/\1/' \
| fzf | fzf --cycle --bind 'tab:toggle-up,btab:toggle-down'
) )
if [[ -n $gitignore_template ]]; then if ! [[ -z $gitignore_template ]]; then
gitignore="$GGROOT/github.com/github/gitignore/$gitignore_template.gitignore" gitignore="$GGROOT/github.com/github/gitignore/$gitignore_template.gitignore"
echo "$gitignore" echo "$gitignore"
echo "$project/.gitignore" echo "$project/.gitignore"
@@ -36,11 +38,13 @@ fi
license_template=$( license_template=$(
find "$GGROOT"/git.janezic.dev/janezicmatej/license/*.license -exec basename {} \; \ find "$GGROOT"/git.janezic.dev/janezicmatej/license/*.license -exec basename {} \; \
| sed 's/\(.*\)\.license/\1/' \ | sed 's/\(.*\)\.license/\1/' \
| fzf | fzf --cycle --bind 'tab:toggle-up,btab:toggle-down'
) )
if [[ -n $license_template ]]; then if ! [[ -z $license_template ]]; then
license="$GGROOT/git.janezic.dev/janezicmatej/license/$license_template.license" license="$GGROOT/git.janezic.dev/janezicmatej/license/$license_template.license"
export YEAR="$(date '+%Y')" export YEAR="$(date '+%Y')"
envsubst <"$license" >"$project/LICENSE" envsubst <"$license" >"$project/LICENSE"
fi fi
ggcompile

34
tmux/tmux-sessionizer Executable file
View File

@@ -0,0 +1,34 @@
#!/usr/bin/env bash
if [[ $# -eq 1 ]]; then
selected=$1
else
selected=$(ggman ls \
| fzf --cycle \
--bind 'tab:toggle-up,btab:toggle-down' \
--header "Navigate with ARROW KEYS or TAB/S-TAB. Select with ENTER." \
--border "double" \
--border-label "tmux-sessionizer" \
--ansi \
--highlight-line \
)
fi
if [[ -z $selected ]]; then
exit 0
fi
selected_name="$(echo $selected | shasum -a 256 | head -c 4)_$(basename $selected | tr . _ | head -c 10)"
tmux_running=$(pgrep tmux)
if [[ -z $TMUX ]] && [[ -z $tmux_running ]]; then
tmux new-session -s $selected_name -c $selected
exit 0
fi
if ! tmux has-session -t=$selected_name 2> /dev/null; then
tmux new-session -ds $selected_name -c $selected
fi
tmux switch-client -t $selected_name

77
tmux/tmux-ssher Executable file
View File

@@ -0,0 +1,77 @@
#!/usr/bin/env bash
function _preview {
file=$1; shift
host=$1; shift
j=$(cat $file \
| jc --ssh-conf \
| jq -r ".[] \
| select(.host == \"$host\") \
| .jumps = (if (.proxyjump | type == \"array\" and length > 0) then (.proxyjump | join(\",\")) else \"null\" end)" \
)
user=$(jq -r '.user' <<<$j)
port=$(jq -r '.port' <<<$j)
hostname=$(jq -r '.hostname' <<<$j)
jumps=$(jq -r '.jumps' <<<$j)
echo "$(cat <<EOF
User $user
HostName $hostname\
$([[ $port != "null" ]] && echo -e "\nPort $port")\
$([[ $jumps != "null" ]] && echo -e "\nProxyJump $jumps")
command:
ssh $user@$hostname$([[ $port != "null" ]] && echo -n " -p $port")$([[ $jumps != "null" ]] && echo -n " -J $jumps")
EOF)"
echo ""
host $hostname
echo ""
ping -c 1 -W 1 $hostname &>/dev/null \
&& echo "Host is reachable!" \
|| echo "Host is not reachable!"
}
export -f _preview
function main() {
if [[ $# -eq 1 ]]; then
selected=$1
else
selected="$(rg -N --no-heading '^Host .*' ~/.ssh \
| sed 's/Host \(.*\)/\1/' \
| SHELL=$(which bash) fzf --cycle \
--bind 'tab:toggle-up,btab:toggle-down' \
--delimiter ':' \
--with-nth 2 \
--preview "_preview {1} {2}" \
| cut -d ":" -f2
)"
fi
if [[ -z $selected ]]; then
exit 0
fi
selected_name="ssh_$selected"
tmux_running=$(pgrep tmux)
if [[ -z $TMUX ]] && [[ -z $tmux_running ]]; then
tmux new-session -s $selected_name -c $selected ssh "$selected"
exit 0
fi
if ! tmux has-session -t=$selected_name 2> /dev/null; then
tmux new-session -ds $selected_name -c $selected ssh "$selected"
fi
tmux switch-client -t $selected_name
}
main "$@"

View File

@@ -1,67 +1,53 @@
# server options set -g default-terminal "xterm-256color"
set -ga terminal-overrides ",xterm-256color:RGB"
set -ga terminal-features ",xterm-256color:clipboard"
set -g allow-passthrough on
set -g set-clipboard on
set -s escape-time 0 set -s escape-time 0
# terminal
set -g default-terminal "tmux-256color"
set -ga terminal-overrides ",xterm-ghostty:RGB,xterm-256color:RGB"
set -ga terminal-features ",xterm-ghostty:clipboard,xterm-256color:clipboard"
set -g set-clipboard on
set -g allow-passthrough on
# general
set -g prefix C-s
set -g base-index 1
set -g focus-events on
set -g update-environment "SSH_AUTH_SOCK SSH_CONNECTION SSH_TTY DISPLAY" set -g update-environment "SSH_AUTH_SOCK SSH_CONNECTION SSH_TTY DISPLAY"
# when a session dies under the client (e.g. a failed ssh in tmux-ssher) go to
# the most recently used session instead of detaching. not "previous": that
# path crashes the whole server on 3.7b+ (tmux issue 5344, fixed after 3.7c)
set -g detach-on-destroy off
# status bar unbind C-b
set-option -g prefix C-s
bind r source-file "$XDG_CONFIG_HOME/tmux/tmux.conf"
set -g base-index 1
set -g status-style 'bg=default fg=default' set -g status-style 'bg=default fg=default'
set -g status-left-length 20 set -g status-left-length 20
# window options set-window-option -g mode-keys vi
set -wg mode-keys vi
set -g pane-base-index 1
set -g renumber-windows on
# prefix
unbind C-b
# copy mode
bind -T copy-mode-vi v send-keys -X begin-selection bind -T copy-mode-vi v send-keys -X begin-selection
bind -T copy-mode-vi y send-keys -X copy-selection-and-cancel
# navigation # differentiate copy on linux and max
if-shell -b '[ "$(uname -s)" = "Linux" ]' {
bind -T copy-mode-vi y send-keys -X copy-pipe-and-cancel 'xclip -in -selection clipboard'
}
if-shell -b '[ "$(uname -s)" = "Darwin" ]' {
bind -T copy-mode-vi y send-keys -X copy-pipe-and-cancel 'pbcopy'
}
# vim-like pane switching
bind ^ last-window bind ^ last-window
bind k select-pane -U bind k select-pane -U
bind j select-pane -D bind j select-pane -D
bind h select-pane -L bind h select-pane -L
bind l select-pane -R bind l select-pane -R
# windows and panes: inside an ssh session (@ssh_host set by tmux-ssher) new
# panes open on the same host over the multiplexed connection.
# new-window/split-window do not expand formats in their command, run-shell does
bind c if-shell -F "#{@ssh_host}" 'run-shell "tmux new-window -t \"#{session_name}\" -n \"#{@ssh_host}\" \"ssh #{@ssh_host}\""' "new-window"
bind '"' if-shell -F "#{@ssh_host}" 'run-shell "tmux split-window -v -t \"#{pane_id}\" \"ssh #{@ssh_host}\""' "split-window -v"
bind % if-shell -F "#{@ssh_host}" 'run-shell "tmux split-window -h -t \"#{pane_id}\" \"ssh #{@ssh_host}\""' "split-window -h"
# reload
bind r source-file "$XDG_CONFIG_HOME/tmux/tmux.conf"
# sessionizer # sessionizer
bind f run-shell "tmux neww ~/.config/tmux/scripts/tmux-sessionizer" bind-key f run-shell "tmux neww ~/.config/tmux/tmux-sessionizer"
bind R run-shell "tmux neww ~/.config/tmux/scripts/tmux-sessionizer ~" bind-key R run-shell "tmux neww ~/.config/tmux/tmux-sessionizer ~"
bind g run-shell "tmux neww ~/.config/tmux/scripts/tmux-sessionizer ~/.config" bind-key g run-shell "tmux neww ~/.config/tmux/tmux-sessionizer ~/.config"
bind T run-shell "tmux neww ~/.config/tmux/scripts/tmux-sessionizer ~/temp" bind-key T run-shell "tmux neww ~/.config/tmux/tmux-sessionizer ~/temp"
# clone # clone
bind G run-shell "tmux neww ~/.config/tmux/scripts/tmux-ggclone" bind-key G run-shell "tmux neww ~/.config/tmux/tmux-ggclone"
# ssh # ssh
bind F run-shell "tmux neww ~/.config/tmux/scripts/tmux-ssher" bind-key F run-shell "tmux neww ~/.config/tmux/tmux-ssher"
# project # project
bind N run-shell "tmux neww ~/.config/tmux/scripts/tmux-new-project" bind-key N run-shell "tmux neww ~/.config/tmux/tmux-new-project"

View File

@@ -1,142 +0,0 @@
[
{
"layer": "top",
"position": "top",
"height": 30,
"spacing": 1,
"margin": 0,
"modules-left": [
"sway/workspaces",
"sway/mode"
],
"modules-center": [
"custom/ssh",
"systemd-failed-units"
],
"modules-right": [
"mpris",
"cpu",
"memory",
"battery",
"wireplumber",
"custom/mic",
"backlight",
"sway/language",
"bluetooth",
"custom/network",
"clock"
],
"sway/workspaces": {
"disable-scroll": true,
"format": "{name}"
},
"sway/mode": {
"format": "{}"
},
"sway/language": {
"format": "{short}",
"on-click": "swaymsg input type:keyboard xkb_switch_layout next"
},
"custom/ssh": {
"format": "{}",
"return-type": "json",
"interval": 15,
"exec": "~/.config/waybar/scripts/ssh-session.sh",
"on-click": "~/.config/waybar/scripts/ssh-session.sh disconnect"
},
"systemd-failed-units": {
"hide-on-ok": true,
"format": "!{nr_failed}",
"format-ok": "",
"system": true,
"user": true,
"on-click": "ghostty -e sh -c 'systemctl --failed; systemctl --user --failed; read'"
},
"mpris": {
"format": "{artist} - {title}",
"format-paused": "{artist} - {title} [paused]",
"format-stopped": "",
"max-length": 35,
"tooltip-format": "{player}: {artist} - {title} ({album})",
"on-click": "playerctl play-pause",
"on-scroll-up": "playerctl next",
"on-scroll-down": "playerctl previous"
},
"custom/network": {
"format": "{}",
"return-type": "json",
"interval": 5,
"exec": "~/.config/waybar/scripts/network-status.sh"
},
"bluetooth": {
"format": "󰂯",
"format-connected": "󰂯 {num_connections}",
"format-connected-battery": "󰂯 {num_connections}",
"tooltip-format-connected": "{device_enumerate}"
},
"cpu": {
"format": "󰻠 {usage}%",
"tooltip": false
},
"memory": {
"interval": 10,
"format": "󰍛 {percentage}%",
"tooltip-format": "total: {total:0.2f}GiB\nused: {used:0.2f}GiB\navailable: {avail:0.2f}GiB\nswap: {swapUsed:0.2f}/{swapTotal:0.2f}GiB"
},
"battery": {
"states": {
"warning": 30,
"critical": 15
},
"format": "󰁹 {capacity}%",
"format-charging": "󰂄 {capacity}%",
"format-plugged": "󰚥 {capacity}%",
"format-full": "󰁹 full",
"format-alt": "{time}",
"tooltip-format": "{timeTo}\n{power}W",
"on-click": "p=$(powerprofilesctl get); case $p in power-saver) n=balanced;; balanced) n=performance;; *) n=power-saver;; esac; powerprofilesctl set $n"
},
"wireplumber": {
"format": "󰕾 {volume}%",
"format-muted": "󰖁 muted",
"on-click": "pamixer -t",
"on-scroll-up": "pamixer -i 5",
"on-scroll-down": "pamixer -d 5",
"tooltip-format": "{node_name}: {volume}%"
},
"custom/mic": {
"format": "{}",
"return-type": "json",
"interval": 2,
"exec": "~/.config/waybar/scripts/mic-status.sh",
"on-click": "pamixer --default-source -t",
"on-scroll-up": "pamixer --default-source -i 5",
"on-scroll-down": "pamixer --default-source -d 5"
},
"backlight": {
"format": "󰃟 {percent}%",
"tooltip": false
},
"clock": {
"interval": 1,
"format": "{:%d/%m %H:%M:%S}",
"tooltip-format": "<big><tt>{calendar}</tt></big>"
}
}
]

View File

@@ -1,12 +0,0 @@
#!/usr/bin/env bash
# mic volume for waybar custom module
vol=$(pamixer --default-source --get-volume 2>/dev/null || echo 0)
muted=$(pamixer --default-source --get-mute 2>/dev/null || echo false)
if [[ "$muted" == "true" ]]; then
printf '{"text": "󰍭", "class": "muted", "tooltip": "mic muted"}\n'
else
printf '{"text": "󰍬 %d%%", "class": "", "tooltip": "mic %d%%"}\n' "$vol" "$vol"
fi

View File

@@ -1,73 +0,0 @@
#!/usr/bin/env bash
# combined network + vpn status for waybar
# format: icon ip/cidr [vpn-name ...]
iface=$(ip route show default 2>/dev/null | awk '/default/ {print $5; exit}')
icon="󰖪"
class="disconnected"
net_text=""
vpn_names=()
tooltip_parts=()
# network
if [[ -n "$iface" ]]; then
cidr=$(ip -4 addr show "$iface" 2>/dev/null | awk '/inet / {print $2; exit}')
gw=$(ip route show default 2>/dev/null | awk '/default/ {print $3; exit}')
if [[ "$iface" == wl* ]]; then
icon="󰖩"
class="wifi"
ssid=$(nmcli -t -f active,ssid dev wifi 2>/dev/null | awk -F: '/^yes:/ {print $2}')
signal=$(nmcli -t -f active,signal dev wifi 2>/dev/null | awk -F: '/^yes:/ {print $2}')
tooltip_parts+=("$iface: $cidr via $gw")
[[ -n "$ssid" ]] && tooltip_parts+=("ssid: $ssid (${signal}%)")
else
icon="󰈀"
class="wired"
tooltip_parts+=("$iface: $cidr via $gw")
fi
net_text="$cidr"
else
tooltip_parts+=("no connection")
fi
# wireguard — show config name (interface name, e.g. wg-office)
if command -v wg &>/dev/null; then
for wg_iface in $(wg show interfaces 2>/dev/null); do
wg_cidr=$(ip -4 addr show "$wg_iface" 2>/dev/null | awk '/inet / {print $2; exit}')
vpn_names+=("$wg_iface")
tooltip_parts+=("wg: $wg_iface $wg_cidr")
done
fi
# tailscale — show exit node name if routed, otherwise tailnet name
if command -v tailscale &>/dev/null; then
ts_json=$(tailscale status --json 2>/dev/null)
ts_state=$(jq -r '.BackendState // empty' <<<"$ts_json")
if [[ "$ts_state" == "Running" ]]; then
ts_ip=$(jq -r '.TailscaleIPs[0] // empty' <<<"$ts_json")
ts_self=$(jq -r '.Self.HostName // empty' <<<"$ts_json")
ts_exit_id=$(jq -r '.ExitNodeStatus.ID // empty' <<<"$ts_json")
if [[ -n "$ts_exit_id" ]]; then
exit_name=$(jq -r --arg id "$ts_exit_id" '[.Peer[] | select(.ID == $id) | .HostName][0] // "exit"' <<<"$ts_json")
vpn_names+=("$exit_name")
tooltip_parts+=("ts: $ts_self $ts_ip → $exit_name")
else
tooltip_parts+=("ts: $ts_self $ts_ip")
fi
fi
fi
# build text
text="$icon"
[[ -n "$net_text" ]] && text+=" $net_text"
if [[ ${#vpn_names[@]} -gt 0 ]]; then
text+=" [$(IFS=,; echo "${vpn_names[*]}")]"
fi
tooltip=$(printf '%s\n' "${tooltip_parts[@]}")
jq -nc --arg text "$text" --arg class "$class" --arg tooltip "$tooltip" \
'{$text,$class,$tooltip}'

View File

@@ -1,21 +0,0 @@
#!/usr/bin/env bash
# detect remote ssh sessions and optionally disconnect them
# outputs waybar JSON; empty when no remote sessions
if [[ "$1" == "disconnect" ]]; then
pkill -HUP -f 'sshd-session:.*@' 2>/dev/null
exit 0
fi
count=$(pgrep -cf '[s]shd-session:.*@' 2>/dev/null)
count=${count:-0}
if [[ "$count" -gt 0 ]]; then
sessions=$(who 2>/dev/null | awk '$NF ~ /\([0-9]/ {gsub(/[()]/, "", $NF); print $1 "@" $NF}')
label="session"; [[ "$count" -gt 1 ]] && label="sessions"
tooltip=${sessions:-"$count remote $label"}
tooltip=${tooltip//$'\n'/\\n}
tooltip=${tooltip//\"/\\\"}
printf '{"text": "●", "class": "active", "tooltip": "%s"}\n' "$tooltip"
fi

View File

@@ -1,231 +0,0 @@
/* gruvbox-material-soft-dark palette
* mapped from ghostty/themes/gruvbox-material-soft-dark
* ansi 0=#252423 1=#ea6962 2=#a9b665 3=#d8a657
* 4=#7daea3 5=#d3869b 6=#89b482 7=#d4be98
* bg=#32302f fg=#d4be98 selection=#45403d */
@define-color bg #32302f;
@define-color bg_dim #252423;
@define-color bg_sel #45403d;
@define-color fg #d4be98;
@define-color red #ea6962;
@define-color green #a9b665;
@define-color yellow #d8a657;
@define-color blue #7daea3;
@define-color magenta #d3869b;
@define-color cyan #89b482;
@define-color gray #928374;
* {
font-family: "JetBrainsMono Nerd Font";
font-size: 14px;
font-weight: bold;
}
/* bar: dark background, modules float inside */
window#waybar {
background-color: @bg_dim;
color: @fg;
min-height: 30px;
}
window#waybar.hidden {
opacity: 0.2;
}
button {
box-shadow: none;
border: none;
border-radius: 0;
}
button:hover {
background: inherit;
box-shadow: none;
}
/* workspaces */
#workspaces {
background-color: @bg;
border-radius: 8px;
margin: 3px 2px;
padding: 0;
}
#workspaces button {
padding: 0 8px;
background-color: transparent;
color: @gray;
margin: 0;
border-radius: 8px;
}
#workspaces button:hover {
background-color: @bg_sel;
color: @fg;
box-shadow: none;
}
#workspaces button.focused {
background-color: @bg_sel;
color: @fg;
box-shadow: none;
}
#workspaces button.urgent {
background-color: @red;
color: @bg_dim;
}
/* floating pill style for all modules */
#mode,
#clock,
#battery,
#cpu,
#memory,
#backlight,
#wireplumber,
#mpris,
#bluetooth,
#language,
#custom-mic,
#custom-network,
#custom-ssh,
#custom-notification,
#systemd-failed-units {
background-color: @bg;
border-radius: 8px;
color: @fg;
padding: 0 10px;
margin: 3px 2px;
}
/* center: alert zone */
#custom-ssh.active {
color: @cyan;
font-size: 18px;
padding: 0 10px;
margin-top: -2px;
margin-bottom: -2px;
}
#custom-notification.notification {
color: @yellow;
}
#custom-notification.dnd-notification {
color: @red;
}
#custom-notification.dnd-none {
color: @gray;
}
#systemd-failed-units {
color: @red;
}
/* right: media (hidden by default, visible only when playing/paused) */
#mpris {
padding: 0;
margin: 0;
min-width: 0;
background-color: transparent;
color: @green;
}
#mpris.playing {
padding: 0 10px;
margin: 3px 2px;
background-color: @bg;
}
#mpris.paused {
padding: 0 10px;
margin: 3px 2px;
background-color: @bg;
color: @gray;
}
/* right: connectivity */
#custom-network.disconnected {
color: @red;
}
#bluetooth {
color: @blue;
}
/* right: system */
#cpu {
margin-right: 0;
border-radius: 8px 0 0 8px;
}
#memory {
margin-left: 0;
border-radius: 0 8px 8px 0;
}
/* right: battery */
#battery.charging,
#battery.plugged {
color: @green;
}
#battery.warning:not(.charging) {
color: @yellow;
}
@keyframes blink {
to {
background-color: @red;
color: @bg_dim;
}
}
#battery.critical:not(.charging) {
color: @red;
animation-name: blink;
animation-duration: 0.5s;
animation-timing-function: steps(12);
animation-iteration-count: infinite;
animation-direction: alternate;
}
/* right: audio */
#wireplumber.muted {
color: @red;
}
#custom-mic.muted {
color: @red;
}
/* right: audio group (volume + mic + brightness) */
#wireplumber {
margin-right: 0;
border-radius: 8px 0 0 8px;
}
#custom-mic {
margin-left: 0;
margin-right: 0;
border-radius: 0;
}
#backlight {
margin-left: 0;
border-radius: 0 8px 8px 0;
}
/* right: clock */
#clock {
color: @fg;
}
/* mode highlight */
#mode {
color: @bg_dim;
background-color: @yellow;
padding: 0 10px;
}

View File

@@ -1,5 +1,2 @@
path=( export PATH="/usr/local/bin:/opt/homebrew/bin:$HOME/neovim/bin:$HOME/bin:${PATH}"
"$HOME/.local/bin"
"$GOPATH/bin"
$path
)

11
zsh/.zsh_plugins.txt Normal file
View File

@@ -0,0 +1,11 @@
# set up Zsh completions with plugins
# mattmc3/ez-compinit
zsh-users/zsh-completions kind:fpath path:src
# frameworks like oh-my-zsh are supported
getantidote/use-omz # handle OMZ dependencies
ohmyzsh/ohmyzsh path:lib # load OMZ's library
ohmyzsh/ohmyzsh path:plugins/git
# popular fish-like plugins
zsh-users/zsh-autosuggestions

View File

@@ -4,8 +4,22 @@ export XDG_CACHE_HOME="$HOME/.cache"
export XDG_DATA_HOME="$HOME/.local/share" export XDG_DATA_HOME="$HOME/.local/share"
export XDG_STATE_HOME="$HOME/.local/state" export XDG_STATE_HOME="$HOME/.local/state"
# deduplicate path # ~/.local/bin
typeset -U path export PATH="$HOME/.local/bin:$PATH"
# brew sbin
export PATH="/opt/homebrew/sbin:$PATH"
# brew lib
export LIBRARY_PATH="$LIBRARY_PATH:/opt/homebrew/lib"
# cargo
if [[ "$OSTYPE" == "darwin"* ]]; then
source "$HOME/.cargo/env"
fi
# language environment
export LANG=en_US.UTF-8
# editor # editor
export EDITOR=nvim export EDITOR=nvim
@@ -13,27 +27,35 @@ export EDITOR=nvim
# go # go
export GOPATH="$XDG_DATA_HOME/go" export GOPATH="$XDG_DATA_HOME/go"
export GOMODCACHE="$XDG_CACHE_HOME/go/mod" export GOMODCACHE="$XDG_CACHE_HOME/go/mod"
export PATH="$GOPATH/bin:$PATH"
# ggman # ggman
export GGROOT="$HOME/git" export GGROOT="$HOME/git"
# neovim
export PATH="$XDG_DATA_HOME/neovim/bin:$PATH"
# pyenv
export PYENV_ROOT="$XDG_DATA_HOME/.pyenv"
command -v pyenv >/dev/null || export PATH="$PYENV_ROOT/bin:$PATH"
# pyenv-virtualenv
# leave python promt to starship
export PYENV_VIRTUALENV_DISABLE_PROMPT=1
# poetry
export POETRY_HOME="$XDG_DATA_HOME/poetry"
command -v poetry >/dev/null || export PATH="$POETRY_HOME/bin:$PATH"
# gnupg
export GPG_TTY=$(tty)
# aflabs
export USER_UID=$(id -u)
export USER_GID=$(id -g)
# name # name
export NAME="Matej Janežič" export NAME="Matej Janežič"
# starship nest config into a folder, default is ~/.config/starship.toml # starship nest config into a folder, default is ~/.config/startship.toml
export STARSHIP_CONFIG="$XDG_CONFIG_HOME/starship/config.toml" export STARSHIP_CONFIG="$XDG_CONFIG_HOME/starship/config.toml"
# claude
export CLAUDE_CONFIG_DIR="$XDG_CONFIG_HOME/claude"
# codex
export CODEX_HOME="$XDG_CONFIG_HOME/codex"
# cliphist
export CLIPHIST_DB_PATH="/tmp/cliphist-db"
# fzf
export FZF_DEFAULT_OPTS="--cycle --bind 'tab:toggle-up,btab:toggle-down' --pointer '>' --color '16' --border 'double' --ansi --highlight-line --header 'Navigate with ARROW KEYS or TAB/S-TAB. Select with ENTER.'"
# ephvm
export EPHVM_FLAKE="/home/matej/git/git.janezic.dev/janezicmatej/matej.nix"

View File

@@ -1,114 +1,46 @@
# shell options # zsh history file
bindkey -e # emacs line editing export HISTFILE="$XDG_STATE_HOME/zsh/history"
unsetopt autocd # don't cd into directories by name export HISTSIZE=100
setopt NO_BEEP # no terminal bell
setopt AUTO_PUSHD # cd pushes to directory stack
setopt PUSHD_IGNORE_DUPS # no duplicates in directory stack
setopt EXTENDED_GLOB # enable extended glob operators (#, ~, ^)
# history ## pre plugin load
HISTFILE="$XDG_STATE_HOME/zsh/history" # nvm
mkdir -p "${HISTFILE:h}" export NVM_COMPLETION=true
HISTSIZE=10000 # zsh autosugestions
SAVEHIST=10000 export ZSH_AUTOSUGGEST_HIGHLIGHT_STYLE="fg=#ffffff,bg=cyan,bold,underline"
setopt HIST_IGNORE_DUPS # skip consecutive duplicates
setopt HIST_IGNORE_SPACE # skip commands starting with space
setopt SHARE_HISTORY # share history across sessions
setopt HIST_REDUCE_BLANKS # strip extra whitespace
# -- plugins -- # source antidote
# auto-cloned on first launch, auto-updated weekly in background source $ZDOTDIR/.antidote/antidote.zsh
PLUGIN_DIR="$XDG_DATA_HOME/zsh/plugins" antidote load
_plugin_stamp="$XDG_CACHE_HOME/zsh/plugins-updated"
_plugin_needs_update=0
# check if a week (168h) has passed since last update if [[ "$OSTYPE" == "darwin"* ]]; then
if [[ ! -f "$_plugin_stamp" ]] || [[ -n $_plugin_stamp(#qN.mh+168) ]]; then antidote bundle lukechilds/zsh-nvm
mkdir -p "${_plugin_stamp:h}"
touch "$_plugin_stamp"
_plugin_needs_update=1
fi fi
function ensure_plugin() { unsetopt autocd
local repo=$1 dir="$PLUGIN_DIR/${1##*/}"
# clone if missing, pull if update is due
if [[ ! -d "$dir" ]]; then
git clone --depth=1 "https://github.com/$repo.git" "$dir"
elif (( _plugin_needs_update )); then
git -C "$dir" pull --quiet &
fi
fpath=("$dir" $fpath)
source "$dir/${dir##*/}.plugin.zsh" 2>/dev/null
}
ensure_plugin zsh-users/zsh-completions if [[ "$OSTYPE" == "darwin"* ]]; then
ensure_plugin zsh-users/zsh-autosuggestions eval "$(pyenv init - zsh)"
ensure_plugin romkatv/zsh-no-ps2 eval "$(pyenv virtualenv-init - zsh)"
(( _plugin_needs_update )) && wait
ZSH_AUTOSUGGEST_HIGHLIGHT_STYLE="fg=#ffffff,bg=cyan,bold,underline"
# line editing widgets
autoload -Uz up-line-or-beginning-search down-line-or-beginning-search bracketed-paste-magic
zle -N up-line-or-beginning-search
zle -N down-line-or-beginning-search
zle -N bracketed-paste bracketed-paste-magic
# only use builtin widgets during paste to avoid autosuggestions conflict
zstyle ':bracketed-paste-magic' active-widgets '.self-*'
# terminal application mode for correct terminfo sequences
if (( ${+terminfo[smkx]} && ${+terminfo[rmkx]} )); then
autoload -Uz add-zle-hook-widget
function zle_application_mode_start { echoti smkx }
function zle_application_mode_stop { echoti rmkx }
add-zle-hook-widget -Uz zle-line-init zle_application_mode_start
add-zle-hook-widget -Uz zle-line-finish zle_application_mode_stop
fi fi
# key bindings if [[ "$OSTYPE" == "darwin"* ]]; then
[[ -n "${terminfo[kcuu1]}" ]] && bindkey -- "${terminfo[kcuu1]}" up-line-or-beginning-search export SSH_AUTH_SOCK=$(gpgconf --list-dirs agent-ssh-socket)
[[ -n "${terminfo[kcud1]}" ]] && bindkey -- "${terminfo[kcud1]}" down-line-or-beginning-search fi
[[ -n "${terminfo[khome]}" ]] && bindkey -- "${terminfo[khome]}" beginning-of-line
[[ -n "${terminfo[kend]}" ]] && bindkey -- "${terminfo[kend]}" end-of-line
[[ -n "${terminfo[kdch1]}" ]] && bindkey -- "${terminfo[kdch1]}" delete-char
[[ -n "${terminfo[kcbt]}" ]] && bindkey -- "${terminfo[kcbt]}" reverse-menu-complete
bindkey -- '^[[1;5C' forward-word # ctrl+right
bindkey -- '^[[1;5D' backward-word # ctrl+left
# aliases and functions # ggman
eval "$(ggman shellrc)"
# custom functions and aliases
source "$ZDOTDIR/aliases.zsh" source "$ZDOTDIR/aliases.zsh"
source "$ZDOTDIR/scripts.zsh" source "$ZDOTDIR/scripts.zsh"
# completion # completion
fpath=("$XDG_DATA_HOME/zsh/completions" $fpath) fpath=($ZDOTDIR/completions $fpath)
autoload -Uz compinit autoload -Uz compinit
# only regenerate dump once per day compinit -d $XDG_CACHE_HOME/zsh/zcompdump-$ZSH_VERSION
local _zcompdump="$XDG_CACHE_HOME/zsh/zcompdump-$ZSH_VERSION" zstyle ':completion:*' cache-path $XDG_CACHE_HOME/zsh/zcompcache
if [[ -n $_zcompdump(#qN.mh+24) ]]; then
compinit -d "$_zcompdump" # opam configuration
else # [[ ! -r /Users/janezicmatej/.opam/opam-init/init.zsh ]] || source /Users/janezicmatej/.opam/opam-init/init.zsh > /dev/null 2> /dev/null
compinit -C -d "$_zcompdump"
fi
zstyle ':completion:*' cache-path "$XDG_CACHE_HOME/zsh/zcompcache"
zstyle ':completion:*' menu select
zstyle ':completion:*' matcher-list 'm:{a-z}={A-Z}'
# _git picks these up for the matching git aliases (git/config)
_git-sign-branch() {
_arguments \
'(-n --dry-run)'{-n,--dry-run}'[list the commits that would be signed]' \
':branch:__git_branch_names' \
':upstream:__git_references'
}
_git-check-signed() {
_arguments ':commit range:__git_commit_ranges'
}
# tool hooks
eval "$(ggman shellrc)"
eval "$(starship init zsh)" eval "$(starship init zsh)"
eval "$(direnv hook zsh)"
# skip direnv in subshells; completion functions cd inside $(...) and trip chpwd
_direnv_hook_toplevel() { (( ZSH_SUBSHELL )) || _direnv_hook }
chpwd_functions=(${(u)chpwd_functions/#%_direnv_hook/_direnv_hook_toplevel})
# gnupg
export GPG_TTY=$(tty)

View File

@@ -1,78 +1,41 @@
alias z="exec zsh" alias z="exec zsh"
alias t=tmux_attach alias t=tmux_attach
alias e="bash /home/matej/git/git.janezic.dev/janezicmatej/matej.nix/scripts/ephvm-run.sh"
alias sb="/home/matej/git/git.aflabs.org/internal-resources/sandbox-vm/scripts/run.sh"
if [[ "$OSTYPE" == "darwin"* ]]; then
alias n=nvim_ve
alias vim=nvim_ve
elif [[ "$OSTYPE" == "linux"* ]]; then
alias n=nvim alias n=nvim
fi
alias s="~/.config/bin/ssh-menu" alias s="ssh-menu"
# notes across ~/git and ~/.config, see claude/skills/lead/notes.md
alias notes='NOTES_ROOTS="$GGROOT:$HOME/.config" python3 ~/.config/claude/skills/lead/notes.py'
# docker # docker
alias dp="docker ps --format 'table {{.Names}}\t{{.Ports}}'" alias dp="docker ps --format 'table {{.Names}}\t{{.Ports}}'"
alias dcp="docker compose ps --format 'table {{.Name}}\t{{.Ports}}'" alias dcp="docker compose ps --format 'table {{.Name}}\t{{.Ports}}'"
# navigation # navigation
alias cdgit='cd $GGROOT' alias cdgit="cd $GGROOT"
alias icloud="cd $HOME/Library/Mobile\ Documents/com\~apple\~CloudDocs"
alias temp="cd $HOME/Desktop/temp"
# git status/diff # apple specific
alias gst="git status" alias accenton="defaults write -g ApplePressAndHoldEnabled -bool true"
alias gd="git diff" alias accentoff="defaults write -g ApplePressAndHoldEnabled -bool false"
alias gds="git diff --staged"
alias gdup="git diff @{upstream}" # git
alias glog="git log --oneline --decorate --graph" alias gros="git reset origin/$(git_current_branch) --soft"
alias gloga="git log --oneline --decorate --graph --all" alias gcan="git commit -v --amend --no-edit"
alias glogaa="git log --oneline --graph --all --pretty=format:\"%C(auto)%h %C(blue)(%aL/%cL)%C(auto)%(decorate) %s%Creset\"" alias glogaa="git log --oneline --graph --all --pretty=format:\"%C(auto)%h %C(blue)(%aL/%cL)%C(auto)%(decorate) %s%Creset\""
# git staging/commit # unsorted
alias ga="git add" alias ci="glab ci list"
alias gc="git commit -v" alias bm=batman
alias gc!="git commit -v --amend" alias grow="~/.local/bin/cbonsai -liWC -M 15 -t 2 -w 60 -s $(date +%s)"
alias gcan="git commit -v --amend --no-edit" alias clean_ds_store='find . -name ".DS_Store" -type f -delete -print'
alias gcan!="git commit -v --all --no-edit --amend"
alias grs="git restore"
# git branch/checkout
alias gb="git branch"
alias gco="git checkout"
alias gcm='git checkout $(git_main_branch)'
alias gsw="git switch"
# git fetch/pull/push
alias gfa="git fetch --all --prune --tags"
alias gl="git pull"
alias gp="git push"
alias gpf="git push --force-with-lease"
alias gpf!="git push --force"
alias gpo="git push origin"
# git rebase
alias grb="git rebase"
alias grba="git rebase --abort"
alias grbc="git rebase --continue"
alias grbi="git rebase -i"
grbm() { git rebase "$(git_main_branch)" }
# git merge
alias gm="git merge"
alias gma="git merge --abort"
alias gmc="git merge --continue"
# git cherry-pick
alias gcp="git cherry-pick"
alias gcpa="git cherry-pick --abort"
alias gcpc="git cherry-pick --continue"
# git stash/reset
alias gstp="git stash pop"
alias grhh="git reset --hard HEAD"
gros() { git reset "origin/$(git_current_branch)" --soft }
groh() { git reset "origin/$(git_current_branch)" --hard }
# git navigation
alias grt='cd "$(git rev-parse --show-toplevel || echo .)"'
# ruff # ruff
alias ruffme="ruff check --fix && ruff format" alias ruffme="ruff check --fix && ruff format"
# clipboard
alias xcp="xclip -selection c"

0
zsh/completions/.keep Normal file
View File

51
zsh/get-dump Executable file
View File

@@ -0,0 +1,51 @@
#!/usr/bin/env bash
function get-dump {
if [[ -z $1 ]]; then
host="$(rg -N --no-heading 'Host .*' ~/.ssh \
| sed 's/Host \(.*\)/\1/' \
| fzf --cycle \
--bind 'tab:toggle-up,btab:toggle-down' \
--delimiter ':' \
--with-nth 2 \
--header "Navigate with ARROW KEYS or TAB/S-TAB. Select with ENTER." \
--border "double" \
--border-label "get-dump" \
--ansi \
--highlight-line \
| cut -d ":" -f2
)"
if [[ -z $host ]]; then
return
fi
else
host=$1
fi
selected=$(ssh "$host" docker ps --format "{{.Names}}" \
| fzf --cycle \
--bind 'tab:toggle-up,btab:toggle-down' \
--header "Navigate with ARROW KEYS or TAB/S-TAB. Select with ENTER." \
--border "double" \
--border-label "get-dump" \
--ansi \
--highlight-line \
)
if [[ -z $selected ]]; then
echo "not provided"
return
fi
if ! [[ -d "./dumps" ]]; then
mkdir "./dumps"
fi
location="./dumps/${host}_${selected}_$(date +'%s')"
echo "dumping to $location"
ssh "$host" docker exec "$selected" pg_dump -U db --format=c db >"$location"
}
get-dump "$@"

View File

@@ -1,32 +1,94 @@
# git helpers for aliases function nvim_ve {
function git_current_branch { if [[ -e $VIRTUAL_ENV && -f "$VIRTUAL_ENV/bin/activate" ]]; then
command git rev-parse --abbrev-ref HEAD 2>/dev/null source "$VIRTUAL_ENV/bin/activate"
command nvim $@
deactivate
else
command nvim $@
fi
} }
function git_main_branch { function afm {
command git rev-parse --git-dir &>/dev/null || return RES=$(curl -s "https://$1/api/monitoring/requirements")
local ref COUNT=$(echo "$RES" | grep -c $2)
for ref in refs/{heads,remotes/{origin,upstream}}/{main,master,trunk}; do if [[ $COUNT -ge 0 ]]; then
if command git show-ref -q --verify "$ref"; then VER=$(echo "$RES" | jq '.[] | select(.name | contains("'"$2"'")).version')
echo ${ref:t} # OP=$(echo "$RES" | jq '.[] | select(.name | contains("'"$2"'")).requirement?[0][0]')
return 0 # echo "$2""$(echo "$OP" | tr -d '"')""$(echo "$VER" | tr -d '"')"
echo "${2}==$(echo "$VER" | tr -d '"')"
fi fi
done
echo main
} }
function nukepip {
pip uninstall $(pip freeze) -y
pip install -r $(pyenv root)/default-packages
}
function pyinit {
if [[ -f .python-version ]]; then
echo "found .python-version, stopping"
return
fi
DIRNAME=$(basename "$PWD")
if [[ $(pyenv versions | grep -E "/${DIRNAME}$") ]]; then
echo "found existing version with this name, setting..."
pyenv local "$DIRNAME"
return
fi
if [[ -z $1 ]]; then
echo "no python version provided, defaulting to $(pyenv version-name)"
VERSION=$(pyenv version-name)
else
VERSION=$1
fi
pyenv virtualenv "$VERSION" "$DIRNAME"
pyenv local "$DIRNAME"
}
function lh {
if [[ -z $1 ]]; then
PORT=7999
else
PORT=$1
fi
open "http://localhost:$PORT"
}
function dps {
if [[ -z $1 ]]; then
docker ps --format table'{{ .ID }}\t{{ .Image }}\t{{ .Ports}}\t{{ .Names }}'
else
docker ps --format table'{{ .ID }}\t{{ .Image }}\t{{ .Ports}}\t{{ .Names }}' | grep $1
fi
}
function tssh {
if [[ -z $1 ]]; then
echo "provide ssh host"
return
fi
ssh -t "$@" "command -v tmux && (tmux a || tmux new-session -s gorazd -c ~) || bash"
}
function tmux_attach { function tmux_attach {
local tmux_running
tmux_running=$(pgrep tmux) tmux_running=$(pgrep tmux)
if [[ -n ${TMUX} ]]; then if ! [[ -z ${TMUX} ]]; then
echo "already attached; refreshing env" echo "already attached; refreshing env"
source <(tmux show-environment | sed -n 's/^\(.*\)=\(.*\)$/export \1="\2"/p') source <(tmux show-environment | sed -n 's/^\(.*\)=\(.*\)$/export \1="\2"/p')
return 0 return 1
fi fi
if [[ -z $tmux_running ]]; then if [[ -z $tmux_running ]]; then
~/.config/tmux/scripts/tmux-sessionizer ~ ~/.config/tmux/tmux-sessionizer ~
else else
tmux a tmux a
fi fi

75
zsh/ssh-menu Executable file
View File

@@ -0,0 +1,75 @@
#!/usr/bin/env bash
function _ssh_menu_preview {
file=$1; shift
host=$1; shift
j=$(cat $file \
| jc --ssh-conf \
| jq -r ".[] \
| select(.host == \"$host\") \
| .jumps = (\
if (.proxyjump | type == \"array\" and length > 0) then \
(.proxyjump | join(\",\")) \
else \
\"null\" \
end \
)" \
)
user=$(jq -r '.user' <<<$j)
port=$(jq -r '.port' <<<$j)
hostname=$(jq -r '.hostname' <<<$j)
jumps=$(jq -r '.jumps' <<<$j)
echo "$(cat <<EOF
User $user
HostName $hostname\
$([[ $port != "null" ]] && echo -e "\nPort $port")\
$([[ $jumps != "null" ]] && echo -e "\nProxyJump $jumps")
command:
ssh $user@$hostname$([[ $port != "null" ]] && echo -n " -p $port")$([[ $jumps != "null" ]] && echo -n " -J $jumps")
EOF
)"
echo ""
if command -v host 2>&1 >/dev/null; then
host $hostname
echo ""
fi
ping -c 1 -W 1 $hostname &>/dev/null \
&& echo "Host is reachable!" \
|| echo "Host is not reachable!"
}
export -f _ssh_menu_preview
function ssh-menu {
selected="$(rg -N --no-heading '^Host .*' ~/.ssh \
| sed 's/Host \(.*\)/\1/' | sort \
| SHELL=$(which bash) fzf --cycle \
--bind 'tab:toggle-up,btab:toggle-down' \
--delimiter ':' \
--with-nth 2 \
--preview "_ssh_menu_preview {1} {2}" \
--preview-label "ssh config info" \
--header "Navigate with ARROW KEYS or TAB/S-TAB. Select with ENTER." \
--border "double" \
--border-label "ssh-menu" \
--ansi \
--highlight-line \
| cut -d ":" -f2
)"
if [[ -z $selected ]]; then
return
fi
ssh "$selected"
}
ssh-menu