SLOPSHOPPER

slash-fuzzy

Fuzzy-find slash commands and skills in the '/' typeahead

newprompt
v0.2.0no licenseupdated 2026-10-11jackyyeh5111/dotfiles/claude/mods/slash-fuzzy
A shopper browsing a rack in a slop shop
README

dotfiles

Personal dotfiles + provisioning scripts for a brand new machine:

  • setup.sh — Debian/Ubuntu (apt)
  • setup.mac.sh — macOS (Homebrew)

Quickstart

git clone <this-repo-url> ~/dotfiles
cd ~/dotfiles

./setup.sh        # Debian/Ubuntu
./setup.mac.sh    # macOS

Both scripts are organized into one section per tool, check whether each tool is already installed before doing work again (safe to re-run), and back up (never delete) any existing config file before replacing it with a symlink. If any step fails, the script prints exactly which step, command, and line failed and stops — fix the issue and re-run it; completed steps are skipped automatically.

What setup.sh installs

  • Base packages: python3/pip, vim, htop, tmux, git, openssh-server, build tools
  • ripgrep, neofetch, fzf, bat, fd, xclip, astyle, eza
  • zsh + oh-my-zsh + zsh-autosuggestions
  • starship prompt, zoxide
  • Rust (rustup)
  • virtualenvwrapper
  • Docker (official convenience script)
  • Neovim (latest official release build, not the apt package)
  • yazi (latest official release build)
  • herdr
  • Claude Code
  • Symlinks all dotfiles into place (.zshrc, .vimrc, .tmux.conf, starship.toml, ghostty.config, herdr_config.toml, nvim/, yazi/*)
  • git config --global core.editor nvim
  • Best-effort bootstrap of nvim plugins (lazy.nvim) and yazi plugins (ya pkg install)

What setup.mac.sh installs

Same set, via Homebrew, minus what macOS already provides:

  • Homebrew itself (and the Xcode Command Line Tools check that precedes it)
  • Base packages: python3, git, tmux, vim, htop, wget
  • ripgrep, fastfetch, fzf, bat, fd, astyle, eza
  • oh-my-zsh + zsh-autosuggestions (the brew formula, which is what .zshrc.mac sources)
  • starship prompt, zoxide
  • Rust (rustup, so ~/.cargo/env exists for .zshrc.mac)
  • virtualenvwrapper (brew formula, so it lands in $(brew --prefix)/bin)
  • Docker Desktop (cask)
  • Neovim, yazi, herdr (brew), Claude Code (install script)
  • Ghostty (cask — unlike Linux, it's automated here)
  • Symlinks all dotfiles into place, with .zshrc.mac as ~/.zshrc
  • git config --global core.editor nvim
  • Best-effort bootstrap of nvim and yazi plugins

Differences from the Linux script, all deliberate:

  • No zsh, build tools, curl/unzip/gpg or xclip — macOS ships zsh and the clipboard is pbcopy; compilers come from the Command Line Tools.
  • No openssh-server — macOS has sshd built in, just toggled off.
  • No bat→batcat / fd→fdfind renames, so no ~/.local/bin shims.
  • neofetch is gone from homebrew-core (archived upstream); fastfetch takes its place.
  • The script warns at the end if .zshrc.mac's hardcoded /usr/local paths or /Users/jackyyeh home directory don't match this machine (they don't on Apple Silicon).

Manual steps

A few things are deliberately not automated because they're either interactive, machine/version-specific, or risky to guess at:

  • Default shell — if chsh didn't run automatically (e.g. non-interactive session), run: ``bash chsh -s $(command -v zsh) `` then log out and back in.
  • Docker group (Linux) — setup.sh adds you to the docker group, but that only takes effect after you log out/in (or run newgrp docker).
  • Docker Desktop (macOS) — setup.mac.sh installs the cask, but you must launch it once from /Applications before the docker CLI exists.
  • Remote Login (macOS) — macOS ships sshd but leaves it off; enable it in System Settings → General → Sharing → Remote Login.
  • Go — install from https://go.dev/dl/ (download the linux tarball and extract to /usr/local/go, matching the PATH entry already in .zshrc.linux).
  • Node.js — install nvm, then: ``bash nvm install --lts ` Note: .zshrc.linux also has a hardcoded PATH entry pointing at a specific past node version directory — harmless if it doesn't exist, just update it to match whatever nvm installs if you want node/npm always on PATH` without loading nvm.
  • Codex CLI — install per OpenAI's current instructions for the Codex CLI; left out here since the install method changes independently of this repo.
  • Ghostty (terminal app) — Linux only: install via your distro's package manager, snap, or from https://ghostty.org/download; setup.sh only symlinks ghostty.config into ~/.config/ghostty/config. On macOS setup.mac.sh installs the cask for you.
  • SSH key for GitHub — generate one and add it to your GitHub account if you'll be pushing over SSH: ``bash ssh-keygen -t ed25519 -C "your_email@example.com" ``
  • First nvim launch — setup.sh best-effort bootstraps plugins headlessly; if that step warned, just open nvim once and let lazy.nvim finish, then Mason will install LSP servers as configured in nvim/lua/plugins.lua.
Source 1 files
hooks/register.ts 83 lines
1import type { CommandInfo, PromptAutocompleteSuggestion, Register } from 'claude-code'
2
3const MAX_ROWS = 10
4
5// The fullscreen '/' menu is 5 lines tall, and a row whose description does
6// not fit beside its name wraps to 2 lines, so long skill descriptions leave
7// room for only 2 rows. Cut each to one short line so 5 rows fit.
8const MAX_DESC = 50
9
10// First sentence, at most MAX_DESC columns.
11// e.g. shorten('Writes a commit message. Use when...') -> 'Writes a commit message.'
12export const shorten = (d: string): string => {
13  const s = d.replace(/\s+/g, ' ').trim()
14  const end = s.search(/[.!?:](\s|$)/)
15  const first = end > 0 ? s.slice(0, end + 1) : s
16  return first.length <= MAX_DESC ? first : first.slice(0, MAX_DESC - 1).trimEnd() + '…'
17}
18
19// fzf-style subsequence score of `query` in `name`, or undefined when the
20// letters of `query` do not all appear in order. Higher is better: a run of
21// consecutive letters, a hit right after `-`/`:`/`_`/`.`, and a hit at the
22// start each earn a bonus; every skipped letter costs a little.
23//
24// e.g. score('cr', 'code-review') hits c(0) and r(5, after '-') -> scores;
25//      score('cr', 'clear') hits c(0) and r(4) -> lower, no boundary bonus.
26export const score = (query: string, name: string): number | undefined => {
27  const q = query.toLowerCase()
28  const n = name.toLowerCase()
29  let total = 0
30  let qi = 0
31  let prev = -2
32  for (let i = 0; i < n.length && qi < q.length; i++) {
33    if (n[i] !== q[qi]) continue
34    let s = 1
35    if (i === 0) s += 8
36    else if ('-:_./ '.includes(n[i - 1])) s += 6
37    if (i === prev + 1) s += 5
38    else if (prev >= 0) s -= Math.min(i - prev - 1, 3)
39    total += s
40    prev = i
41    qi++
42  }
43  if (qi < q.length) return undefined
44  // Prefer shorter names when the hits tie.
45  return total - n.length * 0.01
46}
47
48// The engine's own rows already cover names that start with the query; add
49// only the fuzzy hits it misses, best first.
50export const fuzzyRows = (query: string, commands: readonly CommandInfo[]): PromptAutocompleteSuggestion[] => {
51  const q = query.toLowerCase()
52  const seen = new Set<string>()
53  return commands
54    .filter(c => !c.name.toLowerCase().startsWith(q))
55    .map(c => ({ c, s: score(q, c.name) }))
56    .filter((x): x is { c: CommandInfo; s: number } => x.s !== undefined)
57    .sort((a, b) => b.s - a.s)
58    .filter(({ c }) => !seen.has(c.name) && !!seen.add(c.name))
59    .slice(0, MAX_ROWS)
60    .map(({ c }) => ({ text: `/${c.name} `, label: `/${c.name}`, description: shorten(c.description) }))
61}
62
63export const register: Register = on => {
64  // Descriptions are cached per session; drop the cache so a reload applies.
65  on('session.start', ($, e, next) => {
66    $.ui.invalidate('command.describe')
67    return next(e)
68  })
69
70  on('command.describe', async ($, e, next) => {
71    const r = await next(e)
72    return { ...r, description: shorten(r.description) }
73  })
74
75  on('prompt.autocomplete', { token: /^\/./ }, async ($, e, next) => {
76    const base = await next(e)
77    // Slash commands only count at the very start of the prompt.
78    if (e.text.slice(0, e.start).trim() !== '') return base
79    const rows = fuzzyRows(e.token.slice(1), await $.command.list())
80    return { suggestions: [...base.suggestions, ...rows] }
81  })
82}
83