From 68d1b7dc22adcd42d8cda591a8257ad34e98b6b3 Mon Sep 17 00:00:00 2001 From: Xevion Date: Mon, 29 Dec 2025 18:15:05 -0600 Subject: [PATCH] feat: add fzf abbreviation/alias search with Alt+A keybinding --- home/dot_config/fish/config.fish.tmpl | 6 + .../fish/functions/fzf_search_abbr.fish | 44 ++++ .../bin/executable_fzf-abbr-search.ts | 196 ++++++++++++++++++ 3 files changed, 246 insertions(+) create mode 100644 home/dot_config/fish/functions/fzf_search_abbr.fish create mode 100644 home/dot_local/bin/executable_fzf-abbr-search.ts diff --git a/home/dot_config/fish/config.fish.tmpl b/home/dot_config/fish/config.fish.tmpl index 144d9ba..0fb23b1 100644 --- a/home/dot_config/fish/config.fish.tmpl +++ b/home/dot_config/fish/config.fish.tmpl @@ -44,6 +44,12 @@ end # Load custom functions from ~/.config/fish/functions/ # (Fish does this automatically, no explicit sourcing needed) +# Custom keybindings +if functions -q fzf_search_abbr + bind \ea fzf_search_abbr # Alt+A: Search abbreviations/aliases + bind -M insert \ea fzf_search_abbr # Also bind in insert mode +end + # Load abbreviations if test -f ~/.config/fish/conf.d/abbr.fish source ~/.config/fish/conf.d/abbr.fish diff --git a/home/dot_config/fish/functions/fzf_search_abbr.fish b/home/dot_config/fish/functions/fzf_search_abbr.fish new file mode 100644 index 0000000..1a9a189 --- /dev/null +++ b/home/dot_config/fish/functions/fzf_search_abbr.fish @@ -0,0 +1,44 @@ +function fzf_search_abbr --description "Search Fish abbreviations, aliases, and functions with fzf" + # Use the Bun script to collect items + # Output format: name\texpansion\ttype\tdisplay + set -l result (fzf-abbr-search.ts | fzf \ + --ansi \ + --height=50% \ + --reverse \ + --delimiter='\t' \ + --with-nth=4 \ + --nth=1,2 \ + --prompt='Aliases/Abbrs > ' \ + --preview='echo {2}' \ + --preview-window=up:3:wrap \ + --expect='tab' \ + --header='Enter: insert name | Tab: insert expansion') + + # Handle cancellation - just repaint and return + if test $status -ne 0 -o -z "$result" + commandline -f repaint + return + end + + # First line is the key pressed, second line is the selected item + set -l lines (string split \n $result) + set -l key $lines[1] + set -l selected $lines[2] + + if test -n "$selected" + # Split by tab to get fields + set -l fields (string split \t $selected) + + if test "$key" = "tab" + # Insert expansion (field 2) + commandline -i $fields[2] + else + # Insert name (field 1) + commandline -i $fields[1] + end + end + + commandline -f repaint +end + + diff --git a/home/dot_local/bin/executable_fzf-abbr-search.ts b/home/dot_local/bin/executable_fzf-abbr-search.ts new file mode 100644 index 0000000..20020a9 --- /dev/null +++ b/home/dot_local/bin/executable_fzf-abbr-search.ts @@ -0,0 +1,196 @@ +#!/usr/bin/env bun + +/** + * fzf-abbr-search - Search shell abbreviations, aliases, and functions + * Output format: name\texpansion\ttype\tdisplay + */ + +import { $ } from "bun"; + +// ANSI color codes +const colors = { + name: "\x1b[36m", // Cyan + arrow: "\x1b[90m", // Gray + expansion: "\x1b[32m", // Green + type: "\x1b[33m", // Yellow + reset: "\x1b[0m", +}; + +interface Item { + name: string; + expansion: string; + type: "abbr" | "alias" | "func"; +} + +async function detectShell(): Promise { + const shell = process.env.SHELL || ""; + if (shell.includes("fish")) return "fish"; + if (shell.includes("zsh")) return "zsh"; + if (shell.includes("bash")) return "bash"; + return "bash"; // default +} + +async function getAllFishItems(): Promise { + const items: Item[] = []; + + try { + // Combine all Fish queries into one script for efficiency + const script = ` + # Output abbreviations + for line in (abbr --show) + echo "ABBR|$line" + end + + # Output aliases + for line in (alias) + echo "ALIAS|$line" + end + + # Output functions with descriptions + for func in (functions -n | string match -v '_*') + set -l desc (functions -D -v $func 2>/dev/null | tail -n1) + if test -n "$desc" + echo "FUNC|$func|$desc" + else + echo "FUNC|$func|$func" + end + end + `; + + const result = await $`fish -c ${script}`.quiet(); + const lines = result.text().trim().split("\n"); + + for (const line of lines) { + if (!line) continue; + + if (line.startsWith("ABBR|")) { + const abbrLine = line.slice(5); + const match = abbrLine.match(/^abbr -a -- (\S+) (.+)$/); + if (match) { + items.push({ + name: match[1], + expansion: match[2].replace(/^'|'$/g, ""), + type: "abbr", + }); + } + } else if (line.startsWith("ALIAS|")) { + const aliasLine = line.slice(6); + const match = aliasLine.match(/^alias (\S+) (.+)$/); + if (match) { + items.push({ + name: match[1], + expansion: match[2].replace(/^'|'$/g, ""), + type: "alias", + }); + } + } else if (line.startsWith("FUNC|")) { + const parts = line.slice(5).split("|", 2); + if (parts.length === 2) { + items.push({ + name: parts[0], + expansion: parts[1], + type: "func", + }); + } + } + } + } catch (e) { + // Fish not available + } + + return items; +} + +async function getBashZshAliases(): Promise { + const items: Item[] = []; + const shell = await detectShell(); + + if (shell === "fish") return []; // Already handled by getFishAliases + + try { + const cmd = shell === "zsh" ? "zsh" : "bash"; + const result = await $`${cmd} -i -c 'alias'`.quiet(); + const lines = result.text().trim().split("\n"); + + for (const line of lines) { + // Format: alias name='expansion' or alias name=expansion + const match = line.match(/^alias (\S+)=['"]?(.+?)['"]?$/); + if (match) { + items.push({ + name: match[1], + expansion: match[2], + type: "alias", + }); + } + } + } catch (e) { + // Shell not available or no aliases + } + + return items; +} + +async function getBashZshFunctions(): Promise { + const items: Item[] = []; + const shell = await detectShell(); + + if (shell === "fish") return []; // Already handled by getFishFunctions + + try { + const cmd = shell === "zsh" ? "zsh" : "bash"; + // List all functions (excluding internal ones starting with _) + const result = await $`${cmd} -i -c 'declare -F'`.quiet(); + const lines = result.text().trim().split("\n"); + + for (const line of lines) { + const match = line.match(/^declare -f (\S+)$/); + if (match && !match[1].startsWith("_")) { + items.push({ + name: match[1], + expansion: match[1], // Functions just show their name + type: "func", + }); + } + } + } catch (e) { + // Shell not available + } + + return items; +} + +async function collectAllItems(): Promise { + const shell = await detectShell(); + + if (shell === "fish") { + return await getAllFishItems(); + } else { + const [aliases, funcs] = await Promise.all([ + getBashZshAliases(), + getBashZshFunctions(), + ]); + return [...aliases, ...funcs]; + } +} + +function formatDisplay(item: Item): string { + const { name, expansion, type } = item; + return ( + `${colors.name}${name}${colors.reset} ` + + `${colors.arrow}=>${colors.reset} ` + + `${colors.expansion}${expansion}${colors.reset} ` + + `${colors.type}(${type})${colors.reset}` + ); +} + +async function main() { + const items = await collectAllItems(); + + // Output: name\texpansion\ttype\tdisplay + for (const item of items) { + const display = formatDisplay(item); + console.log(`${item.name}\t${item.expansion}\t${item.type}\t${display}`); + } +} + +main();