Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 26 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
# AGENTS.md

## Scope

These notes apply to work inside this repository.

## Testing Rules

- Keep using the existing approval-test style in `test/approve` unless there is a strong reason to introduce a different layer of testing.
- Never edit approval fixtures in `test/approvals` manually.
- If behavior changes intentionally and a fixture update is needed, leave that to the human approval flow by running `test/approve`.
- If a test change causes an approval diff, keep the approval test in place, stop short of editing the fixture, and let the user run and approve it manually.
- Prefer adding direct assertions in `test/approve` when a regression can be covered without changing fixtures.

## Repo Utilities

- Useful repo commands are defined in `op.conf`.
- Run them as `op COMMAND`.
- Current commands include `op shellcheck`, `op shfmt`, `op codespell`, and `op test`.
- Preserve the existing shell style and test conventions.
- If temporary files or directories are needed, use `./tmp` only.
- Do not create temporary folders outside `./tmp`.
- Approval tests may need a temporary writable `HOME` in constrained environments; if so, place it under `./tmp`.
- Keep `cd -d` history rewrites tied to `FUZZYCD_HISTORY_FILE`; do not reintroduce fixed temp files under `$HOME`.
- Keep completion behavior aligned with this contract: fuzzy history matches first, native `cd` matches after, no duplicates, and preserve `cd` registration to `_fuzzycd_completions`.
- Installer behavior note: Bash completions may be installed automatically for Bash startup, but not for Zsh unless a native or explicitly supported compatibility path exists.
29 changes: 15 additions & 14 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,20 +38,17 @@ Fuzzy CD requires a recent version of [fzf].

### Installing using the setup script

This [setup](setup) script will download the `fuzzycd` function to either your system
bin root, or user bin root (whichever is writable) and place a startup file
in your `.bashrc.d` or `.zshrc.d` directory. Note that it will NOT change your
startup script, so you might need to ensure that your startup directories are
sources.
Use the [setup](setup) script for automatic installation. It installs
`fuzzycd` and adds the required shell startup hook.

```shell
$ curl -Ls get.dannyb.co/fuzzycd/setup | bash
```

### Installing manually

1. Place the `fuzzycd` file in `/usr/local/bin/` and make it executable
2. Source it (`source /usr/local/bin/fuzzycd`) from your startup script (for example: `~/.bashrc`)
1. Place the `fuzzycd` file somewhere on your `PATH` and make it executable.
2. Source it from your startup script (for example: `source /usr/local/bin/fuzzycd` in `~/.bashrc`).


## Usage
Expand Down Expand Up @@ -125,7 +122,10 @@ matching directories when running in interactive mode, or you will

## Bash completions

To enable fuzzy bash completions, add the following line to your `~/.bashrc`:
If you install using the [`setup`](setup) script, fuzzy bash completions are
enabled automatically for Bash.

If you install manually, add the following line to your `~/.bashrc`:

```bash
eval "$(fuzzycd -c)"
Expand All @@ -141,18 +141,19 @@ TAB: menu-complete

## Uninstall

You can either run the uninstall script:
Use the uninstall script for automatic removal:

```shell
$ curl -Ls get.dannyb.co/fuzzycd/uninstall | bash
```

Or remove manually:
Or remove it manually:

1. Remove the `source /usr/local/bin/fuzzycd` line from your `~/.bashrc`.
2. Delete `/usr/local/bin/fuzzycd`.
3. Optionally, delete the history file (`~/.fuzzycd-history`).
4. Restart your session.
1. Remove the line that sources `fuzzycd` from your startup script.
2. Remove the `eval "$(fuzzycd -c)"` line from your startup script, if you added it manually.
3. Delete the `fuzzycd` executable from your `PATH`.
4. Optionally, delete the history file (`~/.fuzzycd-history`).
5. Restart your session.


## Contributing / Support
Expand Down
49 changes: 43 additions & 6 deletions fuzzycd
Original file line number Diff line number Diff line change
Expand Up @@ -108,10 +108,18 @@ fuzzycd_run() {
}

_fzcd_delete_dir() {
local dir tmpfile status
dir="${1:-$PWD}"
grep -Fxv "$dir" "$histfile" >"$HOME/.fuzzycd-history.tmp"
cp "$HOME/.fuzzycd-history.tmp" "$histfile"
rm -f "$HOME/.fuzzycd-history.tmp"
tmpfile=$(mktemp "${histfile}.XXXXXX") || return 1

grep -Fxv "$dir" "$histfile" >"$tmpfile"
status=$?
if [[ $status -gt 1 ]]; then
rm -f "$tmpfile"
return "$status"
fi

mv "$tmpfile" "$histfile"
echo "deleted $dir"
}

Expand Down Expand Up @@ -155,9 +163,38 @@ fuzzycd_run() {
echo ' local cur=${COMP_WORDS[COMP_CWORD]}'
echo ' local histfile=${FUZZYCD_HISTORY_FILE:-"$HOME/.fuzzycd-history"}'
echo ' local count=${FUZZYCD_COMPLETIONS_COUNT:-10}'
echo ' _cd' # invoke original completions
echo ' [[ $cur =~ ^(/|\.) ]] && return'
echo ' COMPREPLY+=( $(fzf --filter "$cur" --exit-0 <"$histfile" | head -n$count) )'
echo ' local match existing found'
echo ' local -a fuzzy_matches native_matches'
echo ' if [[ $cur =~ ^(/|\.) ]]; then'
echo ' [[ $(complete -p cd 2>/dev/null) == *"_fuzzycd_completions"* ]] || complete -o nosort -F _fuzzycd_completions cd'
echo ' declare -F _cd >/dev/null && _cd'
echo ' return'
echo ' fi'
echo ' while IFS= read -r match; do'
echo ' fuzzy_matches+=( "$match" )'
echo ' done < <(fzf --filter "$cur" --exit-0 <"$histfile" | head -n "$count")'
echo ' if ! declare -F _cd >/dev/null && declare -F _completion_loader >/dev/null; then'
echo ' _completion_loader cd >/dev/null 2>&1 || true'
echo ' fi'
echo ' if declare -F _cd >/dev/null; then'
echo ' COMPREPLY=()'
echo ' _cd'
echo ' native_matches=( "${COMPREPLY[@]}" )'
echo ' fi'
echo ' [[ $(complete -p cd 2>/dev/null) == *"_fuzzycd_completions"* ]] || complete -o nosort -F _fuzzycd_completions cd'
echo ' COMPREPLY=()'
echo ' for match in "${fuzzy_matches[@]}" "${native_matches[@]}"; do'
echo ' [[ -n $match ]] || continue'
echo ' found=0'
echo ' for existing in "${COMPREPLY[@]}"; do'
echo ' if [[ $existing == "$match" ]]; then'
echo ' found=1'
echo ' break'
echo ' fi'
echo ' done'
echo ' [[ $found -eq 0 ]] && COMPREPLY+=( "$match" )'
echo ' done'
echo ' return 0'
echo '}'
echo 'complete -o nosort -F _fuzzycd_completions cd'
}
Expand Down
1 change: 1 addition & 0 deletions setup
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ ssi install bin https://github.com/DannyBen/fuzzycd/releases/latest/download/fuz
cat <<'EOF' | ssi install startup --shell bash --name fuzzycd.bashrc -
if command -v fuzzycd >/dev/null 2>&1; then
source "$(command -v fuzzycd)"
eval "$(fuzzycd -c)" # bash completions
fi
EOF

Expand Down
35 changes: 32 additions & 3 deletions test/approvals/cd_c
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,37 @@ _fuzzycd_completions() {
local cur=${COMP_WORDS[COMP_CWORD]}
local histfile=${FUZZYCD_HISTORY_FILE:-"$HOME/.fuzzycd-history"}
local count=${FUZZYCD_COMPLETIONS_COUNT:-10}
_cd
[[ $cur =~ ^(/|\.) ]] && return
COMPREPLY+=( $(fzf --filter "$cur" --exit-0 <"$histfile" | head -n$count) )
local match existing found
local -a fuzzy_matches native_matches
if [[ $cur =~ ^(/|\.) ]]; then
[[ $(complete -p cd 2>/dev/null) == *"_fuzzycd_completions"* ]] || complete -o nosort -F _fuzzycd_completions cd
declare -F _cd >/dev/null && _cd
return
fi
while IFS= read -r match; do
fuzzy_matches+=( "$match" )
done < <(fzf --filter "$cur" --exit-0 <"$histfile" | head -n "$count")
if ! declare -F _cd >/dev/null && declare -F _completion_loader >/dev/null; then
_completion_loader cd >/dev/null 2>&1 || true
fi
if declare -F _cd >/dev/null; then
COMPREPLY=()
_cd
native_matches=( "${COMPREPLY[@]}" )
fi
[[ $(complete -p cd 2>/dev/null) == *"_fuzzycd_completions"* ]] || complete -o nosort -F _fuzzycd_completions cd
COMPREPLY=()
for match in "${fuzzy_matches[@]}" "${native_matches[@]}"; do
[[ -n $match ]] || continue
found=0
for existing in "${COMPREPLY[@]}"; do
if [[ $existing == "$match" ]]; then
found=1
break
fi
done
[[ $found -eq 0 ]] && COMPREPLY+=( "$match" )
done
return 0
}
complete -o nosort -F _fuzzycd_completions cd
57 changes: 57 additions & 0 deletions test/approve
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,46 @@ context "when the shell is interactive"
it "shows completions function"
approve "cd -c"

it "handles spaces when _cd is unavailable"
mkdir -p "tmp/space dir"
printf '%s\n' "$PWD/tmp/space dir" > "$FUZZYCD_HISTORY_FILE"
eval "$(fuzzycd_run -c)"
unset -f _cd 2>/dev/null || true
COMP_WORDS=(cd space)
COMP_CWORD=1
COMPREPLY=()
_fuzzycd_completions
[[ ${#COMPREPLY[@]} == 1 ]] || fail "Expected one completion, got ${#COMPREPLY[@]}"
[[ "${COMPREPLY[0]}" == "$PWD/tmp/space dir" ]] || fail "Expected spaced path completion"
[[ "$(fuzzycd_run -c)" != *mapfile* ]] || fail "Expected completion function to avoid mapfile dependency"

it "puts fuzzy matches before standard cd completions and removes duplicates"
mkdir -p "tmp/fuzzy one" "tmp/fuzzy two" "tmp/native three"
printf '%s\n%s\n' "$PWD/tmp/fuzzy one" "$PWD/tmp/fuzzy two" > "$FUZZYCD_HISTORY_FILE"
eval "$(fuzzycd_run -c)"
_cd() { COMPREPLY=("$PWD/tmp/native three" "$PWD/tmp/fuzzy two"); }
COMP_WORDS=(cd fuzzy)
COMP_CWORD=1
COMPREPLY=()
_fuzzycd_completions
[[ ${#COMPREPLY[@]} == 3 ]] || fail "Expected three merged completions, got ${#COMPREPLY[@]}"
[[ "${COMPREPLY[0]}" == "$PWD/tmp/fuzzy one" ]] || fail "Expected first fuzzy match first"
[[ "${COMPREPLY[1]}" == "$PWD/tmp/fuzzy two" ]] || fail "Expected second fuzzy match second"
[[ "${COMPREPLY[2]}" == "$PWD/tmp/native three" ]] || fail "Expected native completion last"

it "keeps fuzzycd registered as the cd completion function"
printf '%s\n' "$PWD/tmp/one/two" > "$FUZZYCD_HISTORY_FILE"
eval "$(fuzzycd_run -c)"
_cd() {
COMPREPLY=("$PWD/tmp/one/two")
complete -F _cd cd
}
COMP_WORDS=(cd one)
COMP_CWORD=1
COMPREPLY=()
_fuzzycd_completions
[[ "$(complete -p cd)" == *"_fuzzycd_completions"* ]] || fail "Expected fuzzycd completion to remain registered"

describe "cd DIR"
it "adds it to history"
cd tmp/one/two > /dev/null
Expand Down Expand Up @@ -92,6 +132,23 @@ context "when the history file contains gone directories"
it "states that nothing was pruned if there is nothing to do"
approve "cd -p" "cd_p_nothing_pruned"


context "when using a custom history file path"
export FUZZYCD_HISTORY_FILE="$PWD/tmp/custom/history"
mkdir -p "$(dirname "$FUZZYCD_HISTORY_FILE")"
echo /usr/local/bin > "$FUZZYCD_HISTORY_FILE"
echo /usr/local/lib >> "$FUZZYCD_HISTORY_FILE"
rm -f "$HOME/.fuzzycd-history.tmp"

describe "cd -d DIR"
it "rewrites the configured history file without using a fixed HOME temp file"
cd -d /usr/local/bin > /dev/null
[[ ! -e "$HOME/.fuzzycd-history.tmp" ]] || fail "Expected no temp file in HOME"
[[ "$(cat "$FUZZYCD_HISTORY_FILE")" == "/usr/local/lib" ]] || fail "Expected custom history file to be rewritten"

export FUZZYCD_HISTORY_FILE="$PWD/tmp/histfile"


context "when CDPATH contains entries"
echo /usr/local/bin > "$FUZZYCD_HISTORY_FILE"
export CDPATH=".:/usr:/etc"
Expand Down
3 changes: 3 additions & 0 deletions test/approve_setup
Original file line number Diff line number Diff line change
Expand Up @@ -22,4 +22,7 @@ describe "cd is a function"
describe "~/.bashrc.d contains the source directive file"
[[ -f ~/.bashrc.d/fuzzycd.bashrc ]] || fail "Expected to find source directive in ~/.bashrc"

describe "~/.bashrc.d enables fuzzycd bash completions"
grep -q 'eval "$(fuzzycd -c)"' ~/.bashrc.d/fuzzycd.bashrc || fail "Expected bash startup file to enable completions"

../uninstall
Loading