commit c8c8c9a000b4d81d050fc27a41f0fdc14ea2aa70 Author: Prad Nukala Date: Wed Oct 14 13:32:59 2026 -0400 Import from root chezmoi source diff --git a/cable/.keep b/cable/.keep new file mode 100644 index 0000000..e69de29 diff --git a/cable/agents.toml b/cable/agents.toml new file mode 100644 index 0000000..0be8893 --- /dev/null +++ b/cable/agents.toml @@ -0,0 +1,67 @@ +[metadata] +name = "agents" +description = """ +AI coding agent CLIs found on PATH. Enter starts the agent's TUI in the current +directory. +""" +requirements = [] + +# Entries are "\t\t". Only agents +# whose binary resolves on PATH are listed; add a row to the table to support +# another one. The launch command may carry flags (claude, as in `ai`). +[source] +command = ''' +while IFS='|' read -r bin cmd desc; do + command -v "$bin" >/dev/null 2>&1 && + printf '%-13s %s\t%s\t%s\n' "$bin" "$desc" "$cmd" "$bin" +done <<'EOF' +omp|omp|oh-my-pi coding agent +claude|claude --dangerously-skip-permissions|Claude Code (Anthropic) +codex|codex|Codex CLI (OpenAI) +opencode|opencode|OpenCode (sst) +crush|crush|Crush (Charm) +pi|pi|pi coding agent +cursor-agent|cursor-agent|Cursor Agent CLI +copilot|copilot|GitHub Copilot CLI +gemini|gemini|Gemini CLI (Google) +agy|agy|Antigravity CLI (Google) +grok|grok|Grok CLI (xAI) +muse|muse|Muse Code (Meta) +ori|ori|Ori (OpenRouter) +hermes|hermes|Hermes Agent (Nous Research) +amp|amp|Amp (Sourcegraph) +droid|droid|Droid (Factory) +qwen|qwen|Qwen Code (Alibaba) +kimi|kimi|Kimi CLI (Moonshot) +goose|goose|goose (Block) +aider|aider|aider +auggie|auggie|Auggie (Augment Code) +kiro-cli|kiro-cli|Kiro CLI (AWS) +cn|cn|Continue CLI +fractal|fractal|fractal +EOF +''' +shell = "bash" +display = "{split:\\t:0}" +output = "{split:\\t:1}" + +[preview] +command = ''' +p=$(command -v '{split:\t:2}') +printf 'command %s\npath %s\n' '{split:\t:1}' "$p" +r=$(readlink -f "$p") +[ "$r" != "$p" ] && printf 'resolves %s\n' "$r" +# ~/.local/bin wrappers install the agent through mise on first run. +tool=$(grep -o 'mise x "[^"]*"' "$r" 2>/dev/null | cut -d'"' -f2) +[ -n "$tool" ] && printf 'mise %s\n' "$tool" +''' +shell = "bash" + +[keybindings] +enter = "actions:launch" + +[actions.launch] +description = "Start the agent's TUI" +command = "{split:\\t:1}" +shell = "bash" +mode = "execute" diff --git a/cable/alias.toml b/cable/alias.toml new file mode 100644 index 0000000..62862cb --- /dev/null +++ b/cable/alias.toml @@ -0,0 +1,17 @@ +[metadata] +name = "alias" +description = "A channel to select from shell aliases" + +[source] +# you may need to adjust the command depending on your shell +command = "$SHELL -ic 'alias'" +output = "{split:=:0}" + +[preview] +command = "$SHELL -ic 'alias' | grep -E '^(alias )?{split:=:0}='" + +[ui.preview_panel] +border_type = "none" +scrollbar = false +padding = { left = 2, right = 1, top = 0, bottom = 0 } +size = 30 diff --git a/cable/bash-history.toml b/cable/bash-history.toml new file mode 100644 index 0000000..8dc8d58 --- /dev/null +++ b/cable/bash-history.toml @@ -0,0 +1,9 @@ +[metadata] +name = "bash-history" +description = "A channel to select from your bash history" +requirements = ["bash"] + +[source] +command = "sed '1!G;h;$!d' ${HISTFILE:-${HOME}/.bash_history} | awk '!seen[$0]++'" +no_sort = true +frecency = false diff --git a/cable/brew-packages.toml b/cable/brew-packages.toml new file mode 100644 index 0000000..548f5f4 --- /dev/null +++ b/cable/brew-packages.toml @@ -0,0 +1,30 @@ +[metadata] +name = "brew-packages" +description = "List and manage Homebrew packages" +requirements = ["brew"] + +[source] +command = [ + { name = "Formulae", run = "brew list --formula" }, + { name = "Casks", run = "brew list --cask" }, +] + +[preview] +command = "brew info '{}'" + +[ui] +layout = "portrait" + +[keybindings] +ctrl-u = "actions:upgrade" +ctrl-d = "actions:uninstall" + +[actions.upgrade] +description = "Upgrade the selected package" +command = "brew upgrade {}" +mode = "execute" + +[actions.uninstall] +description = "Uninstall the selected package" +command = "brew uninstall {}" +mode = "execute" diff --git a/cable/cargo-commands.toml b/cable/cargo-commands.toml new file mode 100644 index 0000000..fde3b8d --- /dev/null +++ b/cable/cargo-commands.toml @@ -0,0 +1,22 @@ +[metadata] +name = "cargo-commands" +description = "List available cargo commands and extensions" +requirements = ["cargo"] + +[source] +command = "cargo --list 2>/dev/null | tail -n +2 | awk '{print $1}'" + +[preview] +command = "cargo {} --help 2>/dev/null | head -50" + +[ui] +layout = "portrait" +preview_panel = { size = 60, border_type = "none", scrollbar = false, padding = { left = 2, right = 1, top = 0, bottom = 0 } } + +[keybindings] +enter = "actions:run" + +[actions.run] +description = "Run the selected cargo command" +command = "cargo {}" +mode = "execute" diff --git a/cable/cargo-crates.toml b/cable/cargo-crates.toml new file mode 100644 index 0000000..99302d6 --- /dev/null +++ b/cable/cargo-crates.toml @@ -0,0 +1,17 @@ +[metadata] +name = "cargo-crates" +description = "List installed cargo crates" +requirements = ["cargo"] + +[source] +command = "cargo install --list 2>/dev/null | grep -v '^ ' | sed 's/:$//'" +display = "{split: :0} {split: :1}" +output = "{split: :0}" + +[preview] +command = "cargo install --list 2>/dev/null | sed -n '/^{split: :0} /,/^[^ ]/p' | head -20" + +[actions.uninstall] +description = "Uninstall the selected crate" +command = "cargo uninstall '{split: :0}'" +mode = "execute" diff --git a/cable/channels.toml b/cable/channels.toml new file mode 100644 index 0000000..6ad4c69 --- /dev/null +++ b/cable/channels.toml @@ -0,0 +1,19 @@ +[metadata] +name = "channels" +description = "Select a television channel" +requirements = ["tv", "bat"] + +[source] +command = ["tv list-channels"] + +[preview] +command = "bat -pn --color always ${TELEVISION_CONFIG:-${XDG_CONFIG_HOME:-$HOME/.config}/television}/cable/{}.toml" +shell = "bash" + +[keybindings] +enter = "actions:channel-enter" + +[actions.channel-enter] +description = "Enter a television channel" +command = "tv {} --inline" +mode = "execute" diff --git a/cable/crontab.toml b/cable/crontab.toml new file mode 100644 index 0000000..0d3c4d4 --- /dev/null +++ b/cable/crontab.toml @@ -0,0 +1,20 @@ +[metadata] +name = "crontab" +description = "List and manage crontab entries" + +[source] +command = "crontab -l 2>/dev/null | grep -v '^#' | grep -v '^$'" +no_sort = true +frecency = false + +[preview] +command = '''echo 'Schedule: {split: :..5}' && echo '' && echo 'Command:' && echo '{split: :5..}' ''' + +[ui] +layout = "portrait" +preview_panel = { size = 30, border_type = "none", scrollbar = false, padding = { left = 2, right = 1, top = 0, bottom = 0 } } + +[actions.edit] +description = "Open crontab for editing" +command = "crontab -e" +mode = "execute" diff --git a/cable/dirs.toml b/cable/dirs.toml new file mode 100644 index 0000000..515d000 --- /dev/null +++ b/cable/dirs.toml @@ -0,0 +1,26 @@ +[metadata] +name = "dirs" +description = "A channel to select from directories" +requirements = ["fd"] + +[source] +command = [ + { name = "Default", run = "fd -t d" }, + { name = "Hidden", run = "fd -t d --hidden" }, +] + +[preview] +command = "ls -la --color=always '{}'" + +[keybindings] +shortcut = "f2" + +[actions.cd] +description = "Open a shell in the selected directory" +command = "cd {} && $SHELL" +mode = "execute" + +[actions.goto_parent_dir] +description = "Re-opens tv in the parent directory" +command = "tv dirs .. --inline" +mode = "execute" diff --git a/cable/docker-compose.toml b/cable/docker-compose.toml new file mode 100644 index 0000000..2379dcc --- /dev/null +++ b/cable/docker-compose.toml @@ -0,0 +1,32 @@ +[metadata] +name = "docker-compose" +description = "Manage Docker Compose services" +requirements = ["docker"] + +[source] +command = "docker compose ps --format '{{.Name}}\t{{.Service}}\t{{.Status}}'" +display = "{split:\t:1} ({split:\t:2})" +output = "{split:\t:1}" + +[preview] +command = "docker compose logs --tail=30 --no-log-prefix '{split:\t:1}'" + +[actions.up] +description = "Start the selected service" +command = "docker compose up -d '{split:\t:1}'" +mode = "fork" + +[actions.down] +description = "Stop and remove the selected service" +command = "docker compose down '{split:\t:1}'" +mode = "fork" + +[actions.restart] +description = "Restart the selected service" +command = "docker compose restart '{split:\t:1}'" +mode = "fork" + +[actions.logs] +description = "Follow logs of the selected service" +command = "docker compose logs -f '{split:\t:1}'" +mode = "execute" diff --git a/cable/docker-containers.toml b/cable/docker-containers.toml new file mode 100644 index 0000000..a5f2953 --- /dev/null +++ b/cable/docker-containers.toml @@ -0,0 +1,56 @@ +[metadata] +name = "docker-containers" +description = "List and manage Docker containers" +requirements = ["docker", "jq"] + +[source] +command = [ + { name = "Running", run = "docker ps --format '{{.Names}}\\t{{.Image}}\\t{{.Status}}'" }, + { name = "All", run = "docker ps -a --format '{{.Names}}\\t{{.Image}}\\t{{.Status}}'" }, +] +display = "{split:\\t:0} ({split:\\t:2})" +output = "{split:\\t:0}" + +[preview] +command = "docker inspect '{split:\\t:0}' | jq -C '.[0] | {Name, State, Config: {Image: .Config.Image, Cmd: .Config.Cmd}, NetworkSettings: {IPAddress: .NetworkSettings.IPAddress}}'" + +[ui] +layout = "portrait" + +[keybindings] +ctrl-s = "actions:start" +f2 = "actions:stop" +ctrl-r = "actions:restart" +ctrl-l = "actions:logs" +ctrl-e = "actions:exec" +ctrl-d = "actions:remove" + +[actions.start] +description = "Start the selected container" +command = "docker start '{split:\\t:0}'" +mode = "fork" + +[actions.stop] +description = "Stop the selected container" +command = "docker stop '{split:\\t:0}'" +mode = "fork" + +[actions.restart] +description = "Restart the selected container" +command = "docker restart '{split:\\t:0}'" +mode = "fork" + +[actions.logs] +description = "Follow logs of the selected container" +command = "docker logs -f '{split:\\t:0}'" +mode = "execute" + +[actions.exec] +description = "Execute shell in the selected container" +command = "docker exec -it '{split:\\t:0}' /bin/sh" +mode = "execute" + +[actions.remove] +description = "Remove the selected container" +command = "docker rm '{split:\\t:0}'" +mode = "execute" diff --git a/cable/docker-images.toml b/cable/docker-images.toml new file mode 100644 index 0000000..a7948ab --- /dev/null +++ b/cable/docker-images.toml @@ -0,0 +1,34 @@ +[metadata] +name = "docker-images" +description = "A channel to select from Docker images" +requirements = ["docker", "jq"] + +[source] +command = "docker images --format '{{.Repository}}:{{.Tag}} {{.ID}}'" +output = "{split: :-1}" + +[preview] +command = "docker image inspect '{split: :-1}' | jq -C" + +[ui] +layout = "portrait" + +[keybindings] +ctrl-r = "actions:run" +ctrl-d = "actions:remove" +ctrl-s = "actions:shell" + +[actions.run] +description = "Run a container from the selected image" +command = "docker run -it '{split: :-1}'" +mode = "execute" + +[actions.shell] +description = "Run a shell in the selected image" +command = "docker run -it '{split: :-1}' /bin/sh" +mode = "execute" + +[actions.remove] +description = "Remove the selected image" +command = "docker rmi '{split: :-1}'" +mode = "execute" diff --git a/cable/docker-networks.toml b/cable/docker-networks.toml new file mode 100644 index 0000000..b57b2c5 --- /dev/null +++ b/cable/docker-networks.toml @@ -0,0 +1,20 @@ +[metadata] +name = "docker-networks" +description = "List and manage Docker networks" +requirements = ["docker", "jq"] + +[source] +command = "docker network ls --format '{{.Name}}\t{{.Driver}}\t{{.Scope}}'" +display = "{split:\t:0} ({split:\t:1}, {split:\t:2})" +output = "{split:\t:0}" + +[preview] +command = "docker network inspect '{split:\t:0}' | jq -C '.[0] | {Name, Driver, Scope, IPAM, Containers: (.Containers // {} | to_entries | map({name: .value.Name, ipv4: .value.IPv4Address}))}'" + +[ui] +layout = "portrait" + +[actions.remove] +description = "Remove the selected network" +command = "docker network rm '{split:\t:0}'" +mode = "execute" diff --git a/cable/docker-volumes.toml b/cable/docker-volumes.toml new file mode 100644 index 0000000..03b8e6f --- /dev/null +++ b/cable/docker-volumes.toml @@ -0,0 +1,25 @@ +[metadata] +name = "docker-volumes" +description = "List and manage Docker volumes" +requirements = ["docker", "jq"] + +[source] +command = "docker volume ls --format '{{.Name}}\t{{.Driver}}'" +display = "{split:\t:0} ({split:\t:1})" +output = "{split:\t:0}" + +[preview] +command = "docker volume inspect '{split:\t:0}' | jq -C '.[0]'" + +[ui] +layout = "portrait" + +[actions.remove] +description = "Remove the selected volume" +command = "docker volume rm '{split:\t:0}'" +mode = "execute" + +[actions.inspect] +description = "Inspect the selected volume in a pager" +command = "docker volume inspect '{split:\t:0}' | jq -C '.[0]' | less -R" +mode = "execute" diff --git a/cable/dotfiles.toml b/cable/dotfiles.toml new file mode 100644 index 0000000..10d21de --- /dev/null +++ b/cable/dotfiles.toml @@ -0,0 +1,19 @@ +[metadata] +name = "dotfiles" +description = "A channel to select from your user's dotfiles" +requirements = ["fd", "bat"] + +[source] +command = "fd -t f . $HOME/.config" + +[preview] +command = "bat -n --color=always '{}'" + +[keybindings] +enter = "actions:edit" + +[actions.edit] +description = "Edit the selected dotfile" +command = "${EDITOR:-vim} {}" +shell = "bash" +mode = "execute" diff --git a/cable/downloads.toml b/cable/downloads.toml new file mode 100644 index 0000000..939e218 --- /dev/null +++ b/cable/downloads.toml @@ -0,0 +1,31 @@ +[metadata] +name = "downloads" +description = "Browse recent files in Downloads folder" +requirements = ["fd", "bat"] + +[source] +command = "fd -t f . ~/Downloads 2>/dev/null | head -200" + +[preview] +command = "bat -n --color=always '{}' 2>/dev/null || file '{}'" +env = { BAT_THEME = "ansi" } + +[keybindings] +enter = "actions:open" +ctrl-d = "actions:delete" +ctrl-m = "actions:move" + +[actions.open] +description = "Open the selected file with default application" +command = "xdg-open {} 2>/dev/null || open {}" +mode = "fork" + +[actions.delete] +description = "Delete the selected file" +command = "rm -i {}" +mode = "execute" + +[actions.move] +description = "Move the selected file to current directory" +command = "mv {} ." +mode = "fork" diff --git a/cable/env.toml b/cable/env.toml new file mode 100644 index 0000000..c654b2f --- /dev/null +++ b/cable/env.toml @@ -0,0 +1,22 @@ +[metadata] +name = "env" +description = "A channel to select from environment variables" + +[source] +command = "printenv" +output = "{split:=:1..}" # output the value + +[preview] +command = "echo '{split:=:1..}'" + +[ui] +layout = "portrait" +preview_panel = { size = 20, header = "{split:=:0}", border_type = "none", scrollbar = false, padding = { left = 2, right = 1, top = 0, bottom = 0 } } + +[keybindings] +shortcut = "f3" + +[actions.name] +description = "Output the variable name instead of the value" +command = "echo '{split:=:0}'" +mode = "execute" diff --git a/cable/files.toml b/cable/files.toml new file mode 100644 index 0000000..35c0d27 --- /dev/null +++ b/cable/files.toml @@ -0,0 +1,33 @@ +[metadata] +name = "files" +description = "A channel to select files and directories" +requirements = ["fd", "bat", "mtime-sort"] + +# Most recently modified first; tv ranks matches and frecency on top. +[source] +command = [ + { name = "Default", run = "fd -t f | mtime-sort" }, + { name = "Hidden", run = "fd -t f -H | mtime-sort" }, +] +shell = "bash" + +[preview] +command = "bat -n --color=always '{}'" +env = { BAT_THEME = "ansi" } + +# ctrl-e (edit) and ctrl-h (goto_parent_dir) are bound in config.toml. +[keybindings] +shortcut = "f1" +[actions.edit] +description = "Opens the selected entries in nvim" +# Redirect to the tty: the fish ctrl-t binding runs tv inside a command +# substitution, so stdout is a pipe there. +command = "nvim {} /dev/tty" +shell = "bash" +# use `mode = "fork"` if you want to return to tv afterwards +mode = "execute" + +[actions.goto_parent_dir] +description = "Re-opens tv in the parent directory" +command = "tv files .. --inline" +mode = "execute" diff --git a/cable/fish-history.toml b/cable/fish-history.toml new file mode 100644 index 0000000..792ee02 --- /dev/null +++ b/cable/fish-history.toml @@ -0,0 +1,9 @@ +[metadata] +name = "fish-history" +description = "A channel to select from your fish history" +requirements = ["fish"] + +[source] +command = "fish -c 'history'" +no_sort = true +frecency = false diff --git a/cable/fonts.toml b/cable/fonts.toml new file mode 100644 index 0000000..561c91d --- /dev/null +++ b/cable/fonts.toml @@ -0,0 +1,16 @@ +[metadata] +name = "fonts" +description = "List installed system fonts" +requirements = ["fc-list"] + +[source] +command = "fc-list --format='%{family}\n' | sort -uf" + +[preview] +command = "fc-list '{}' --format='%{file}\n%{style}\n%{family}\n' | head -20" + +[ui.preview_panel] +border_type = "none" +scrollbar = false +padding = { left = 2, right = 1, top = 0, bottom = 0 } +size = 70 diff --git a/cable/gh-issues.toml b/cable/gh-issues.toml new file mode 100644 index 0000000..cae57f6 --- /dev/null +++ b/cable/gh-issues.toml @@ -0,0 +1,47 @@ +[metadata] +name = "gh-issues" +description = "List GitHub issues for the current repo" +requirements = ["gh", "jq"] + +[source] +command = '''gh issue list --state open --limit 100 --json number,title,createdAt,author,labels | jq -r 'sort_by(.createdAt) | reverse | .[] | " \u001b[32m#\(.number)\u001b[39m \(.title) \u001b[33m@\(.author.login)\u001b[39m" + (if (.labels | length) > 0 then " " + ([.labels[] | "\u001b[35m" + .name + "\u001b[39m"] | join(" ")) else "" end)' +''' +ansi = true +output = "{strip_ansi|split:#:1|split: :0}" + +[ui] +layout = "portrait" +preview_panel = { header = '{strip_ansi|split:#:1|split: :0}', border_type = "none", scrollbar = false, padding = { left = 2, right = 1, top = 0, bottom = 0 } } + +[preview] +command = '''gh issue view '{strip_ansi|split:#:1|split: :0}' --json number,title,body,state,author,createdAt,updatedAt,labels,assignees | jq -r ' +" " + .title, +" #" + (.number | tostring), +"", +" \u001b[36mStatus:\u001b[39m \u001b[32m" + .state + "\u001b[39m", +" \u001b[36mAuthor:\u001b[39m \u001b[33m" + .author.login + "\u001b[39m", +" \u001b[36mCreated:\u001b[39m " + (.createdAt | fromdateiso8601 | (now - .) | if . < 3600 then (./60|floor|tostring) + " minutes ago" elif . < 86400 then (./3600|floor|tostring) + " hours ago" else (./86400|floor|tostring) + " days ago" end), +" \u001b[36mUpdated:\u001b[39m " + (.updatedAt | fromdateiso8601 | (now - .) | if . < 3600 then (./60|floor|tostring) + " minutes ago" elif . < 86400 then (./3600|floor|tostring) + " hours ago" else (./86400|floor|tostring) + " days ago" end), +(if (.labels | length) > 0 then " \u001b[36mLabels:\u001b[39m " + ([.labels[] | "\u001b[35m" + .name + "\u001b[39m"] | join(" ")) else "" end), +(if (.assignees | length) > 0 then " \u001b[36mAssignees:\u001b[39m " + ([.assignees[].login] | join(", ")) else "" end), +"", +" \u001b[90m────────────────────────────────────────────────────────────\u001b[39m", +"", +(.body // "")' +''' + +[actions.open] +description = "Open the issue in the browser" +command = "gh issue view {strip_ansi|split:#:1|split: :0} --web" +mode = "fork" + +[actions.close] +description = "Close the selected issue" +command = "gh issue close {strip_ansi|split:#:1|split: :0}" +mode = "fork" + +[actions.comment] +description = "Add a comment to the selected issue" +command = "gh issue comment {strip_ansi|split:#:1|split: :0}" +mode = "execute" + diff --git a/cable/gh-prs.toml b/cable/gh-prs.toml new file mode 100644 index 0000000..2ac73c5 --- /dev/null +++ b/cable/gh-prs.toml @@ -0,0 +1,52 @@ +[metadata] +name = "gh-prs" +description = "List GitHub PRs for the current repo" +requirements = ["gh", "jq"] + +[source] +command = '''gh pr list --state open --limit 100 --json number,title,createdAt,author,labels | jq -r 'sort_by(.createdAt) | reverse | .[] | " \u001b[32m#\(.number)\u001b[39m \(.title) \u001b[33m@\(.author.login)\u001b[39m" + (if (.labels | length) > 0 then " " + ([.labels[] | "\u001b[35m" + .name + "\u001b[39m"] | join(" ")) else "" end)' +''' +output = '{strip_ansi|split:#:1|split: :0}' +ansi = true + +[ui] +layout = "portrait" + +[preview] +command = '''gh pr view '{strip_ansi|split:#:1|split: :0}' --json number,title,body,state,headRefName,baseRefName,author,createdAt,updatedAt,mergeable,changedFiles,additions,deletions,labels,assignees,reviewDecision,headRepository,headRepositoryOwner | jq -r ' +" " + .title, +" #" + (.number | tostring), +"", +" \u001b[36mStatus:\u001b[39m \u001b[32m" + .state + "\u001b[39m " + .baseRefName + " ← " + .headRefName, +" \u001b[36mRepo:\u001b[39m \u001b[34m" + (.headRepositoryOwner.login) + "/" + (.headRepository.name) + "\u001b[39m", +" \u001b[36mAuthor:\u001b[39m \u001b[33m" + .author.login + "\u001b[39m", +" \u001b[36mCreated:\u001b[39m " + (.createdAt | fromdateiso8601 | (now - .) | if . < 3600 then (./60|floor|tostring) + " minutes ago" elif . < 86400 then (./3600|floor|tostring) + " hours ago" else (./86400|floor|tostring) + " days ago" end), +" \u001b[36mUpdated:\u001b[39m " + (.updatedAt | fromdateiso8601 | (now - .) | if . < 3600 then (./60|floor|tostring) + " minutes ago" elif . < 86400 then (./3600|floor|tostring) + " hours ago" else (./86400|floor|tostring) + " days ago" end), +(if (.labels | length) > 0 then " \u001b[36mLabels:\u001b[39m " + ([.labels[] | "\u001b[35m" + .name + "\u001b[39m"] | join(" ")) else "" end), +" \u001b[36mMerge Status:\u001b[39m " + (if .mergeable == "MERGEABLE" then "\u001b[32m✓ Clean\u001b[39m" elif .mergeable == "CONFLICTING" then "\u001b[31m✗ Dirty\u001b[39m" else "\u001b[33m? Unknown\u001b[39m" end), +" \u001b[36mChanges:\u001b[39m " + (.changedFiles | tostring) + " files \u001b[32m+" + (.additions | tostring) + "\u001b[39m \u001b[31m-" + (.deletions | tostring) + "\u001b[39m", +"", +" \u001b[90m────────────────────────────────────────────────────────────\u001b[39m", +"", +(.body // "")' +''' + +[actions.open] +description = "Open the PR in the browser" +command = "gh pr view {strip_ansi|split:#:1|split: :0} --web" +mode = "execute" + +[actions.checkout] +description = "Checkout the PR branch locally" +command = "gh pr checkout {strip_ansi|split:#:1|split: :0}" +mode = "execute" + +[actions.merge] +description = "Merge the selected PR" +command = "gh pr merge {strip_ansi|split:#:1|split: :0}" +mode = "execute" + +[actions.diff] +description = "View the PR diff" +command = "gh pr diff {strip_ansi|split:#:1|split: :0} | less" +mode = "execute" diff --git a/cable/ghq-repos.toml b/cable/ghq-repos.toml new file mode 100644 index 0000000..a153050 --- /dev/null +++ b/cable/ghq-repos.toml @@ -0,0 +1,87 @@ +[metadata] +name = "ghq-repos" +description = """ +ghq repositories (plus the chezmoi source tree) and Obsidian vaults, newest +git activity first. Enter prints the directory; the gho shell function (ctrl-o +in fish, zsh and bash) cds into it. +""" +requirements = ["ghq", "fd", "git", "serie", "onefetch", "mtime-sort"] + +# Entries are "\t": the name is what you fuzzy-match, the +# path is what enter prints. ctrl-s cycles repos → vaults. +[source] +command = [ + { name = "Repos", run = ''' +root=$(ghq root) +{ + # Checkouts living outside ghq root, listed first so they win over a ghq + # clone of the same repo: chezmoi's source (github.com/prdlk/dotfiles) and + # the nvim config (github.com/prdlk/nvim). + c=$HOME/.local/share/chezmoi + [ -d "$c/.git" ] && printf 'github.com/prdlk/dotfiles\t%s\n' "$c" + n=$HOME/.config/nvim + [ -d "$n/.git" ] && printf 'github.com/prdlk/nvim\t%s\n' "$n" + ghq list | awk -v r="$root" '{ print $0 "\t" r "/" $0 }' +} | awk -F '\t' '!seen[$1]++' | mtime-sort --repo +''' }, + { name = "Vaults", run = ''' +v=$HOME/Documents/Obsidian +fd -H -t d -d 3 '^\.obsidian$' "$v" -x dirname | + awk -v r="$v/" '{ print substr($0, length(r) + 1) "\t" $0 }' | + mtime-sort +''' }, +] +shell = "bash" +display = "{split:\\t:0}" +output = "{split:\\t:1}" + +# Two previews, ctrl-f cycles: commit graph (default), then onefetch. serie +# itself cannot draw here -- it is a fullscreen TUI that paints the graph with +# kitty images and has no print mode -- so ctrl-g (history) opens it. +[preview] +command = [ + ''' +d='{split:\t:1}' +if [ -e "$d/.git" ]; then + git -C "$d" log --graph --all --color=always --date=relative -n 300 \ + --format='%C(yellow)%h%C(reset) %C(blue)%<(12,trunc)%ad%C(reset) %s%C(auto)%d' 2>&1 +else + eza --tree --level 2 --color=always "$d" 2>/dev/null || ls -la "$d" +fi +''', + ''' +d='{split:\t:1}' +if [ -e "$d/.git" ]; then + onefetch --no-art "$d" 2>&1 +else + eza --tree --level 2 --color=always "$d" 2>/dev/null || ls -la "$d" +fi +''', +] +shell = "bash" + +# ctrl-e (edit) is bound globally in config.toml. +[keybindings] +ctrl-b = "actions:browse" +ctrl-g = "actions:history" + +[actions.edit] +description = "Open the repository in nvim" +# /dev/tty: the gho wrapper captures tv's stdout to read the picked path. +command = "cd '{split:\\t:1}' && nvim . /dev/tty" +shell = "bash" +mode = "execute" + +[actions.browse] +description = "Open the repository's web page (gop)" +command = "cd '{split:\\t:1}' && gop >/dev/null 2>&1" +shell = "bash" +mode = "fork" + +[actions.history] +description = "Browse the commit graph in serie" +# Fork: quitting serie (q) returns to the picker. /dev/tty for the same reason +# as edit; serie also needs a real tty to detect kitty graphics. +command = "cd '{split:\\t:1}' && [ -e .git ] && serie /dev/tty" +shell = "bash" +mode = "fork" diff --git a/cable/git-branch.toml b/cable/git-branch.toml new file mode 100644 index 0000000..d1e2032 --- /dev/null +++ b/cable/git-branch.toml @@ -0,0 +1,37 @@ +[metadata] +name = "git-branch" +description = "A channel to select from git branches" +requirements = ["git"] + +[source] +command = "git --no-pager branch --all --format=\"%(refname:short)\"" +output = "{split: :0}" + +[preview] +command = "git show -p --stat --pretty=fuller --color=always '{0}'" + +[keybindings] +enter = "actions:checkout" +ctrl-d = "actions:delete" +ctrl-m = "actions:merge" +ctrl-r = "actions:rebase" + +[actions.checkout] +description = "Checkout the selected branch" +command = "git checkout '{0}'" +mode = "execute" + +[actions.delete] +description = "Delete the selected branch" +command = "git branch -d '{0}'" +mode = "execute" + +[actions.merge] +description = "Merge the selected branch into current branch" +command = "git merge '{0}'" +mode = "execute" + +[actions.rebase] +description = "Rebase current branch onto the selected branch" +command = "git rebase '{0}'" +mode = "execute" diff --git a/cable/git-deletions.toml b/cable/git-deletions.toml new file mode 100644 index 0000000..bb047c9 --- /dev/null +++ b/cable/git-deletions.toml @@ -0,0 +1,24 @@ +[metadata] +name = "git-deletions" +description = "A channel to list files which were deleted from the Git repository" +requirements = ["git", "rg", "bat"] + +[source] +command = "git log --diff-filter=D --summary | rg 'delete mode \\d+' -r '' --trim" + +[preview] +command = ["git show \"$(git rev-list -n1 HEAD -- '{}')^:{}\" | bat -n --color always --file-name '{}'", "git show --summary \"$(git rev-list -n1 HEAD -- '{}')\""] + +[keybindings] +enter = "actions:print_commit" +ctrl-r = "actions:restore_file" + +[actions.print_commit] +description = "Print the latest commit where file was deleted" +command = "echo \"$(git rev-list -n1 HEAD -- {})\"" +mode = "execute" + +[actions.restore_file] +description = "Restore the selected file" +command = "git checkout \"$(git rev-list -n1 HEAD -- {})^\" -- {}" + diff --git a/cable/git-diff.toml b/cable/git-diff.toml new file mode 100644 index 0000000..fd44677 --- /dev/null +++ b/cable/git-diff.toml @@ -0,0 +1,34 @@ +[metadata] +name = "git-diff" +description = "A channel to select files from git diff commands" +requirements = ["git"] + +[source] +command = "git diff --name-only HEAD" + +[preview] +command = "git diff HEAD --color=always -- '{}'" + +[ui] +layout = "portrait" + +[keybindings] +ctrl-s = "actions:stage" +ctrl-r = "actions:restore" +ctrl-e = "actions:edit" + +[actions.stage] +description = "Stage the selected file" +command = "git add {}" +mode = "fork" + +[actions.restore] +description = "Discard changes in the selected file" +command = "git restore {}" +mode = "fork" + +[actions.edit] +description = "Open the selected file in editor" +command = "${EDITOR:-vim} {}" +shell = "bash" +mode = "execute" diff --git a/cable/git-files.toml b/cable/git-files.toml new file mode 100644 index 0000000..e51a20f --- /dev/null +++ b/cable/git-files.toml @@ -0,0 +1,35 @@ +[metadata] +name = "git-files" +description = """ +Files tracked in the current git repository, most recently modified first +(all files under the cwd outside a repo). Enter opens the selection in nvim; +bound to ctrl-e in fish, zsh and bash. +""" +requirements = ["git", "fd", "bat", "mtime-sort"] + +[source] +command = [ + { name = "Tracked", run = ''' +if top=$(git rev-parse --show-toplevel 2>/dev/null); then + git -c core.quotePath=false ls-files -- "$top" +else + fd -t f +fi | mtime-sort +''' }, + { name = "All", run = "fd -t f -H -E .git | mtime-sort" }, +] +shell = "bash" + +[preview] +command = "bat -n --color=always '{}'" +env = { BAT_THEME = "ansi" } + +# ctrl-e (edit) is bound globally in config.toml. +[keybindings] +enter = "actions:edit" + +[actions.edit] +description = "Opens the selected entries in nvim" +command = "nvim {} /dev/tty" +shell = "bash" +mode = "execute" diff --git a/cable/git-log.toml b/cable/git-log.toml new file mode 100644 index 0000000..9f80a9f --- /dev/null +++ b/cable/git-log.toml @@ -0,0 +1,34 @@ +[metadata] +name = "git-log" +description = "A channel to select from git log entries" +requirements = ["git"] + +[source] +command = "git log --graph --pretty=format:'%C(yellow)%h%Creset -%C(yellow)%d%Creset %s %Cgreen(%cr) %C(bold blue)<%an>%Creset' --abbrev-commit --color=always" +output = "{strip_ansi|split: :1}" +ansi = true +no_sort = true +frecency = false + +[preview] +command = "git show -p --stat --pretty=fuller --color=always '{strip_ansi|split: :1}' | head -n 1000" + +[keybindings] +ctrl-y = "actions:cherry-pick" +ctrl-r = "actions:revert" +ctrl-o = "actions:checkout" + +[actions.cherry-pick] +description = "Cherry-pick the selected commit" +command = "git cherry-pick '{strip_ansi|split: :1}'" +mode = "execute" + +[actions.revert] +description = "Revert the selected commit" +command = "git revert '{strip_ansi|split: :1}'" +mode = "execute" + +[actions.checkout] +description = "Checkout the selected commit" +command = "git checkout '{strip_ansi|split: :1}'" +mode = "execute" diff --git a/cable/git-reflog.toml b/cable/git-reflog.toml new file mode 100644 index 0000000..e16dff1 --- /dev/null +++ b/cable/git-reflog.toml @@ -0,0 +1,28 @@ +[metadata] +name = "git-reflog" +description = "A channel to select from git reflog entries" +requirements = ["git"] + +[source] +command = "git reflog --decorate --color=always" +output = "{0|strip_ansi}" +ansi = true +no_sort = true +frecency = false + +[preview] +command = "git show -p --stat --pretty=fuller --color=always '{0|strip_ansi}'" + +[keybindings] +ctrl-o = "actions:checkout" +ctrl-r = "actions:reset" + +[actions.checkout] +description = "Checkout the selected reflog entry" +command = "git checkout '{0|strip_ansi}'" +mode = "execute" + +[actions.reset] +description = "Reset --hard to the selected reflog entry" +command = "git reset --hard '{0|strip_ansi}'" +mode = "execute" diff --git a/cable/git-remotes.toml b/cable/git-remotes.toml new file mode 100644 index 0000000..08ce83e --- /dev/null +++ b/cable/git-remotes.toml @@ -0,0 +1,20 @@ +[metadata] +name = "git-remotes" +description = "List and manage git remotes" +requirements = ["git"] + +[source] +command = "git remote" + +[preview] +command = "git remote show '{}'" + +[actions.fetch] +description = "Fetch from the selected remote" +command = "git fetch {}" +mode = "execute" + +[actions.remove] +description = "Remove the selected remote" +command = "git remote remove {}" +mode = "execute" diff --git a/cable/git-repos.toml b/cable/git-repos.toml new file mode 100644 index 0000000..a67421a --- /dev/null +++ b/cable/git-repos.toml @@ -0,0 +1,31 @@ +[metadata] +name = "git-repos" +requirements = ["fd", "git"] +description = """ +A channel to select from git repositories on your local machine. + +This channel uses `fd` to find directories that contain a `.git` subdirectory, and then allows you to preview the git log of the selected repository. +""" + +[source] +# this is a macos version of the command. While perfectly usable on linux, you may want to tweak it a bit. +command = "fd -g .git -HL -t d -d 10 --prune ~ -E 'Library' -E 'Application Support' --exec dirname '{}'" +display = "{split:/:-1}" + +[preview] +command = "cd '{}'; git log -n 200 --pretty=medium --all --graph --color" + +[keybindings] +enter = "actions:cd" +ctrl-e = "actions:edit" + +[actions.cd] +description = "Open a new shell in the selected repository" +command = "cd {} && $SHELL" +mode = "execute" + +[actions.edit] +description = "Open the repository in editor" +command = "${EDITOR:-vim} {}" +shell = "bash" +mode = "execute" diff --git a/cable/git-stash.toml b/cable/git-stash.toml new file mode 100644 index 0000000..bcd6719 --- /dev/null +++ b/cable/git-stash.toml @@ -0,0 +1,37 @@ +[metadata] +name = "git-stash" +description = "Browse and manage git stash entries" +requirements = ["git"] + +[source] +command = "git stash list --color=always" +ansi = true +output = "{strip_ansi|split:\\::0}" +no_sort = true +frecency = false + +[preview] +command = "git stash show -p --color=always '{strip_ansi|split:\\::0}'" + +[ui] +layout = "portrait" + +[keybindings] +enter = "actions:apply" +ctrl-p = "actions:pop" +ctrl-d = "actions:drop" + +[actions.apply] +description = "Apply the selected stash" +command = "git stash apply '{strip_ansi|split:\\::0}'" +mode = "execute" + +[actions.pop] +description = "Pop the selected stash (apply and remove)" +command = "git stash pop '{strip_ansi|split:\\::0}'" +mode = "execute" + +[actions.drop] +description = "Drop the selected stash" +command = "git stash drop '{strip_ansi|split:\\::0}'" +mode = "execute" diff --git a/cable/git-submodules.toml b/cable/git-submodules.toml new file mode 100644 index 0000000..1680b71 --- /dev/null +++ b/cable/git-submodules.toml @@ -0,0 +1,20 @@ +[metadata] +name = "git-submodules" +description = "List and manage git submodules" +requirements = ["git"] + +[source] +command = "git submodule status | awk '{print $2}'" + +[preview] +command = "git -C '{}' log --oneline -10 --color=always" + +[actions.update] +description = "Update the selected submodule" +command = "git submodule update --init --recursive {}" +mode = "execute" + +[actions.sync] +description = "Sync the selected submodule URL" +command = "git submodule sync {}" +mode = "execute" diff --git a/cable/git-tags.toml b/cable/git-tags.toml new file mode 100644 index 0000000..8230fa3 --- /dev/null +++ b/cable/git-tags.toml @@ -0,0 +1,26 @@ +[metadata] +name = "git-tags" +description = "Browse and checkout git tags" +requirements = ["git"] + +[source] +command = "git tag --sort=-creatordate" +no_sort = true +frecency = false + +[preview] +command = "git show --color=always '{}'" + +[keybindings] +enter = "actions:checkout" +ctrl-d = "actions:delete" + +[actions.checkout] +description = "Checkout the selected tag" +command = "git checkout {}" +mode = "execute" + +[actions.delete] +description = "Delete the selected tag" +command = "git tag -d {}" +mode = "execute" diff --git a/cable/git-worktrees.toml b/cable/git-worktrees.toml new file mode 100644 index 0000000..4d8010c --- /dev/null +++ b/cable/git-worktrees.toml @@ -0,0 +1,24 @@ +[metadata] +name = "git-worktrees" +description = "List and switch between git worktrees" +requirements = ["git"] + +[source] +command = "git worktree list --porcelain | grep '^worktree' | cut -d' ' -f2-" + +[preview] +command = "cd '{}' && git log --oneline -10 --color=always && echo && git status --short" + +[keybindings] +enter = "actions:cd" +ctrl-d = "actions:remove" + +[actions.cd] +description = "Change to the selected worktree" +command = "cd {} && $SHELL" +mode = "execute" + +[actions.remove] +description = "Remove the selected worktree" +command = "git worktree remove {}" +mode = "execute" diff --git a/cable/images.toml b/cable/images.toml new file mode 100644 index 0000000..53d5b1c --- /dev/null +++ b/cable/images.toml @@ -0,0 +1,21 @@ +[metadata] +name = "images" +description = "Browse image files with preview" +requirements = ["fd", "chafa"] + +[source] +command = [ + { name = "Default", run = "fd -t f -e png -e jpg -e jpeg -e gif -e webp -e bmp -e svg ." }, + { name = "Hidden", run = "fd -t f -e png -e jpg -e jpeg -e gif -e webp -e bmp -e svg -H ." }, +] + +[preview] +command = "chafa -s 80x40 '{}' 2>/dev/null || file '{}'" + +[keybindings] +enter = "actions:open" + +[actions.open] +description = "Open the selected image with default viewer" +command = "xdg-open {} 2>/dev/null || open {}" +mode = "fork" diff --git a/cable/jobs.toml b/cable/jobs.toml new file mode 100644 index 0000000..540a1bc --- /dev/null +++ b/cable/jobs.toml @@ -0,0 +1,57 @@ +[metadata] +name = "jobs" +description = """ +The current fish shell's background jobs (stopped or running). tv runs in a +child process and cannot see the shell's job table, so the ctrl-z binding +(_jobs_bind) writes it to the file named by $TV_JOBS; enter prints the job id +and the binding runs `fg %`; ctrl-d kills the job. Opened any other way, +the list is empty. +""" +requirements = ["ps"] + +# Record: "% ", job id, process group (tab separated). +# Newest job first, the order fish's `jobs` prints. +[source] +command = '[ -n "$TV_JOBS" ] && [ -r "$TV_JOBS" ] && cat "$TV_JOBS"' +shell = "bash" +display = "{split:\\t:0}" +output = "{split:\\t:1}" + +[preview] +command = ''' +pg='{split:\t:2}' +# No other braces in this script: tv would read them as templates. procps +# `ps -g` matches sessions, not process groups, so filter on pgid by hand. +pids=$(ps -e -o pgid=,pid= --sort=start_time | while read -r g p; do [ "$g" = "$pg" ] && echo "$p"; done) +if [ -z "$pids" ]; then + echo "process group $pg is gone" + exit 0 +fi +first=$(echo "$pids" | head -n1) +printf '\033[1mcwd\033[0m %s\n' "$(readlink "/proc/$first/cwd" 2>/dev/null || echo '?')" +printf '\033[1mstarted\033[0m %s\n\n' "$(ps -o lstart= -p "$first" | sed 's/^ *//')" +# STAT T = stopped (ctrl-z), S/R = running in the background. +ps -o pid,stat,etime,pcpu,rss,args --forest -p "$(echo $pids | tr ' ' ,)" +''' +shell = "bash" + +[keybindings] +ctrl-d = ["actions:kill", "reload_source"] + +# SIGTERM to the whole process group, then SIGCONT: a stopped job holds TERM +# until it runs again. The row leaves $TV_JOBS only once the group is gone +# (up to 1 s), so reload_source keeps a job that ignored TERM. No braces: tv +# would read them as templates. +[actions.kill] +description = "Kill the job (SIGTERM to its process group)" +command = ''' +pg='{split:\t:2}' +kill -TERM -- "-$pg" 2>/dev/null +kill -CONT -- "-$pg" 2>/dev/null +for i in 1 2 3 4 5 6 7 8 9 10; do kill -0 -- "-$pg" 2>/dev/null || break; sleep 0.1; done +if ! kill -0 -- "-$pg" 2>/dev/null && [ -w "$TV_JOBS" ]; then + awk -F '\t' -v g="$pg" '$3 != g' "$TV_JOBS" >"$TV_JOBS.new" && mv "$TV_JOBS.new" "$TV_JOBS" +fi +''' +shell = "bash" +mode = "fork" diff --git a/cable/journal.toml b/cable/journal.toml new file mode 100644 index 0000000..5161d0b --- /dev/null +++ b/cable/journal.toml @@ -0,0 +1,24 @@ +[metadata] +name = "journal" +description = "Browse systemd journal log identifiers and their logs" +requirements = ["journalctl"] + +[source] +command = "journalctl --field SYSLOG_IDENTIFIER 2>/dev/null | sort -f" + +[preview] +command = "journalctl -b --no-pager -o short-iso -n 50 SYSLOG_IDENTIFIER='{}' 2>/dev/null" + +[ui] +layout = "portrait" +preview_panel = { size = 70, border_type = "none", scrollbar = false, padding = { left = 2, right = 1, top = 0, bottom = 0 } } + +[actions.logs] +description = "Follow live logs for the selected identifier" +command = "journalctl -f SYSLOG_IDENTIFIER={}" +mode = "execute" + +[actions.full] +description = "View all logs for the selected identifier in a pager" +command = "journalctl -b --no-pager -o short-iso SYSLOG_IDENTIFIER={} | less" +mode = "fork" diff --git a/cable/just-recipes.toml b/cable/just-recipes.toml new file mode 100644 index 0000000..69b2fd2 --- /dev/null +++ b/cable/just-recipes.toml @@ -0,0 +1,18 @@ +[metadata] +name = "just-recipes" +description = "A channel to select recipes from Justfiles" +requirements = ["just"] + +[source] +command = ["just --summary | tr '[:blank:]' '\n'"] + +[preview] +command = "just -s {}" + +[keybindings] +f5 = "actions:execute-recipe" + +[actions.execute-recipe] +description = "Execute a justfile recipe" +command = "just {}" +mode = "execute" diff --git a/cable/kitty-days.toml b/cable/kitty-days.toml new file mode 100644 index 0000000..d4b19e9 --- /dev/null +++ b/cable/kitty-days.toml @@ -0,0 +1,75 @@ +[metadata] +name = "kitty-days" +description = """ +Daily kitty sessions saved by ~/.config/kitty/day_session.py, last modified +first. The preview lists each tab's windows (cwd, the program restored in it) +with the tail of its saved scrollback. Enter opens the day in a new kitty. +""" +requirements = ["kitty", "python3", "mtime-sort"] + +# Entries are " week of tabs\t". +[source] +command = ''' +for f in "$HOME"/.local/share/kitty-sessions/*/*.kitty-session; do + [ -e "$f" ] || continue + day=$(basename "$f" .kitty-session) + printf '%s %s week of %s %s tabs\t%s\n' "$day" "$(date -d "$day" +%a)" \ + "$(basename "$(dirname "$f")")" "$(grep -c '^new_tab' "$f")" "$f" +done | mtime-sort +''' +shell = "bash" +display = "{split:\\t:0}" +output = "{split:\\t:1}" + +# The script avoids braces: tv would read them as templates. +[preview] +command = ''' +python3 - '{split:\t:1}' <<'PY' +import json, os, shlex, sys + +FLAG = 'kitty-unserialize-data=' +home = os.path.expanduser('~') +tab, cwd = 0, '' +for line in open(sys.argv[1]).read().splitlines(): + word, _, rest = line.partition(' ') + if word == 'new_tab': + tab += 1 + print('\x1b[1mtab %d\x1b[m' % tab) + elif word == 'cd': + cwd = shlex.split(rest)[0] + elif word == 'launch': + args = shlex.split(rest) + data = json.loads(args.pop(0)[len(FLAG):]) if args and args[0].startswith(FLAG) else dict() + wcwd = next((a.partition('=')[2] for a in args if a.startswith('--cwd=')), cwd) + cmd = [a for a in args if not a.startswith('-')] # kitty writes every option as --opt=value + dump, program = '', shlex.join(cmd) + for step in data.get('cmd_at_shell_startup', '').split('; '): + if step.startswith('cat '): + dump = shlex.split(step)[1] + elif step: + program = step + print(' \x1b[36m%s\x1b[m %s' % (wcwd.replace(home, '~', 1), program)) + if dump and os.path.exists(dump): + for l in open(dump, errors='replace').read().splitlines()[-6:]: + print(' ' + l + '\x1b[m') +PY +''' +shell = "bash" + +# ctrl-e (edit) is bound globally in config.toml. +[keybindings] +enter = "actions:open" + +# Today's file, while no running kitty owns it, comes back as the live day +# (saved again); any other day is a read-only snapshot. +[actions.open] +description = "Open the day in a new kitty" +command = "setsid -f kitty --session '{split:\\t:1}' >/dev/null 2>&1" +shell = "bash" +mode = "execute" + +[actions.edit] +description = "Open the session file in nvim" +command = "nvim '{split:\\t:1}' /dev/tty" +shell = "bash" +mode = "execute" diff --git a/cable/make-targets.toml b/cable/make-targets.toml new file mode 100644 index 0000000..dbf78aa --- /dev/null +++ b/cable/make-targets.toml @@ -0,0 +1,18 @@ +[metadata] +name = "make-targets" +description = "List and run Makefile targets" +requirements = ["make", "awk"] + +[source] +command = "make -pRrq 2>/dev/null | awk -F: '/^[a-zA-Z0-9][^$#\\/\\t=]*:([^=]|$)/ {split($1,a,\" \"); print a[1]}' | sort -u | grep -v '^Makefile$'" + +[preview] +command = "awk '/^{}[[:space:]]*:/{found=1} found{print; if(/^[^\\t]/ && NR>1 && !/^{}[[:space:]]*:/) exit}' Makefile" + +[keybindings] +enter = "actions:run" + +[actions.run] +description = "Run the selected make target" +command = "make {}" +mode = "execute" diff --git a/cable/man-pages.toml b/cable/man-pages.toml new file mode 100644 index 0000000..6a20097 --- /dev/null +++ b/cable/man-pages.toml @@ -0,0 +1,25 @@ +[metadata] +name = "man-pages" +description = "Browse and preview system manual pages" +requirements = ["apropos", "man"] + +[source] +# List all man pages using apropos +command = "apropos ." + +[preview] +# Show the man page for the selected entry +command = "man '{0}'" +env = { "MANWIDTH" = "80" } + +[keybindings] +enter = "actions:open" + +[actions.open] +description = "Open the selected man page in the system pager" +command = "man '{0}'" +mode = "execute" + +[ui] +layout = "portrait" +preview_panel = { header = "{0}", border_type = "none", scrollbar = false, padding = { left = 2, right = 1, top = 0, bottom = 0 } } diff --git a/cable/mounts.toml b/cable/mounts.toml new file mode 100644 index 0000000..531aa70 --- /dev/null +++ b/cable/mounts.toml @@ -0,0 +1,19 @@ +[metadata] +name = "mounts" +description = "List mounted filesystems" +requirements = ["df", "awk"] + +[source] +command = "df -h --output=target,fstype,size,used,avail,pcent 2>/dev/null | tail -n +2" +display = "{split: :0}" + +[preview] +command = "df -h '{}' && echo && ls -la '{}' 2>/dev/null | head -20" + +[keybindings] +enter = "actions:cd" + +[actions.cd] +description = "Open a shell in the selected mount point" +command = "cd {} && $SHELL" +mode = "execute" diff --git a/cable/node-packages.toml b/cable/node-packages.toml new file mode 100644 index 0000000..89cee7a --- /dev/null +++ b/cable/node-packages.toml @@ -0,0 +1,21 @@ +[metadata] +name = "node-packages" +description = "Browse local node_modules dependencies" +requirements = ["node"] + +[source] +command = "ls -d node_modules/*/ 2>/dev/null | sed 's|node_modules/||;s|/$||' | grep -v '^\\.'" +output = "{}" + +[preview] +command = "cat 'node_modules/{}/package.json' 2>/dev/null | jq -C '{name, version, description, license, homepage, main}'" + +[actions.readme] +description = "View the package README" +command = "cat node_modules/{}/README.md 2>/dev/null | less" +mode = "execute" + +[actions.homepage] +description = "Open the package homepage" +command = "node -e \"const p=require('./node_modules/{}/package.json'); const u=p.homepage||'https://www.npmjs.com/package/'+p.name; require('child_process').exec('xdg-open '+u)\"" +mode = "fork" diff --git a/cable/npm-packages.toml b/cable/npm-packages.toml new file mode 100644 index 0000000..6d26a9e --- /dev/null +++ b/cable/npm-packages.toml @@ -0,0 +1,20 @@ +[metadata] +name = "npm-packages" +description = "List globally installed npm packages" +requirements = ["npm"] + +[source] +command = "npm list -g --depth=0 --parseable 2>/dev/null | tail -n +2 | xargs -I{} basename {}" + +[preview] +command = "npm info '{}' 2>/dev/null | head -30" + +[actions.uninstall] +description = "Uninstall the selected global package" +command = "npm uninstall -g {}" +mode = "execute" + +[actions.update] +description = "Update the selected global package" +command = "npm update -g {}" +mode = "execute" diff --git a/cable/npm-scripts.toml b/cable/npm-scripts.toml new file mode 100644 index 0000000..ab80f6d --- /dev/null +++ b/cable/npm-scripts.toml @@ -0,0 +1,25 @@ +[metadata] +name = "npm-scripts" +description = "List and run npm scripts from package.json" +requirements = ["jq"] + +[source] +command = "jq -r '.scripts | to_entries[] | \"\\(.key)\\t\\(.value)\"' package.json 2>/dev/null" +display = "{split:\\t:0}" + +[preview] +command = "jq -r '.scripts[\"{split:\\t:0}\"]' package.json" + +[ui.preview_panel] +border_type = "none" +scrollbar = false +padding = { left = 2, right = 1, top = 0, bottom = 0 } +size = 30 + +[keybindings] +enter = "actions:run" + +[actions.run] +description = "Run the selected npm script" +command = "npm run '{split:\\t:0}'" +mode = "execute" diff --git a/cable/nu-history.toml b/cable/nu-history.toml new file mode 100644 index 0000000..f6d9899 --- /dev/null +++ b/cable/nu-history.toml @@ -0,0 +1,9 @@ + +[metadata] +name = "nu-history" +description = "A channel to select from your nu history" + +[source] +command = "nu -c 'open $nu.history-path | lines | uniq | reverse | to text'" +no_sort = true +frecency = false diff --git a/cable/obsidian.toml b/cable/obsidian.toml new file mode 100644 index 0000000..a065dc5 --- /dev/null +++ b/cable/obsidian.toml @@ -0,0 +1,66 @@ +[metadata] +name = "obsidian" +description = """ +Obsidian vaults under ~/Documents/Obsidian, most recently modified first. +Enter opens the vault in Obsidian; vaults Obsidian has never opened are +registered in obsidian.json first. +""" +requirements = ["fd", "jq", "eza", "obsidian", "xdg-open", "mtime-sort"] + +# Entries are "\t": the name is what you fuzzy-match, the +# path is what the actions use. +[source] +command = ''' +v=$HOME/Documents/Obsidian +fd -H -t d -d 3 '^\.obsidian$' "$v" -x dirname | + awk -v r="$v/" '{ print substr($0, length(r) + 1) "\t" $0 }' | + mtime-sort +''' +shell = "bash" +display = "{split:\\t:0}" +output = "{split:\\t:1}" + +[preview] +command = "eza --tree --level 2 --color=always '{split:\\t:1}' 2>/dev/null || ls -la '{split:\\t:1}'" +shell = "bash" + +# ctrl-e (edit) is bound globally in config.toml. +[keybindings] +enter = "actions:open" + +# obsidian://open?path= only resolves vaults listed in obsidian.json, and +# Obsidian reads that file once at startup and rewrites it from memory. An +# unlisted vault is therefore added only while Obsidian is closed; while it +# runs, the vault switcher opens instead. +[actions.open] +description = "Open the vault in Obsidian" +command = ''' +d='{split:\t:1}' +cfg=${XDG_CONFIG_HOME:-$HOME/.config}/obsidian/obsidian.json +uri="obsidian://open?path=$(jq -rn --arg p "$d" '$p | @uri')" +if ! jq -e --arg p "$d" 'any(.vaults[]?; .path == $p)' "$cfg" >/dev/null 2>&1; then + # [o]: keep pgrep from matching this script's own command line. + if pgrep -f '[o]bsidian/app.asar' >/dev/null; then + msg="Vault not registered and Obsidian is running: use 'Open folder as vault' for $d" + echo "$msg" >&2 + notify-send Obsidian "$msg" 2>/dev/null + uri="obsidian://choose-vault" + else + mkdir -p "${cfg%/*}" + [ -s "$cfg" ] || echo '{}' >"$cfg" + tmp=$(mktemp "$cfg.XXXXXX") || exit 1 + jq --arg id "$(od -An -N8 -tx1 /dev/urandom | tr -d ' \n')" --arg p "$d" \ + --argjson ts "$(date +%s%3N)" '.vaults[$id] = {path: $p, ts: $ts}' \ + "$cfg" >"$tmp" && mv "$tmp" "$cfg" || rm -f "$tmp" + fi +fi +setsid -f xdg-open "$uri" >/dev/null 2>&1 +''' +shell = "bash" +mode = "execute" + +[actions.edit] +description = "Open the vault in nvim" +command = "cd '{split:\\t:1}' && nvim . /dev/tty" +shell = "bash" +mode = "execute" diff --git a/cable/omp-sessions.toml b/cable/omp-sessions.toml new file mode 100644 index 0000000..e078403 --- /dev/null +++ b/cable/omp-sessions.toml @@ -0,0 +1,38 @@ +[metadata] +name = "omp-sessions" +description = """ +omp sessions of the current git repo, newest first: age, the branch HEAD was +on when the session was last written, and the title. The preview is the +conversation's message text; enter resumes the session, ctrl-n starts a new +one instead. Listing, preview, resume and new live in ~/.local/bin/omp-session, +whose launcher (ctrl-a in fish and zsh) opens this cable when it has no +session to resume on the default branch. +""" +requirements = ["omp-session", "omp"] + +# Record: display, session file (tab separated). +[source] +command = "omp-session --list" +shell = "bash" +display = "{split:\\t:0}" +output = "{split:\\t:1}" + +[preview] +command = "omp-session --preview '{split:\\t:1}'" +shell = "bash" + +[keybindings] +enter = "actions:resume" +ctrl-n = "actions:new" + +[actions.resume] +description = "Resume the session" +command = "omp-session --resume '{split:\\t:1}'" +shell = "bash" +mode = "execute" + +[actions.new] +description = "Start a new omp session" +command = "omp-session --new" +shell = "bash" +mode = "execute" diff --git a/cable/opencode-sessions.toml b/cable/opencode-sessions.toml new file mode 100644 index 0000000..30fe3f2 --- /dev/null +++ b/cable/opencode-sessions.toml @@ -0,0 +1,38 @@ +[metadata] +name = "opencode-sessions" +description = "Browse and resume OpenCode sessions" +requirements = ["opencode", "jq"] + +[source] +# Use literal strings (''') to avoid escape sequence errors +command = '''opencode session list --format json | jq -r '.[] | "\(.id)\t\(.title)\t\(.directory)"' ''' +display = "{split:\t:1} ({split:\t:2})" +output = "{split:\t:0}" + +[preview] +# We use triple single quotes so we don't have to escape the backslashes for jq or the template engine +command = '''opencode session list --format json | jq -r '.[] | select(.id == "{split:\t:0}") | "TITLE: \(.title)\nID: \(.id)\nPROJECT: \(.projectId)\nDIRECTORY: \(.directory)\nUPDATED: \((.updated / 1000) | strftime("%Y-%m-%d %H:%M:%S"))"' ''' + +[ui] +layout = "landscape" +[ui.preview_panel] +size = 60 +header = "Session: {split:\t:1}" +border_type = "none" +scrollbar = false +padding = { left = 2, right = 1, top = 0, bottom = 0 } + +[keybindings] +shortcut = "f4" +enter = "actions:resume" +ctrl-d = "actions:delete" + +[actions.resume] +description = "Resume session" +command = "opencode -s {split:\t:0}" +mode = "execute" + +[actions.delete] +description = "Delete session" +command = '''bash -c 'echo "Delete {split:\t:1}?" && read -p "[y/N]: " conf && [[ $conf == "y" ]] && opencode session delete {split:\t:0} ' ''' +mode = "execute" diff --git a/cable/pacman-packages.toml b/cable/pacman-packages.toml new file mode 100644 index 0000000..c53a7e4 --- /dev/null +++ b/cable/pacman-packages.toml @@ -0,0 +1,21 @@ +[metadata] +name = "pacman-packages" +description = "List and manage installed pacman packages" +requirements = ["pacman"] + +[source] +command = "pacman -Q" +output = "{split: :0}" + +[preview] +command = "pacman -Qi '{split: :0}' 2>/dev/null" + +[actions.remove] +description = "Remove the selected package" +command = "sudo pacman -R '{split: :0}'" +mode = "execute" + +[actions.files] +description = "List files owned by the selected package" +command = "pacman -Ql '{split: :0}' | less" +mode = "execute" diff --git a/cable/path.toml b/cable/path.toml new file mode 100644 index 0000000..ec4bec2 --- /dev/null +++ b/cable/path.toml @@ -0,0 +1,15 @@ +[metadata] +name = "path" +description = "Investigate PATH contents" +requirements = ["fd", "bat"] + +[source] +command = "printf '%s\n' \"$PATH\" | tr ':' '\n'" + +[preview] +command = "fd -tx -d1 . \"{}\" -X printf \"%s\n\" \"{/}\" | sort -f | bat -n --color=always" + +[actions.cd] +description = "Open a shell in the selected PATH directory" +command = "cd {} && $SHELL" +mode = "execute" diff --git a/cable/pdf-files.toml b/cable/pdf-files.toml new file mode 100644 index 0000000..b41b01a --- /dev/null +++ b/cable/pdf-files.toml @@ -0,0 +1,24 @@ +[metadata] +name = "pdf-files" +description = "Browse PDF files" +requirements = ["fd", "pdftotext"] + +[source] +command = [ + { name = "Default", run = "fd -t f -e pdf ." }, + { name = "Hidden", run = "fd -t f -e pdf -H ." }, +] + +[preview] +command = "pdftotext -l 2 -layout '{}' - 2>/dev/null | head -100 || file '{}'" + +[ui] +layout = "portrait" + +[keybindings] +enter = "actions:open" + +[actions.open] +description = "Open the selected PDF with default viewer" +command = "xdg-open {} 2>/dev/null || open {}" +mode = "fork" diff --git a/cable/pip-packages.toml b/cable/pip-packages.toml new file mode 100644 index 0000000..8dd88b8 --- /dev/null +++ b/cable/pip-packages.toml @@ -0,0 +1,27 @@ +[metadata] +name = "pip-packages" +description = "List installed Python packages" +requirements = ["pip"] + +[source] +command = "pip list --format=freeze 2>/dev/null | cut -d= -f1" + +[preview] +command = "pip show '{}'" + +[ui] +layout = "portrait" + +[keybindings] +ctrl-u = "actions:upgrade" +ctrl-d = "actions:uninstall" + +[actions.upgrade] +description = "Upgrade the selected package" +command = "pip install --upgrade {}" +mode = "execute" + +[actions.uninstall] +description = "Uninstall the selected package" +command = "pip uninstall {}" +mode = "execute" diff --git a/cable/pj.toml b/cable/pj.toml new file mode 100644 index 0000000..c11c654 --- /dev/null +++ b/cable/pj.toml @@ -0,0 +1,29 @@ +[metadata] +name = "pj" +description = """ +Project tasks from every task source between the cwd and the git toplevel +(package.json, Makefile, justfile, mise, devbox, pyproject), tasks of the most +recently modified manifest first. Enter runs the task. Discovery, preview and +run live in ~/.local/bin/pj; `pj` (ctrl-p, and down on an empty prompt in fish, +zsh and bash) opens this cable with its own scan. +""" +requirements = ["pj", "mtime-sort"] + +# Record: display, kind, target, root, runner, manifest (tab separated). +[source] +command = "pj --list 2>/dev/null" +shell = "bash" +display = "{split:\\t:0}" + +[preview] +command = "pj --preview '{split:\\t:1}' '{split:\\t:2}' '{split:\\t:3}' '{split:\\t:5}'" +shell = "bash" + +[keybindings] +enter = "actions:run" + +[actions.run] +description = "Run the selected task" +command = "pj --run '{split:\\t:1}' '{split:\\t:2}' '{split:\\t:3}' '{split:\\t:4}'" +shell = "bash" +mode = "execute" diff --git a/cable/ports.toml b/cable/ports.toml new file mode 100644 index 0000000..33d2e75 --- /dev/null +++ b/cable/ports.toml @@ -0,0 +1,22 @@ +[metadata] +name = "ports" +description = "List listening ports and associated processes" +requirements = ["ss", "awk"] + +[source] +command = "ss -tlnp 2>/dev/null | tail -n +2 | awk '{gsub(/.*:/,\"\",$4); print $4, $1, $6}' | sed 's/users:((\"//; s/\".*//'" +display = "{split: :0} ({split: :2})" + +[preview] +command = "ss -tlnp 2>/dev/null | grep ':{split: :0} ' | head -20" + +[ui.preview_panel] +border_type = "none" +scrollbar = false +padding = { left = 2, right = 1, top = 0, bottom = 0 } +size = 40 + +[actions.kill] +description = "Kill the process listening on the selected port" +command = "fuser -k {split: :0}/tcp" +mode = "execute" diff --git a/cable/procs.toml b/cable/procs.toml new file mode 100644 index 0000000..de87ff7 --- /dev/null +++ b/cable/procs.toml @@ -0,0 +1,38 @@ +[metadata] +name = "procs" +description = "A channel to find and manage running processes" +requirements = ["ps", "awk"] + +[source] +command = "ps -e -o pid=,ucomm= | awk '{print $1, $2}'" +display = "{split: :1}" +output = "{split: :0}" + +[preview] +command = "ps -p '{split: :0}' -o user,pid,ppid,state,%cpu,%mem,command | fold" + +[keybindings] +ctrl-k = "actions:kill" +f2 = "actions:term" +ctrl-s = "actions:stop" +ctrl-c = "actions:cont" + +[actions.kill] +description = "Kill the selected process (SIGKILL)" +command = "kill -9 {split: :0}" +mode = "execute" + +[actions.term] +description = "Terminate the selected process (SIGTERM)" +command = "kill -15 {split: :0}" +mode = "execute" + +[actions.stop] +description = "Stop/pause the selected process (SIGSTOP)" +command = "kill -STOP {split: :0}" +mode = "fork" + +[actions.cont] +description = "Continue/resume the selected process (SIGCONT)" +command = "kill -CONT {split: :0}" +mode = "fork" diff --git a/cable/python-venvs.toml b/cable/python-venvs.toml new file mode 100644 index 0000000..e1bc12b --- /dev/null +++ b/cable/python-venvs.toml @@ -0,0 +1,20 @@ +[metadata] +name = "python-venvs" +description = "Find Python virtual environments" +requirements = ["find"] + +[source] +command = "find ~ -maxdepth 5 -type f -name 'pyvenv.cfg' 2>/dev/null | xargs -I{} dirname {}" + +[preview] +command = "cat '{}/pyvenv.cfg' 2>/dev/null && echo '' && echo 'Packages:' && '{}/bin/pip' list --format=columns 2>/dev/null | head -20" + +[actions.activate] +description = "Open a shell with the selected venv activated" +command = "source '{}/bin/activate' && $SHELL" +mode = "execute" + +[actions.packages] +description = "List all packages in the selected venv" +command = "'{}/bin/pip' list | less" +mode = "execute" diff --git a/cable/recent-files.toml b/cable/recent-files.toml new file mode 100644 index 0000000..e044bf7 --- /dev/null +++ b/cable/recent-files.toml @@ -0,0 +1,23 @@ +[metadata] +name = "recent-files" +description = "List recently modified files (via git or filesystem)" +requirements = ["git", "bat"] + +[source] +command = [ + { name = "Recent", run = "git diff --name-only HEAD~10 HEAD 2>/dev/null || find . -type f -mtime -7 -not -path '*/.*' 2>/dev/null | head -100" }, + { name = "Last hour", run = "find . -type f -mmin -60 -not -path '*/.*' 2>/dev/null | head -100" }, +] + +[preview] +command = "bat -n --color=always '{}'" +env = { BAT_THEME = "ansi" } + +[keybindings] +enter = "actions:edit" + +[actions.edit] +description = "Open the selected file in editor" +command = "${EDITOR:-vim} {}" +shell = "bash" +mode = "execute" diff --git a/cable/rustup.toml b/cable/rustup.toml new file mode 100644 index 0000000..11bb7cd --- /dev/null +++ b/cable/rustup.toml @@ -0,0 +1,20 @@ +[metadata] +name = "rustup" +description = "Manage Rust toolchains" +requirements = ["rustup"] + +[source] +command = "rustup toolchain list" + +[preview] +command = "rustup run '{split: :0}' rustc --version --verbose 2>/dev/null" + +[actions.default] +description = "Set the selected toolchain as default" +command = "rustup default '{split: :0}'" +mode = "execute" + +[actions.uninstall] +description = "Uninstall the selected toolchain" +command = "rustup toolchain uninstall '{split: :0}'" +mode = "execute" diff --git a/cable/ssh-hosts.toml b/cable/ssh-hosts.toml new file mode 100644 index 0000000..9604571 --- /dev/null +++ b/cable/ssh-hosts.toml @@ -0,0 +1,18 @@ +[metadata] +name = "ssh-hosts" +description = "A channel to select hosts from your SSH config" +requirements = ["grep", "tr", "cut", "awk"] + +[source] +command = "grep -E '^Host(name)? ' $HOME/.ssh/config | tr -s ' ' | cut -d' ' -f2- | tr ' ' '\n' | grep -v '^$'" + +[preview] +command = "awk '/^Host / { found=0 } /^Host (.*[[:space:]])?'{}'([[:space:]].*)?$/ { found=1 } found' $HOME/.ssh/config" + +[keybindings] +enter = "actions:connect" + +[actions.connect] +description = "SSH into the selected host" +command = "ssh {}" +mode = "execute" diff --git a/cable/systemd-units.toml b/cable/systemd-units.toml new file mode 100644 index 0000000..fe8c6c2 --- /dev/null +++ b/cable/systemd-units.toml @@ -0,0 +1,49 @@ +[metadata] +name = "systemd-units" +description = "List and manage systemd services" +requirements = ["systemctl"] + +[source] +command = [ + { name = "Active", run = "systemctl list-units --type=service --no-pager --no-legend --plain" }, + { name = "All", run = "systemctl list-units --type=service --all --no-pager --no-legend --plain" }, +] +display = "{split: :0}" + +[preview] +command = "systemctl status '{split: :0}' --no-pager" + +[ui] +layout = "portrait" + +[keybindings] +ctrl-s = "actions:start" +f2 = "actions:stop" +ctrl-r = "actions:restart" +ctrl-e = "actions:enable" +ctrl-d = "actions:disable" + +[actions.start] +description = "Start the selected service" +command = "sudo systemctl start '{split: :0}'" +mode = "execute" + +[actions.stop] +description = "Stop the selected service" +command = "sudo systemctl stop '{split: :0}'" +mode = "execute" + +[actions.restart] +description = "Restart the selected service" +command = "sudo systemctl restart '{split: :0}'" +mode = "execute" + +[actions.enable] +description = "Enable the selected service" +command = "sudo systemctl enable '{split: :0}'" +mode = "execute" + +[actions.disable] +description = "Disable the selected service" +command = "sudo systemctl disable '{split: :0}'" +mode = "execute" diff --git a/cable/tailscale-exit-node.toml b/cable/tailscale-exit-node.toml new file mode 100644 index 0000000..099b8ac --- /dev/null +++ b/cable/tailscale-exit-node.toml @@ -0,0 +1,18 @@ +[metadata] +name = "tailscale-exit-node" +description = "A channel to select a tailscale exit node" +requirements = ["tailscale", "awk"] + +[source] +command = "tailscale exit-node list | awk '/^[[:space:]]*[0-9]+\\./ {gsub(/^[[:space:]]+/, \"\"); print $1}'" + +[preview] +command = "tailscale whois {}" + +[keybindings] +enter = "actions:connect" + +[actions.connect] +description = "Set selected host as exit node" +command = "tailscale set --exit-node={}" +mode = "execute" diff --git a/cable/tea-issues.toml b/cable/tea-issues.toml new file mode 100644 index 0000000..fa78a2e --- /dev/null +++ b/cable/tea-issues.toml @@ -0,0 +1,36 @@ +[metadata] +name = "tea-issues" +description = "List Gitea issues for the current repo" +requirements = ["tea", "jq"] + +[source] +command = '''tea issues list --output json --state open 0 then "\u001b[36mLabels:\u001b[39m " + ([.labels[] | "\u001b[35m" + .name + "\u001b[39m"] +| join(" ")) else empty end), +"", +"\u001b[90m────────────────────────────────────────────────────────────\u001b[39m", +"", +(.body // "") +' +''' + +[actions.close] +description = "Close the selected issue" +command = "tea issues close {strip_ansi|split:#:1|split:\t:0}" +mode = "fork" diff --git a/cable/tea-prs.toml b/cable/tea-prs.toml new file mode 100644 index 0000000..9f62e5e --- /dev/null +++ b/cable/tea-prs.toml @@ -0,0 +1,42 @@ +[metadata] +name = "tea-prs" +description = "List Gitea PRs for the current repo" +requirements = ["tea", "jq"] + +[source] +command = '''tea pulls list --output json --state open 0 then "\u001b[36mLabels:\u001b[39m " + ([.labels[] | "\u001b[35m" + .name + "\u001b[39m"] | join(" ")) else empty end), +"\u001b[36mMergeable:\u001b[39m " + (if .mergeable then "\u001b[32m✓ Clean\u001b[39m" else "\u001b[31m✗ Dirty\u001b[39m" end), +"", +"\u001b[90m────────────────────────────────────────────────────────────\u001b[39m", +"", +(.body // "") +' +''' + +[actions.checkout] +description = "Checkout the PR branch locally" +command = "tea pulls checkout {strip_ansi|split:#:1|split:\t:0}" +mode = "execute" + +[actions.merge] +description = "Merge the selected PR" +command = "tea pulls merge {strip_ansi|split:#:1|split:\t:0}" +mode = "execute" diff --git a/cable/text.toml b/cable/text.toml new file mode 100644 index 0000000..d7544ff --- /dev/null +++ b/cable/text.toml @@ -0,0 +1,29 @@ +[metadata] +name = "text" +description = "A channel to find and select text from files" +requirements = ["rg", "bat"] + +[source] +command = [ + { name = "Default", run = "rg . --no-heading --line-number --colors 'match:fg:white' --colors 'path:fg:blue' --color=always" }, + { name = "Hidden", run = "rg . --no-heading --line-number --hidden --colors 'match:fg:white' --colors 'path:fg:blue' --color=always" }, +] +ansi = true +output = "{strip_ansi|split:\\::..2}" + +[preview] +command = "bat -n --color=always '{strip_ansi|split:\\::0}'" +env = { BAT_THEME = "ansi" } +offset = '{strip_ansi|split:\::1}' + +[ui] +preview_panel = { header = '{strip_ansi|split:\::..2}', border_type = "none", scrollbar = false, padding = { left = 2, right = 1, top = 0, bottom = 0 } } + +[keybindings] +enter = "actions:edit" + +[actions.edit] +description = "Open file in editor at line" +command = "${EDITOR:-vim} '+{strip_ansi|split:\\::1}' '{strip_ansi|split:\\::0}'" +shell = "bash" +mode = "execute" diff --git a/cable/tldr.toml b/cable/tldr.toml new file mode 100644 index 0000000..884b453 --- /dev/null +++ b/cable/tldr.toml @@ -0,0 +1,21 @@ +[metadata] +name = "tldr" +description = "Browse and preview TLDR help pages for command-line tools" +requirements = ["tldr"] + +[source] +command = "tldr --list" + +[preview] +command = "tldr '{0}' --color always" + +[ui] +layout = "portrait" + +[keybindings] +ctrl-e = "actions:open" + +[actions.open] +description = "Open the selected TLDR page" +command = "tldr '{0}'" +mode = "execute" diff --git a/cable/tmux-sessions.toml b/cable/tmux-sessions.toml new file mode 100644 index 0000000..6e77d24 --- /dev/null +++ b/cable/tmux-sessions.toml @@ -0,0 +1,22 @@ +[metadata] +name = "tmux-sessions" +description = "List and manage tmux sessions" +requirements = ["tmux"] + +[source] +command = "tmux list-sessions -F '#{session_name}\t#{session_windows} windows\t#{session_created_string}'" +display = "{split:\t:0} ({split:\t:1})" +output = "{split:\t:0}" + +[preview] +command = "tmux capture-pane -t '{split:\t:0}' -p 2>/dev/null || echo 'No preview available'" + +[actions.attach] +description = "Attach to the selected session" +command = "tmux attach-session -t '{split:\t:0}'" +mode = "execute" + +[actions.kill] +description = "Kill the selected session" +command = "tmux kill-session -t '{split:\t:0}'" +mode = "fork" diff --git a/cable/tmux-windows.toml b/cable/tmux-windows.toml new file mode 100644 index 0000000..557e259 --- /dev/null +++ b/cable/tmux-windows.toml @@ -0,0 +1,22 @@ +[metadata] +name = "tmux-windows" +description = "List and switch between tmux windows" +requirements = ["tmux"] + +[source] +command = "tmux list-windows -a -F '#{session_name}:#{window_index}\t#{window_name}\t#{pane_current_command}'" +display = "{split:\t:0} - {split:\t:1} ({split:\t:2})" +output = "{split:\t:0}" + +[preview] +command = "tmux capture-pane -t '{split:\t:0}' -p 2>/dev/null || echo 'No preview available'" + +[actions.select] +description = "Switch to the selected window" +command = "tmux select-window -t '{split:\t:0}'" +mode = "execute" + +[actions.kill] +description = "Kill the selected window" +command = "tmux kill-window -t '{split:\t:0}'" +mode = "fork" diff --git a/cable/todo-comments.toml b/cable/todo-comments.toml new file mode 100644 index 0000000..e3e049b --- /dev/null +++ b/cable/todo-comments.toml @@ -0,0 +1,26 @@ +[metadata] +name = "todo-comments" +description = "Find TODO, FIXME, HACK, and XXX comments in codebase" +requirements = ["rg", "bat"] + +[source] +command = "rg -n --color=always '(TODO|FIXME|HACK|XXX|BUG|WARN):?'" +ansi = true +output = "{strip_ansi|split:\\::..2}" + +[preview] +command = "bat -n --color=always --highlight-line '{strip_ansi|split:\\::1}' '{strip_ansi|split:\\::0}'" +env = { BAT_THEME = "ansi" } +offset = "{strip_ansi|split:\\::1}" + +[ui] +preview_panel = { header = "{strip_ansi|split:\\::..2}", border_type = "none", scrollbar = false, padding = { left = 2, right = 1, top = 0, bottom = 0 } } + +[keybindings] +enter = "actions:edit" + +[actions.edit] +description = "Open file in editor at the comment" +command = "${EDITOR:-vim} '+{strip_ansi|split:\\::1}' '{strip_ansi|split:\\::0}'" +shell = "bash" +mode = "execute" diff --git a/cable/unicode.toml b/cable/unicode.toml new file mode 100644 index 0000000..c96a6cf --- /dev/null +++ b/cable/unicode.toml @@ -0,0 +1,43 @@ +[metadata] +name = "unicode" +description = '''Search and insert unicode characters + +The UnicodeData.txt file is included by many packages. + +In addition to: + +Alpine Linux: unicode-character-database +Arch: unicode-character-database +Debian/Ubuntu: unicode-data +Fedora / RHEL / CentOS unicode-ucd +Gentoo: app-i18n/unicode-data +NixOS: unicode/unicode-data +openSUSE: unicode-ucd + +UnicodData.txt may also aleady be provided by: + +1) Many java packages +2) Latex packages +3) Still others + +It may in some cases be necessary to alter UNICODE_FILE below. + +''' +requirements = ["awk", "perl"] + +[source] +command = ''' +UNICODE_FILE="/usr/share/unicode/ucd/UnicodeData.txt" +awk -F';' ' + $2 !~ /^/dev/null | grep -v '^:' | sort -t: -k2 -rn" +display = "{split:\\::0} ({split:\\::1}% {split:\\::2})" +output = "{split:\\::0}" + +[preview] +command = "nmcli -t -f SSID,BSSID,MODE,FREQ,SIGNAL,SECURITY,ACTIVE device wifi list 2>/dev/null | grep '^{split:\\::0}:'" + +[actions.connect] +description = "Connect to the selected network" +command = "nmcli device wifi connect '{split:\\::0}'" +mode = "execute" diff --git a/cable/zoxide.toml b/cable/zoxide.toml new file mode 100644 index 0000000..aa69084 --- /dev/null +++ b/cable/zoxide.toml @@ -0,0 +1,26 @@ +[metadata] +name = "zoxide" +description = "Browse zoxide directory history" +requirements = ["zoxide"] + +[source] +command = "zoxide query -l" +no_sort = true +frecency = false + +[preview] +command = "ls -la --color=always '{}'" + +[keybindings] +enter = "actions:cd" +ctrl-d = "actions:remove" + +[actions.cd] +description = "Change to the selected directory" +command = "cd {} && $SHELL" +mode = "execute" + +[actions.remove] +description = "Remove the selected directory from zoxide" +command = "zoxide remove {}" +mode = "fork" diff --git a/cable/zsh-history.toml b/cable/zsh-history.toml new file mode 100644 index 0000000..6bc8f90 --- /dev/null +++ b/cable/zsh-history.toml @@ -0,0 +1,16 @@ +[metadata] +name = "zsh-history" +description = "A channel to select from your zsh history" +requirements = ["zsh"] + +[source] +command = "sed '1!G;h;$!d' ${HISTFILE:-${HOME}/.zsh_history}" +display = "{split:;:1..}" +output = "{split:;:1..}" +no_sort = true +frecency = false + +[actions.execute] +description = "Execute the selected command" +command = "zsh -c '{split:;:1..}'" +mode = "execute" diff --git a/config.toml b/config.toml new file mode 100644 index 0000000..9faebd7 --- /dev/null +++ b/config.toml @@ -0,0 +1,256 @@ +# CONFIGURATION FILE LOCATION ON YOUR SYSTEM: +# ------------------------------------------- +# Defaults: +# --------- +# Linux: `$HOME/.config/television/config.toml` +# macOS: `$HOME/.config/television/config.toml` +# Windows: `%LocalAppData%\television\config\config.toml` +# +# XDG dirs: +# --------- +# You may use XDG_CONFIG_HOME if set on your system. +# In that case, television will expect the configuration file to be in: +# `$XDG_CONFIG_HOME/television/config.toml` +# + +# General settings +# ---------------------------------------------------------------------------- +tick_rate = 50 +default_channel = "files" +# Shell settings +# -------------- +# Default shell used for executing commands (source, preview, actions). +# Options: bash, zsh, fish, powershell, cmd, nu +# If not specified, the shell is detected from the environment ($SHELL on Unix). +# Channel-specific shell settings override this global setting. +# shell = "bash" +# History settings +# --------------- +# Maximum number of entries to keep in the global history (default: 100) +# The history tracks search queries across all channels and sessions +# Set to 0 to disable history functionality entirely +history_size = 200 +# Whether to use global history (default: false) +# When true: history navigation shows entries from all channels +# When false: history navigation is scoped to the current channel +global_history = false + +[ui] +# How much space to allocate for the UI (in percentage of the screen) +# ┌─────────────────────────┐ +# │ Terminal screen │ +# │ ┌───────────────────┐ │ +# │ │ │ │ +# │ │ Television UI │ │ +# │ │ │ │ +# │ └───────────────────┘ │ +# │ │ +# └─────────────────────────┘ +ui_scale = 100 +# What orientation should tv be (landscape or portrait) +orientation = "landscape" +# The theme to use for the UI: themes/cyberdream-transparent.toml in this +# config dir (a builtin theme shadows a user theme of the same name). +theme = "cyberdream-transparent" + +# Minimal UI: no borders, no status bar, no scrollbar — padding alone +# separates the panels. tv 0.15 has no config key for inline +# mode (--inline / --height are CLI-only), so every call site in the shells, +# pj and the cables passes --inline. A channel that sets its own +# [ui.preview_panel] resets these fields to tv's defaults (rounded border), +# so cables leave that table alone. +[ui.input_bar] +# Where to place the input bar in the UI (top or bottom) +position = "top" +prompt = "❯" +# header = "{}" +border_type = "none" +# a blank row between the query and the results +padding = { left = 1, right = 1, top = 0, bottom = 1 } + +[ui.status_bar] +separator_open = "" +separator_close = "" +# f12 toggles it back +hidden = true + +[ui.results_panel] +border_type = "none" +padding = { left = 1, right = 1, top = 0, bottom = 0 } + +[ui.preview_panel] +# Preview panel size (percentage of screen width/height) +size = 50 +#header = "" +#footer = "" +scrollbar = false +border_type = "none" +padding = { left = 2, right = 1, top = 0, bottom = 0 } +hidden = false + +[ui.help_panel] +# Whether to split the help panel by categories +show_categories = true +hidden = true + +[ui.remote_control] +# Whether to show channel descriptions in remote control mode +show_channel_descriptions = true +# Whether to sort channels alphabetically +sort_alphabetically = true +# disabled = false + +# Theme color overrides +# --------------------- +# You can override specific colors from the selected theme by adding them here. +# This allows you to customize the appearance without creating a full theme file. +# Colors can be specified as ANSI color names (e.g., "red", "bright-blue") or +# as hex values (e.g., "#ff0000", "#1e1e2e"). +# +# Example overrides: +# [ui.theme_overrides] +# background = "#000000" +# text_fg = "#ffffff" +# selection_bg = "#444444" +# match_fg = "#ff0000" + +# Keybindings and Events +# ---------------------------------------------------------------------------- +# +[keybindings] +# Application control +# ------------------ +esc = "quit" +ctrl-c = "quit" + +# Navigation and selection +# ----------------------- +down = "select_next_entry" +ctrl-n = "select_next_entry" +ctrl-j = "select_next_entry" +up = "select_prev_entry" +ctrl-p = "select_prev_entry" +ctrl-k = "select_prev_entry" + +# History navigation +# ----------------- +ctrl-up = "select_prev_history" +ctrl-down = "select_next_history" + +# Multi-selection +# -------------- +tab = "toggle_selection_down" +backtab = "toggle_selection_up" +enter = "confirm_selection" + +# Preview panel control +# -------------------- +pagedown = "scroll_preview_half_page_down" +pageup = "scroll_preview_half_page_up" +ctrl-f = "cycle_previews" + +# Data operations +# -------------- +ctrl-y = "copy_entry_to_clipboard" +ctrl-r = "reload_source" +ctrl-s = "cycle_sources" + +# UI Features +# ---------- +ctrl-t = "toggle_remote_control" +ctrl-x = "toggle_action_picker" +ctrl-o = "toggle_preview" +# ctrl-h/ctrl-e run the current channel's actions (files: goto_parent_dir, +# edit in nvim — see cable/files.toml); `end` still jumps to input end. +ctrl-h = "actions:goto_parent_dir" +f12 = "toggle_status_bar" +ctrl-l = "toggle_layout" + +# Input field actions +# ---------------------------------------- +backspace = "delete_prev_char" +ctrl-w = "delete_prev_word" +ctrl-u = "delete_line" +delete = "delete_next_char" +left = "go_to_prev_char" +right = "go_to_next_char" +home = "go_to_input_start" +ctrl-a = "go_to_input_start" +end = "go_to_input_end" +ctrl-e = "actions:edit" + +# Shell integration +# ---------------------------------------------------------------------------- +# +# The shell integration feature allows you to use television as a picker for +# your shell commands (as well as your shell history with ). +# E.g. typing `git checkout ` will open television with a list of +# branches to choose from. + +[shell_integration] +# This specifies the default fallback channel if no other channel is matched. +fallback_channel = "files" + +[shell_integration.channel_triggers] +# Add your channel triggers here. Each key is a channel that will be triggered +# by the corresponding commands. +# Example: say you want the following commands to trigger the following channels +# when pressing : +# `git checkout` should trigger the `git-branches` channel +# `ls` should trigger the `dirs` channel +# `cat` and `cp` should trigger the `files` channel +# +# You would add the following to your configuration file: +# ``` +# [shell_integration.channel_triggers] +# "git-branches" = ["git checkout"] +# "dirs" = ["ls"] +# "files" = ["cat", "cp"] +# ``` +"alias" = ["alias", "unalias"] +"env" = ["export", "unset"] +"dirs" = ["cd", "ls", "rmdir", "z"] +"files" = [ + "cat", + "less", + "head", + "tail", + "vim", + "nano", + "bat", + "cp", + "mv", + "rm", + "touch", + "chmod", + "chown", + "ln", + "tar", + "zip", + "unzip", + "gzip", + "gunzip", + "xz", +] +"git-diff" = ["git add", "git restore"] +"git-branch" = [ + "git checkout", + "git switch", + "git branch", + "git merge", + "git rebase", + "git pull", + "git push", +] +"git-log" = ["git log", "git show"] +"docker-images" = ["docker run"] +"git-repos" = ["nvim", "code", "hx", "git clone"] + + +[shell_integration.keybindings] +# controls which key binding should trigger tv +# for shell autocomplete +"smart_autocomplete" = "ctrl-t" +# controls which keybinding should trigger tv +# for command history +"command_history" = "ctrl-r" diff --git a/themes/cyberdream-transparent.toml b/themes/cyberdream-transparent.toml new file mode 100644 index 0000000..ef61c8d --- /dev/null +++ b/themes/cyberdream-transparent.toml @@ -0,0 +1,29 @@ +# cyberdream-transparent — https://github.com/scottmckendry/cyberdream.nvim +# Same palette as the kitty, fish, atuin, bat and yazi cyberdream themes. +# Differs from tv's builtin `cyberdream` (which shadows a user theme of that +# name): no `background`, so tv draws on kitty's own translucent #16181a +# (tv 0.15 rejects `background = 'none'` and falls back to the default theme). + +# general +border_fg = '#3c4048' +text_fg = '#ffffff' +dimmed_text_fg = '#7b8496' +# input +input_text_fg = '#ffffff' +result_count_fg = '#ffbd5e' +# results +result_name_fg = '#5ea1ff' +result_line_number_fg = '#f1ff5e' +result_value_fg = '#ffffff' +selection_fg = '#5eff6c' +selection_bg = '#3c4048' +match_fg = '#5ef1ff' +# preview +preview_title_fg = '#ffaecf' +# modes +channel_mode_fg = '#16181a' +channel_mode_bg = '#5ef1ff' +remote_control_mode_fg = '#16181a' +remote_control_mode_bg = '#5eff6c' +action_picker_mode_fg = '#16181a' +action_picker_mode_bg = '#bd5eff'