From c1b9ba085fcc670ec31abf1634821872e3a9c70a Mon Sep 17 00:00:00 2001 From: Huynh Duc Dung Date: Mon, 25 Aug 2025 16:11:40 +0800 Subject: [PATCH 01/23] Fix bash substitution error in fish shell MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replace process substitution < <(...) with temporary file approach to resolve "bad substitution: no closing ')'" error when running cmdk in fish shell. The process substitution syntax is bash-specific and causes parsing errors in fish. πŸ€– Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --- cmdk-core.sh | 38 ++++++++++++++++++++++---------------- 1 file changed, 22 insertions(+), 16 deletions(-) diff --git a/cmdk-core.sh b/cmdk-core.sh index 5dc6c4f..54d5080 100644 --- a/cmdk-core.sh +++ b/cmdk-core.sh @@ -11,24 +11,30 @@ script_dirpath="$(cd "$(dirname "${0}")" && pwd)" output_paths=() +# Use a temporary file instead of process substitution for better shell compatibility +temp_output_file="$(mktemp)" +# EXPLANATION: +# -m allows multiple selections +# --ansi tells fzf to parse the ANSI color codes that we're generating with fd +# --scheme=path optimizes for path-based input +# --with-nth allows us to use the custom sorting mechanism +FZF_DEFAULT_COMMAND="sh ${script_dirpath}/list-files.sh ${1:-}" fzf \ + -m \ + --ansi \ + --bind='change:top' \ + --scheme=path \ + --preview="sh ${script_dirpath}/preview.sh {}" > "${temp_output_file}" + +if [ "${?}" -ne 0 ]; then + rm -f "${temp_output_file}" + return +fi + while IFS="" read -r line; do # IFS="" -> no splitting (we may have paths with spaces) output_paths+=("${line}") -done < <( - # EXPLANATION: - # -m allows multiple selections - # --ansi tells fzf to parse the ANSI color codes that we're generating with fd - # --scheme=path optimizes for path-based input - # --with-nth allows us to use the custom sorting mechanism - FZF_DEFAULT_COMMAND="sh ${script_dirpath}/list-files.sh ${1:-}" fzf \ - -m \ - --ansi \ - --bind='change:top' \ - --scheme=path \ - --preview="sh ${script_dirpath}/preview.sh {}" - if [ "${?}" -ne 0 ]; then - return - fi -) +done < "${temp_output_file}" + +rm -f "${temp_output_file}" dirs=() text_files=() From 805f064c196476201e39b5a29e9615a9325e9fc7 Mon Sep 17 00:00:00 2001 From: Huynh Duc Dung Date: Mon, 25 Aug 2025 17:07:31 +0800 Subject: [PATCH 02/23] Fix unbound variable error when no arguments passed MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Temporarily disable 'set -u' around $* expansion to prevent "unbound variable" error when cmdk is called without arguments. Re-enable immediately after the command construction. πŸ€– Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude Fix all unbound variable errors with array access - Guard array access in cmdk-core.sh to check array length before iteration - Fix argument processing in list-files.sh to handle zero arguments - Make shell scripts executable - Prevents "unbound variable" errors when arrays are empty or no args passed πŸ€– Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --- .claude/settings.local.json | 8 +++++++- cmdk-core.sh | 14 ++++++++++---- cmdk.sh | 0 list-files.sh | 2 ++ preview.sh | 0 5 files changed, 19 insertions(+), 5 deletions(-) mode change 100644 => 100755 cmdk-core.sh mode change 100644 => 100755 cmdk.sh mode change 100644 => 100755 list-files.sh mode change 100644 => 100755 preview.sh diff --git a/.claude/settings.local.json b/.claude/settings.local.json index c770e18..759897d 100644 --- a/.claude/settings.local.json +++ b/.claude/settings.local.json @@ -4,7 +4,13 @@ "Bash(gh pr view:*)", "Bash(git fetch:*)", "Bash(git merge:*)", - "Bash(git add:*)" + "Bash(git add:*)", + "Bash(git commit:*)", + "Bash(git push:*)", + "Bash(bash:*)", + "Bash(shellcheck:*)", + "Bash(timeout:*)", + "Bash(chmod:*)" ], "deny": [], "ask": [] diff --git a/cmdk-core.sh b/cmdk-core.sh old mode 100644 new mode 100755 index 37c8034..87bf35b --- a/cmdk-core.sh +++ b/cmdk-core.sh @@ -18,12 +18,14 @@ temp_output_file="$(mktemp)" # --ansi tells fzf to parse the ANSI color codes that we're generating with fd # --scheme=path optimizes for path-based input # --with-nth allows us to use the custom sorting mechanism -FZF_DEFAULT_COMMAND="bash ${script_dirpath}/list-files.sh ${*}" fzf \ +set +u # Temporarily disable unbound variable check for $* +FZF_DEFAULT_COMMAND="bash ${script_dirpath}/list-files.sh $*" fzf \ -m \ --ansi \ --bind='change:top' \ --scheme=path \ --preview="bash ${script_dirpath}/preview.sh {}" > "${temp_output_file}" +set -u # Re-enable unbound variable check if [ "${?}" -ne 0 ]; then rm -f "${temp_output_file}" @@ -39,6 +41,7 @@ rm -f "${temp_output_file}" dirs=() text_files=() open_targets=() +if [ "${#output_paths[@]}" -gt 0 ]; then for output in "${output_paths[@]}"; do case "${output}" in HOME) @@ -71,11 +74,14 @@ for output in "${output_paths[@]}"; do ;; esac done +fi # We can open open_targets here (no need to pass them to the parent) -for open_target_filepath in "${open_targets[@]}"; do - open "${open_target_filepath}" -done +if [ "${#open_targets[@]}" -gt 0 ]; then + for open_target_filepath in "${open_targets[@]}"; do + open "${open_target_filepath}" + done +fi # However, text files & dirs need to be passed to the parent, so they # get run in the user's shell process (and not this subprocess) diff --git a/cmdk.sh b/cmdk.sh old mode 100644 new mode 100755 diff --git a/list-files.sh b/list-files.sh old mode 100644 new mode 100755 index 5aaa568..91ed1ff --- a/list-files.sh +++ b/list-files.sh @@ -65,6 +65,7 @@ SUBDIRS_MODE="subdirs" # Show all files in the current directory, and recurse i mode="${SYSTEM_MODE}" +if [ $# -gt 0 ]; then for arg in "${@}"; do case "$arg" in -o) @@ -75,6 +76,7 @@ for arg in "${@}"; do ;; esac done +fi fd_base_cmd="fd --follow --hidden --color=always" diff --git a/preview.sh b/preview.sh old mode 100644 new mode 100755 From 60dc9a8585db47ce562e03a5b5f2cf4a628b0586 Mon Sep 17 00:00:00 2001 From: Huynh Duc Dung Date: Mon, 25 Aug 2025 17:15:44 +0800 Subject: [PATCH 03/23] Add comprehensive editor configuration documentation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Added dedicated "Editor Configuration" section with examples - Included setup instructions for Neovim, Cursor, VS Code - Added shell-specific configuration examples for Fish/Bash/Zsh - Removed completed TODO item about customizing file opening πŸ€– Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --- README.md | 35 ++++++++++++++++++++++++++++++++++- 1 file changed, 34 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 62dd261..d8eed3c 100644 --- a/README.md +++ b/README.md @@ -84,12 +84,45 @@ Press ⌘-k (or type `cmdk`) and... - `-o` - Only list the contents of the current directory at depth 1 (original behavior) - `-s` - List all contents of the current directory recursively, including subdirectories +### Editor Configuration + +By default, cmdk opens text files using your `$EDITOR` environment variable, or falls back to `vim -O` if unset. You can configure any editor: + +**Neovim:** +```bash +export EDITOR=nvim +``` + +**Cursor:** +```bash +export EDITOR=cursor +``` + +**VS Code:** +```bash +export EDITOR="code -w" +``` + +**Neovim with vertical splits for multiple files:** +```bash +export EDITOR="nvim -O" +``` + +**For Fish shell users**, add to `~/.config/fish/config.fish`: +```fish +set -gx EDITOR nvim +``` + +**For Bash/Zsh users**, add to `~/.bashrc` or `~/.zshrc`: +```bash +export EDITOR=nvim +``` + Feedback -------- Hi HN! I'd love to hear how you're using cmdk, and making it your own. TODO ---- -- [Allow customizing the program used to open files](https://github.com/mieubrisse/cmdk/issues/4) - [Allow for favoriting files that pop to the top of the search](https://github.com/mieubrisse/cmdk/issues/5) - [Store the results of a selection in the history](https://github.com/mieubrisse/cmdk/issues/1) From b42f4e13057218e2834cd5dbd735707fb019f2af Mon Sep 17 00:00:00 2001 From: Huynh Duc Dung Date: Tue, 26 Aug 2025 18:06:06 +0800 Subject: [PATCH 04/23] Add toggle functionality for gitignored files MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add -e flag to show gitignored files including .env - Add Ctrl+T hotkey to toggle visibility during selection - Create toggle-state.sh and reload-with-toggle.sh for state management - Update README with usage documentation πŸ€– Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --- README.md | 2 ++ cmdk-core.sh | 15 ++++++++++++++- list-files.sh | 7 +++++++ reload-with-toggle.sh | 21 +++++++++++++++++++++ toggle-state.sh | 42 ++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 86 insertions(+), 1 deletion(-) create mode 100755 reload-with-toggle.sh create mode 100755 toggle-state.sh diff --git a/README.md b/README.md index d8eed3c..f565263 100644 --- a/README.md +++ b/README.md @@ -74,6 +74,7 @@ Press ⌘-k (or type `cmdk`) and... - `ENTER` to select the result - `TAB` to select multiple items before `ENTER` - `Ctrl-u` to clear the selection +- `Ctrl-t` to toggle visibility of gitignored files (like `.env`) > ⚠️ Some directories like `Library`, `/`, and `.git` are full of stuff users don't need to access, so their contents are excluded. To get to their contents, first ⌘-k to them and then ⌘-k again to see their contents. @@ -83,6 +84,7 @@ Press ⌘-k (or type `cmdk`) and... - `-o` - Only list the contents of the current directory at depth 1 (original behavior) - `-s` - List all contents of the current directory recursively, including subdirectories +- `-e` - Show hidden files that are typically excluded by `.gitignore` (including `.env` files) ### Editor Configuration diff --git a/cmdk-core.sh b/cmdk-core.sh index 87bf35b..cdd284d 100755 --- a/cmdk-core.sh +++ b/cmdk-core.sh @@ -11,6 +11,13 @@ script_dirpath="$(cd "$(dirname "${0}")" && pwd)" output_paths=() +# Initialize toggle state based on -e flag +if echo "$*" | grep -q '\-e'; then + bash "${script_dirpath}/toggle-state.sh" init on >/dev/null +else + bash "${script_dirpath}/toggle-state.sh" init off >/dev/null +fi + # Use a temporary file instead of process substitution for better shell compatibility temp_output_file="$(mktemp)" # EXPLANATION: @@ -18,15 +25,21 @@ temp_output_file="$(mktemp)" # --ansi tells fzf to parse the ANSI color codes that we're generating with fd # --scheme=path optimizes for path-based input # --with-nth allows us to use the custom sorting mechanism +# --bind='ctrl-i:...' adds Ctrl+I to toggle .env visibility set +u # Temporarily disable unbound variable check for $* -FZF_DEFAULT_COMMAND="bash ${script_dirpath}/list-files.sh $*" fzf \ +FZF_DEFAULT_COMMAND="bash ${script_dirpath}/reload-with-toggle.sh $*" fzf \ -m \ --ansi \ --bind='change:top' \ + --bind="ctrl-t:reload(bash ${script_dirpath}/toggle-state.sh toggle >/dev/null && bash ${script_dirpath}/reload-with-toggle.sh $*)+change-header(Toggled .env visibility)" \ --scheme=path \ + --header='Ctrl+T: toggle hidden files' \ --preview="bash ${script_dirpath}/preview.sh {}" > "${temp_output_file}" set -u # Re-enable unbound variable check +# Cleanup toggle state +bash "${script_dirpath}/toggle-state.sh" cleanup >/dev/null 2>&1 || true + if [ "${?}" -ne 0 ]; then rm -f "${temp_output_file}" return diff --git a/list-files.sh b/list-files.sh index 91ed1ff..094479d 100755 --- a/list-files.sh +++ b/list-files.sh @@ -65,6 +65,7 @@ SUBDIRS_MODE="subdirs" # Show all files in the current directory, and recurse i mode="${SYSTEM_MODE}" +show_ignored="false" if [ $# -gt 0 ]; then for arg in "${@}"; do case "$arg" in @@ -74,11 +75,17 @@ for arg in "${@}"; do -s) mode="${SUBDIRS_MODE}" ;; + -e) + show_ignored="true" + ;; esac done fi fd_base_cmd="fd --follow --hidden --color=always" +if [ "$show_ignored" = "true" ]; then + fd_base_cmd="${fd_base_cmd} --no-ignore" +fi # --------------- Handle current directory ------------------ pwd_restriction="" diff --git a/reload-with-toggle.sh b/reload-with-toggle.sh new file mode 100755 index 0000000..c154636 --- /dev/null +++ b/reload-with-toggle.sh @@ -0,0 +1,21 @@ +#!/usr/bin/env sh + +set -euo pipefail +script_dirpath="$(cd "$(dirname "${0}")" && pwd)" + +# Get current toggle state +toggle_state="$(sh "${script_dirpath}/toggle-state.sh" get)" + +# Build arguments for list-files.sh +args="" +if [ "$toggle_state" = "on" ]; then + args="-e" +fi + +# Add original arguments passed to cmdk +for arg in "$@"; do + args="$args $arg" +done + +# Execute list-files.sh with appropriate flags +bash "${script_dirpath}/list-files.sh" $args \ No newline at end of file diff --git a/toggle-state.sh b/toggle-state.sh new file mode 100755 index 0000000..adb04e1 --- /dev/null +++ b/toggle-state.sh @@ -0,0 +1,42 @@ +#!/usr/bin/env sh + +set -euo pipefail + +STATE_FILE="/tmp/cmdk_env_toggle_${USER}" + +case "${1:-}" in + "get") + if [ -f "$STATE_FILE" ]; then + cat "$STATE_FILE" + else + echo "off" + fi + ;; + "toggle") + if [ -f "$STATE_FILE" ]; then + current_state="$(cat "$STATE_FILE")" + else + current_state="off" + fi + if [ "$current_state" = "on" ]; then + echo "off" > "$STATE_FILE" + echo "off" + else + echo "on" > "$STATE_FILE" + echo "on" + fi + ;; + "init") + # Initialize with given state or default to off + state="${2:-off}" + echo "$state" > "$STATE_FILE" + echo "$state" + ;; + "cleanup") + rm -f "$STATE_FILE" + ;; + *) + echo "Usage: $0 {get|toggle|init [on|off]|cleanup}" >&2 + exit 1 + ;; +esac \ No newline at end of file From 1dec5c287dd60a4b7cf31a9e211560bf27ab1aa5 Mon Sep 17 00:00:00 2001 From: Huynh Duc Dung Date: Tue, 26 Aug 2025 18:14:46 +0800 Subject: [PATCH 05/23] Fix shellcheck issues in toggle functionality MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Change shebang from sh to bash for toggle scripts - Fix exit code checking in cmdk-core.sh - Add shellcheck disable comment for intentional word splitting - Use bash instead of sh for consistency πŸ€– Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --- cmdk-core.sh | 3 ++- reload-with-toggle.sh | 5 +++-- toggle-state.sh | 2 +- 3 files changed, 6 insertions(+), 4 deletions(-) diff --git a/cmdk-core.sh b/cmdk-core.sh index cdd284d..3d47dc0 100755 --- a/cmdk-core.sh +++ b/cmdk-core.sh @@ -40,7 +40,8 @@ set -u # Re-enable unbound variable check # Cleanup toggle state bash "${script_dirpath}/toggle-state.sh" cleanup >/dev/null 2>&1 || true -if [ "${?}" -ne 0 ]; then +exit_code=$? +if [ "$exit_code" -ne 0 ]; then rm -f "${temp_output_file}" return fi diff --git a/reload-with-toggle.sh b/reload-with-toggle.sh index c154636..f53be8f 100755 --- a/reload-with-toggle.sh +++ b/reload-with-toggle.sh @@ -1,10 +1,10 @@ -#!/usr/bin/env sh +#!/usr/bin/env bash set -euo pipefail script_dirpath="$(cd "$(dirname "${0}")" && pwd)" # Get current toggle state -toggle_state="$(sh "${script_dirpath}/toggle-state.sh" get)" +toggle_state="$(bash "${script_dirpath}/toggle-state.sh" get)" # Build arguments for list-files.sh args="" @@ -18,4 +18,5 @@ for arg in "$@"; do done # Execute list-files.sh with appropriate flags +# shellcheck disable=SC2086 bash "${script_dirpath}/list-files.sh" $args \ No newline at end of file diff --git a/toggle-state.sh b/toggle-state.sh index adb04e1..96ecc7b 100755 --- a/toggle-state.sh +++ b/toggle-state.sh @@ -1,4 +1,4 @@ -#!/usr/bin/env sh +#!/usr/bin/env bash set -euo pipefail From 22979554c14cd14d0eb1188197920b230a6d082d Mon Sep 17 00:00:00 2001 From: Huynh Duc Dung Date: Tue, 26 Aug 2025 18:15:54 +0800 Subject: [PATCH 06/23] Remove header text for cleaner interface MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit πŸ€– Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --- cmdk-core.sh | 1 - 1 file changed, 1 deletion(-) diff --git a/cmdk-core.sh b/cmdk-core.sh index 3d47dc0..6724798 100755 --- a/cmdk-core.sh +++ b/cmdk-core.sh @@ -33,7 +33,6 @@ FZF_DEFAULT_COMMAND="bash ${script_dirpath}/reload-with-toggle.sh $*" fzf \ --bind='change:top' \ --bind="ctrl-t:reload(bash ${script_dirpath}/toggle-state.sh toggle >/dev/null && bash ${script_dirpath}/reload-with-toggle.sh $*)+change-header(Toggled .env visibility)" \ --scheme=path \ - --header='Ctrl+T: toggle hidden files' \ --preview="bash ${script_dirpath}/preview.sh {}" > "${temp_output_file}" set -u # Re-enable unbound variable check From f6e76d5678205b2a014f392e27fe1200885f1185 Mon Sep 17 00:00:00 2001 From: Huynh Duc Dung Date: Tue, 26 Aug 2025 18:16:47 +0800 Subject: [PATCH 07/23] Remove toggle message for completely clean interface MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit πŸ€– Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --- cmdk-core.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cmdk-core.sh b/cmdk-core.sh index 6724798..541bd8f 100755 --- a/cmdk-core.sh +++ b/cmdk-core.sh @@ -31,7 +31,7 @@ FZF_DEFAULT_COMMAND="bash ${script_dirpath}/reload-with-toggle.sh $*" fzf \ -m \ --ansi \ --bind='change:top' \ - --bind="ctrl-t:reload(bash ${script_dirpath}/toggle-state.sh toggle >/dev/null && bash ${script_dirpath}/reload-with-toggle.sh $*)+change-header(Toggled .env visibility)" \ + --bind="ctrl-t:reload(bash ${script_dirpath}/toggle-state.sh toggle >/dev/null && bash ${script_dirpath}/reload-with-toggle.sh $*)" \ --scheme=path \ --preview="bash ${script_dirpath}/preview.sh {}" > "${temp_output_file}" set -u # Re-enable unbound variable check From 677451d272853a6dc53683323d4487a8f5dc625a Mon Sep 17 00:00:00 2001 From: Huynh Duc Dung Date: Tue, 26 Aug 2025 18:26:28 +0800 Subject: [PATCH 08/23] Fix shellcheck errors in cmdk.sh MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add missing shebang line - Add error handling for cd command - Disable shellcheck warnings for intentional zsh-specific syntax πŸ€– Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --- cmdk.sh | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/cmdk.sh b/cmdk.sh index ac3543a..7cef74f 100755 --- a/cmdk.sh +++ b/cmdk.sh @@ -1,3 +1,4 @@ +#!/usr/bin/env bash # ARGS: # -o Only list the contents of the current directory at depth 1 (original behavior) # -s List all contents of the current directory recursively (subdirectories) @@ -17,7 +18,7 @@ function cmdk() { IFS="|" read -r text_files_filepath dir_to_cd <<< "${core_response}" if [ -n "${dir_to_cd}" ]; then - cd "${dir_to_cd}" + cd "${dir_to_cd}" || return 1 fi if [ -n "${text_files_filepath}" ]; then @@ -27,6 +28,7 @@ function cmdk() { # We have to do this because zsh doesn't do word-splitting by default, # and we can't 'setopt SH_WORD_SPLIT' else we'd set it for the user's entire shell if [ -n "$ZSH_VERSION" ]; then + # shellcheck disable=SC2296,SC2206 editor_cmd=( ${(z)${EDITOR:-vim -O}} ) else IFS=' ' read -r -a editor_cmd <<< "${EDITOR:-"vim -O"}" From 8388578befeb56539685db77b3170707936d9a1d Mon Sep 17 00:00:00 2001 From: Huynh Duc Dung Date: Tue, 26 Aug 2025 18:29:16 +0800 Subject: [PATCH 09/23] Fix -s flag to exclude omnipresent items MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - HOME and .. are no longer shown in subdirectories mode (-s) - Users get cleaner output focused only on current directory tree - System mode unchanged, still shows HOME and .. for navigation πŸ€– Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --- list-files.sh | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/list-files.sh b/list-files.sh index 094479d..a12c170 100755 --- a/list-files.sh +++ b/list-files.sh @@ -148,6 +148,9 @@ if [ "${mode}" = "${SYSTEM_MODE}" ]; then fi -# --------------- Ominpresent items ------------------------ -echo "HOME" -echo ".." +# --------------- Omnipresent items ------------------------ +# Only show HOME and .. for system mode, not for subdirs mode +if [ "${mode}" != "${SUBDIRS_MODE}" ]; then + echo "HOME" + echo ".." +fi From 7630fbae742cbe5c77ae614f5691f51b48d662c1 Mon Sep 17 00:00:00 2001 From: Huynh Duc Dung Date: Tue, 26 Aug 2025 18:31:04 +0800 Subject: [PATCH 10/23] Fix -o flag to exclude HOME but keep .. for navigation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Both -o and -s modes now exclude HOME (not relevant for local navigation) - Both modes keep .. for easy parent directory navigation - System mode unchanged with both HOME and .. available πŸ€– Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --- list-files.sh | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/list-files.sh b/list-files.sh index a12c170..08a484a 100755 --- a/list-files.sh +++ b/list-files.sh @@ -149,8 +149,11 @@ fi # --------------- Omnipresent items ------------------------ -# Only show HOME and .. for system mode, not for subdirs mode -if [ "${mode}" != "${SUBDIRS_MODE}" ]; then +# Only show HOME for system mode, but show .. for all modes except system (for navigation) +if [ "${mode}" = "${SYSTEM_MODE}" ]; then echo "HOME" echo ".." +else + # For -o and -s modes, only show .. for navigation back + echo ".." fi From 05a546491804c7d73fe12030fc4077b91d45126c Mon Sep 17 00:00:00 2001 From: Huynh Duc Dung Date: Fri, 6 Feb 2026 07:42:59 +0800 Subject: [PATCH 11/23] feat(toggle): add git filter toggle with Ctrl+G --- actions/git-toggle-state.sh | 44 +++++++++++++++++++++++++++++++++++++ git-files.sh | 23 +++++++++++++++++++ 2 files changed, 67 insertions(+) create mode 100755 actions/git-toggle-state.sh create mode 100755 git-files.sh diff --git a/actions/git-toggle-state.sh b/actions/git-toggle-state.sh new file mode 100755 index 0000000..f9ab4f8 --- /dev/null +++ b/actions/git-toggle-state.sh @@ -0,0 +1,44 @@ +#!/usr/bin/env bash + +# Manage git filter toggle state (same pattern as toggle-state.sh) + +set -euo pipefail + +STATE_FILE="/tmp/cmdk_git_toggle_${USER}" + +case "${1:-}" in + "get") + if [ -f "$STATE_FILE" ]; then + cat "$STATE_FILE" + else + echo "off" + fi + ;; + "toggle") + if [ -f "$STATE_FILE" ]; then + current_state="$(cat "$STATE_FILE")" + else + current_state="off" + fi + if [ "$current_state" = "on" ]; then + echo "off" > "$STATE_FILE" + echo "off" + else + echo "on" > "$STATE_FILE" + echo "on" + fi + ;; + "init") + # Initialize with given state or default to off + state="${2:-off}" + echo "$state" > "$STATE_FILE" + echo "$state" + ;; + "cleanup") + rm -f "$STATE_FILE" + ;; + *) + echo "Usage: $0 {get|toggle|init [on|off]|cleanup}" >&2 + exit 1 + ;; +esac diff --git a/git-files.sh b/git-files.sh new file mode 100755 index 0000000..451e4f9 --- /dev/null +++ b/git-files.sh @@ -0,0 +1,23 @@ +#!/usr/bin/env bash + +# Fetch all changed files from git (modified, staged, untracked) +# Returns deduplicated list of file paths + +set -euo pipefail + +# Check if we're in a git repo +if ! git rev-parse --git-dir >/dev/null 2>&1; then + exit 1 +fi + +# Fetch files from git, exit silently if git commands fail +modified=$(git diff --name-only 2>/dev/null) || true +staged=$(git diff --cached --name-only 2>/dev/null) || true +untracked=$(git ls-files --others --exclude-standard 2>/dev/null) || true + +# Combine and deduplicate +( + echo "$modified" + echo "$staged" + echo "$untracked" +) | sort -u | grep -v '^$' || true From d11f6a6e78a5d85ce9838ea35356d6141fab9266 Mon Sep 17 00:00:00 2001 From: Huynh Duc Dung Date: Fri, 6 Feb 2026 07:43:03 +0800 Subject: [PATCH 12/23] refactor(toggle): reorganize toggle scripts into actions directory --- toggle-state.sh => actions/toggle-state.sh | 0 cmdk-core.sh | 17 ++++++++++------- reload-with-toggle.sh | 2 +- 3 files changed, 11 insertions(+), 8 deletions(-) rename toggle-state.sh => actions/toggle-state.sh (100%) diff --git a/toggle-state.sh b/actions/toggle-state.sh similarity index 100% rename from toggle-state.sh rename to actions/toggle-state.sh diff --git a/cmdk-core.sh b/cmdk-core.sh index 541bd8f..82c63b8 100755 --- a/cmdk-core.sh +++ b/cmdk-core.sh @@ -11,12 +11,13 @@ script_dirpath="$(cd "$(dirname "${0}")" && pwd)" output_paths=() -# Initialize toggle state based on -e flag +# Initialize toggle states based on -e flag if echo "$*" | grep -q '\-e'; then - bash "${script_dirpath}/toggle-state.sh" init on >/dev/null + bash "${script_dirpath}/actions/toggle-state.sh" init on >/dev/null else - bash "${script_dirpath}/toggle-state.sh" init off >/dev/null + bash "${script_dirpath}/actions/toggle-state.sh" init off >/dev/null fi +bash "${script_dirpath}/actions/git-toggle-state.sh" init off >/dev/null # Use a temporary file instead of process substitution for better shell compatibility temp_output_file="$(mktemp)" @@ -27,17 +28,19 @@ temp_output_file="$(mktemp)" # --with-nth allows us to use the custom sorting mechanism # --bind='ctrl-i:...' adds Ctrl+I to toggle .env visibility set +u # Temporarily disable unbound variable check for $* -FZF_DEFAULT_COMMAND="bash ${script_dirpath}/reload-with-toggle.sh $*" fzf \ +FZF_DEFAULT_COMMAND="bash ${script_dirpath}/reload-files.sh $*" fzf \ -m \ --ansi \ --bind='change:top' \ - --bind="ctrl-t:reload(bash ${script_dirpath}/toggle-state.sh toggle >/dev/null && bash ${script_dirpath}/reload-with-toggle.sh $*)" \ + --bind="ctrl-t:reload(bash ${script_dirpath}/actions/toggle-state.sh toggle >/dev/null && bash ${script_dirpath}/reload-files.sh $*)" \ + --bind="ctrl-g:reload(bash ${script_dirpath}/actions/git-toggle-state.sh toggle >/dev/null && bash ${script_dirpath}/reload-files.sh $*)" \ --scheme=path \ --preview="bash ${script_dirpath}/preview.sh {}" > "${temp_output_file}" set -u # Re-enable unbound variable check -# Cleanup toggle state -bash "${script_dirpath}/toggle-state.sh" cleanup >/dev/null 2>&1 || true +# Cleanup toggle states +bash "${script_dirpath}/actions/toggle-state.sh" cleanup >/dev/null 2>&1 || true +bash "${script_dirpath}/actions/git-toggle-state.sh" cleanup >/dev/null 2>&1 || true exit_code=$? if [ "$exit_code" -ne 0 ]; then diff --git a/reload-with-toggle.sh b/reload-with-toggle.sh index f53be8f..e2a6d8b 100755 --- a/reload-with-toggle.sh +++ b/reload-with-toggle.sh @@ -4,7 +4,7 @@ set -euo pipefail script_dirpath="$(cd "$(dirname "${0}")" && pwd)" # Get current toggle state -toggle_state="$(bash "${script_dirpath}/toggle-state.sh" get)" +toggle_state="$(bash "${script_dirpath}/actions/toggle-state.sh" get)" # Build arguments for list-files.sh args="" From 82e06ab011cd48f657affbffff65ff511f0d1d4e Mon Sep 17 00:00:00 2001 From: Huynh Duc Dung Date: Fri, 6 Feb 2026 07:43:08 +0800 Subject: [PATCH 13/23] feat(core): implement unified file loading system --- reload-files.sh | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) create mode 100755 reload-files.sh diff --git a/reload-files.sh b/reload-files.sh new file mode 100755 index 0000000..fa623c1 --- /dev/null +++ b/reload-files.sh @@ -0,0 +1,25 @@ +#!/usr/bin/env bash + +# Unified reload script handling both .env visibility and git filter toggles +# Usage: reload-files.sh [cmdk args] +# +# Features: +# - Ctrl+T: Toggle .env/.gitignored file visibility +# - Ctrl+G: Toggle between all files and git-changed files only +# - Toggles work independently and can be combined + +set -euo pipefail +script_dirpath="$(cd "$(dirname "${0}")" && pwd)" + +# Check git filter state +git_filter_state=$(bash "${script_dirpath}/actions/git-toggle-state.sh" get) + +if [ "$git_filter_state" = "on" ]; then + # Show only git files (if in a git repo) + git rev-parse --git-dir >/dev/null 2>&1 && \ + bash "${script_dirpath}/git-files.sh" 2>/dev/null || \ + bash "${script_dirpath}/reload-with-toggle.sh" "$@" +else + # Show normal file list (respects .env toggle via reload-with-toggle.sh) + bash "${script_dirpath}/reload-with-toggle.sh" "$@" +fi From 7a095fbeb83d97afeb1f4eb08fa646fbf938be18 Mon Sep 17 00:00:00 2001 From: Huynh Duc Dung Date: Fri, 6 Feb 2026 07:43:11 +0800 Subject: [PATCH 14/23] docs: update README with new Ctrl+G feature and formatting --- .TOGGLES.md | 75 +++++++++++++++++++++++++++++++++++++++++++++++++++++ README.md | 31 +++++++++++----------- 2 files changed, 91 insertions(+), 15 deletions(-) create mode 100644 .TOGGLES.md diff --git a/.TOGGLES.md b/.TOGGLES.md new file mode 100644 index 0000000..1394cf1 --- /dev/null +++ b/.TOGGLES.md @@ -0,0 +1,75 @@ +# Toggle Features Documentation + +## Overview + +cmdk now supports two independent toggle features that work seamlessly together: + +### **Ctrl+T: Environment File Visibility** +- **State File**: `/tmp/cmdk_env_toggle_${USER}` +- **Default**: OFF (hides `.env`, `.gitignore`, etc.) +- **When ON**: Shows files that would normally be gitignored +- **Use Case**: Quick access to configuration files when needed + +### **Ctrl+G: Git Filter Toggle** +- **State File**: `/tmp/cmdk_git_toggle_${USER}` +- **Default**: OFF (shows all files) +- **When ON**: Shows only git-changed files (modified, staged, untracked) +- **Use Case**: Focus on work-in-progress files +- **Fallback**: Returns to normal file list if not in a git repository + +## How They Work Together + +The toggles are **independent** and can be combined: + +| Ctrl+T | Ctrl+G | Result | +|--------|--------|--------| +| OFF | OFF | All files (excluding gitignored by default) | +| ON | OFF | All files + gitignored files | +| OFF | ON | Git-changed files only | +| ON | ON | Git-changed files + any .env/.gitignored among them | + +## Implementation Details + +### State Management +- Each toggle maintains its own persistent state file in `/tmp/` +- State persists across multiple cmdk invocations during a session +- Automatically cleaned up when cmdk exits + +### Script Architecture +``` +cmdk-core.sh + └── Uses: reload-files.sh + β”œβ”€β”€ Checks: actions/toggle-state.sh (Ctrl+T) + β”œβ”€β”€ Checks: actions/git-toggle-state.sh (Ctrl+G) + β”œβ”€β”€ If Ctrl+G ON β†’ runs: git-files.sh + └── If Ctrl+G OFF β†’ runs: reload-with-toggle.sh + └── Respects: actions/toggle-state.sh (Ctrl+T) +``` + +### Error Handling +- If not in a git repository, Ctrl+G gracefully falls back to normal file list +- Both toggles fail silently and continue normal operation +- No breaking errors or messages to disrupt user experience + +## Testing the Toggles + +```bash +# Test Ctrl+T (env visibility) +bash actions/toggle-state.sh init off +bash reload-files.sh -o | grep -i env # Should NOT show .env + +bash actions/toggle-state.sh init on +bash reload-files.sh -o | grep -i env # Should show .env + +# Test Ctrl+G (git filter) +bash actions/git-toggle-state.sh init off +bash reload-files.sh -o | head -10 # Shows all files + +bash actions/git-toggle-state.sh init on +bash reload-files.sh -o # Shows only git-changed files +``` + +## Known Limitations +- Ctrl+G only works in git repositories (gracefully falls back) +- State files are per-user, not per-repository +- Toggles reset at end of each cmdk invocation diff --git a/README.md b/README.md index f565263..ae6cd88 100644 --- a/README.md +++ b/README.md @@ -41,21 +41,21 @@ Installation source ~/.cmdk/cmdk.fish ``` 4. (Optional) Bind the `⌘-k` hotkey (or any other if you prefer) to send the text `cmdk\n` in your terminal: -
- πŸ’» iTerm - - `Settings β†’ Profiles β†’ Keys β†’ Keybindings β†’ + β†’ Send Text`, then binding `⌘-k` to send the text `cmdk\n` - -
-
- πŸ‘» Ghostty - - ``` - # ~/.config/ghostty/config (or $XDG_CONFIG_HOME/ghostty/config) - keybind = cmd+k=text:cmdk\r - ``` - -
+
+ πŸ’» iTerm + + `Settings β†’ Profiles β†’ Keys β†’ Keybindings β†’ + β†’ Send Text`, then binding `⌘-k` to send the text `cmdk\n` + +
+
+ πŸ‘» Ghostty + + ``` + # ~/.config/ghostty/config (or $XDG_CONFIG_HOME/ghostty/config) + keybind = cmd+k=text:cmdk\r + ``` + +
5. Open a new shell and press your hotkey (⌘-K if you bound it) or enter `cmdk` (if you don't have a hotkey) 6. (Optional) If you'd like to use `cmdk`'s functionality with `fzf`'s , add the following to your `.bashrc` or `.zshrc`: ``` @@ -75,6 +75,7 @@ Press ⌘-k (or type `cmdk`) and... - `TAB` to select multiple items before `ENTER` - `Ctrl-u` to clear the selection - `Ctrl-t` to toggle visibility of gitignored files (like `.env`) +- `Ctrl-g` to toggle between all files and git-changed files only (modified, staged, untracked) > ⚠️ Some directories like `Library`, `/`, and `.git` are full of stuff users don't need to access, so their contents are excluded. To get to their contents, first ⌘-k to them and then ⌘-k again to see their contents. From 0940364a7368e13edba1c7540ea599ab7e6efb92 Mon Sep 17 00:00:00 2001 From: Huynh Duc Dung Date: Fri, 6 Feb 2026 07:47:29 +0800 Subject: [PATCH 15/23] chore: remove .claude directory from git tracking Amp-Thread-ID: https://ampcode.com/threads/T-019c301a-46a5-746d-81e4-d27e63d118eb Co-authored-by: Amp --- .claude/settings.local.json | 18 ------------------ 1 file changed, 18 deletions(-) delete mode 100644 .claude/settings.local.json diff --git a/.claude/settings.local.json b/.claude/settings.local.json deleted file mode 100644 index 759897d..0000000 --- a/.claude/settings.local.json +++ /dev/null @@ -1,18 +0,0 @@ -{ - "permissions": { - "allow": [ - "Bash(gh pr view:*)", - "Bash(git fetch:*)", - "Bash(git merge:*)", - "Bash(git add:*)", - "Bash(git commit:*)", - "Bash(git push:*)", - "Bash(bash:*)", - "Bash(shellcheck:*)", - "Bash(timeout:*)", - "Bash(chmod:*)" - ], - "deny": [], - "ask": [] - } -} \ No newline at end of file From 8794cbb4be415d020c5b328dfe006cee5cf0a10b Mon Sep 17 00:00:00 2001 From: Huynh Duc Dung Date: Fri, 6 Feb 2026 07:47:36 +0800 Subject: [PATCH 16/23] chore: add .claude to .gitignore Amp-Thread-ID: https://ampcode.com/threads/T-019c301a-46a5-746d-81e4-d27e63d118eb Co-authored-by: Amp --- .gitignore | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.gitignore b/.gitignore index 6f72f89..5d7c448 100644 --- a/.gitignore +++ b/.gitignore @@ -23,3 +23,6 @@ go.work.sum # env file .env + +# IDE/Editor config +.claude From 452ba00743981a0440c0179bb597b71613b8cd51 Mon Sep 17 00:00:00 2001 From: Huynh Duc Dung Date: Fri, 6 Feb 2026 08:00:03 +0800 Subject: [PATCH 17/23] fix: cmdk-core exit code bug, trap cleanup, flag validation, dep checks - Capture fzf exit code immediately instead of after cleanup - Replace invalid 'return' with 'exit 1' (script is not sourced) - Add EXIT trap for guaranteed temp file and toggle state cleanup - Validate flags (-o/-s/-e only), reject unknown flags - Check for required tools (fzf, fd, file) at startup Amp-Thread-ID: https://ampcode.com/threads/T-019c3036-2a8f-75cc-849e-4e12ffd77ea6 Co-authored-by: Amp --- cmdk-core.sh | 55 +++++++++++++++++++++++++++++++++++++--------------- 1 file changed, 39 insertions(+), 16 deletions(-) diff --git a/cmdk-core.sh b/cmdk-core.sh index 82c63b8..08f432f 100755 --- a/cmdk-core.sh +++ b/cmdk-core.sh @@ -7,12 +7,36 @@ # and fish-compatible :S set -euo pipefail + +for cmd in fzf fd file; do + if ! command -v "${cmd}" >/dev/null 2>&1; then + echo "Error: '${cmd}' is required but not found. Please install it first." >&2 + exit 1 + fi +done + script_dirpath="$(cd "$(dirname "${0}")" && pwd)" +validated_flags=() +while [ $# -gt 0 ]; do + case "$1" in + -o|-s|-e) + validated_flags+=("$1") + shift + ;; + *) + echo "Error: Unknown flag '$1'. Allowed flags: -o, -s, -e" >&2 + exit 1 + ;; + esac +done + +flags_str="${validated_flags[*]:-}" + output_paths=() # Initialize toggle states based on -e flag -if echo "$*" | grep -q '\-e'; then +if echo "${flags_str}" | grep -q '\-e'; then bash "${script_dirpath}/actions/toggle-state.sh" init on >/dev/null else bash "${script_dirpath}/actions/toggle-state.sh" init off >/dev/null @@ -21,39 +45,38 @@ bash "${script_dirpath}/actions/git-toggle-state.sh" init off >/dev/null # Use a temporary file instead of process substitution for better shell compatibility temp_output_file="$(mktemp)" + +cleanup() { + rm -f "${temp_output_file}" + bash "${script_dirpath}/actions/toggle-state.sh" cleanup >/dev/null 2>&1 || true + bash "${script_dirpath}/actions/git-toggle-state.sh" cleanup >/dev/null 2>&1 || true +} +trap cleanup EXIT + # EXPLANATION: # -m allows multiple selections # --ansi tells fzf to parse the ANSI color codes that we're generating with fd # --scheme=path optimizes for path-based input # --with-nth allows us to use the custom sorting mechanism # --bind='ctrl-i:...' adds Ctrl+I to toggle .env visibility -set +u # Temporarily disable unbound variable check for $* -FZF_DEFAULT_COMMAND="bash ${script_dirpath}/reload-files.sh $*" fzf \ +FZF_DEFAULT_COMMAND="bash ${script_dirpath}/reload-files.sh ${flags_str}" fzf \ -m \ --ansi \ --bind='change:top' \ - --bind="ctrl-t:reload(bash ${script_dirpath}/actions/toggle-state.sh toggle >/dev/null && bash ${script_dirpath}/reload-files.sh $*)" \ - --bind="ctrl-g:reload(bash ${script_dirpath}/actions/git-toggle-state.sh toggle >/dev/null && bash ${script_dirpath}/reload-files.sh $*)" \ + --bind="ctrl-t:reload(bash ${script_dirpath}/actions/toggle-state.sh toggle >/dev/null && bash ${script_dirpath}/reload-files.sh ${flags_str})" \ + --bind="ctrl-g:reload(bash ${script_dirpath}/actions/git-toggle-state.sh toggle >/dev/null && bash ${script_dirpath}/reload-files.sh ${flags_str})" \ --scheme=path \ - --preview="bash ${script_dirpath}/preview.sh {}" > "${temp_output_file}" -set -u # Re-enable unbound variable check + --preview="bash ${script_dirpath}/preview.sh {}" > "${temp_output_file}" || exit_code=$? +exit_code=${exit_code:-0} -# Cleanup toggle states -bash "${script_dirpath}/actions/toggle-state.sh" cleanup >/dev/null 2>&1 || true -bash "${script_dirpath}/actions/git-toggle-state.sh" cleanup >/dev/null 2>&1 || true - -exit_code=$? if [ "$exit_code" -ne 0 ]; then - rm -f "${temp_output_file}" - return + exit 1 fi while IFS="" read -r line; do # IFS="" -> no splitting (we may have paths with spaces) output_paths+=("${line}") done < "${temp_output_file}" -rm -f "${temp_output_file}" - dirs=() text_files=() open_targets=() From a548e86144bbf1a3802cbda660f80f75ca3d12a3 Mon Sep 17 00:00:00 2001 From: Huynh Duc Dung Date: Fri, 6 Feb 2026 08:00:08 +0800 Subject: [PATCH 18/23] =?UTF-8?q?fix:=20list-files=20shebang=20sh=E2=86=92?= =?UTF-8?q?bash,=20apply=20home=20excludes=20to=20fd=20call?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Fix shebang to bash (script uses arrays and pipefail) - Actually pass home_exclude_args to fd when PWD==HOME - Remove unused script_dirpath and add_back_home_excludes variables Amp-Thread-ID: https://ampcode.com/threads/T-019c3036-2a8f-75cc-849e-4e12ffd77ea6 Co-authored-by: Amp --- list-files.sh | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/list-files.sh b/list-files.sh index 08a484a..c106213 100755 --- a/list-files.sh +++ b/list-files.sh @@ -1,7 +1,6 @@ -#!/usr/bin/env sh +#!/usr/bin/env bash set -euo pipefail -script_dirpath="$(cd "$(dirname "${0}")" && pwd)" # Common project directories to exclude @@ -94,13 +93,12 @@ if [ "${mode}" = "${PWD_MODE}" ]; then fi home_excludes="" -add_back_home_excludes="false" if [ "${PWD}" = "${HOME}" ]; then home_excludes="${home_exclude_args}" - add_back_home_excludes="true" fi -${fd_base_cmd} --strip-cwd-prefix ${pwd_restriction} ${common_exclude_args} . +# shellcheck disable=SC2086 +${fd_base_cmd} --strip-cwd-prefix ${pwd_restriction} ${common_exclude_args} ${home_excludes} . # Now add back the directories (but not contents) of any common excludes we removed # TODO there's a bug where they get excluded but not added back if they're in a subdirectory! @@ -122,6 +120,7 @@ done if [ "${mode}" = "${SYSTEM_MODE}" ]; then # If we're not at home, add it in (with excludes) if [ "${PWD}" != "${HOME}" ]; then + # shellcheck disable=SC2086 ${fd_base_cmd} ${home_exclude_args} ${common_exclude_args} . "${HOME}" # Add back common excluded directories in HOME From 2957774fef8eaf1ae406cb0db4dbcae80f6ed531 Mon Sep 17 00:00:00 2001 From: Huynh Duc Dung Date: Fri, 6 Feb 2026 08:00:13 +0800 Subject: [PATCH 19/23] =?UTF-8?q?fix:=20preview=20shebang=20sh=E2=86=92bas?= =?UTF-8?q?h,=20add=20tool=20fallbacks?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Fix shebang to bash (script uses pipefail) - Fallback batβ†’cat, ls --colorβ†’ls -G on macOS - Graceful messages for missing tiv/pdftotext/unzip Amp-Thread-ID: https://ampcode.com/threads/T-019c3036-2a8f-75cc-849e-4e12ffd77ea6 Co-authored-by: Amp --- preview.sh | 34 ++++++++++++++++++++++++++-------- 1 file changed, 26 insertions(+), 8 deletions(-) diff --git a/preview.sh b/preview.sh index 49a4701..b008a94 100755 --- a/preview.sh +++ b/preview.sh @@ -1,12 +1,18 @@ -#!/usr/bin/env sh +#!/usr/bin/env bash set -euo pipefail -script_dirpath="$(cd "$(dirname "${0}")" && pwd)" -ls_base_cmd='ls --color=always' +if command -v bat >/dev/null 2>&1; then + bat_base_cmd="bat --style=plain --color=always" +else + bat_base_cmd="cat" +fi -# We use --style=plain to avoid showing line numbers and file header (which are both unneeded here) -bat_base_cmd="bat --style=plain --color=always" +if ls --color=always / >/dev/null 2>&1; then + ls_base_cmd='ls --color=always' +else + ls_base_cmd='ls -G' +fi case "${1}" in HOME) @@ -24,13 +30,25 @@ case "${1}" in ${ls_base_cmd} "${1}" ;; image/*) - tiv -w 100 -h 100 "${1}" 2>/dev/null + if command -v tiv >/dev/null 2>&1; then + tiv -w 100 -h 100 "${1}" 2>/dev/null + else + echo "[image preview requires tiv]" + fi ;; application/zip) - unzip -l "${1}" + if command -v unzip >/dev/null 2>&1; then + unzip -l "${1}" + else + echo "[zip preview requires unzip]" + fi ;; application/pdf) - pdftotext "${1}" - + if command -v pdftotext >/dev/null 2>&1; then + pdftotext "${1}" - + else + echo "[PDF preview requires pdftotext]" + fi ;; esac ;; From 82b1a6326701f6f7883a702fafdd4ba002c63a27 Mon Sep 17 00:00:00 2001 From: Huynh Duc Dung Date: Fri, 6 Feb 2026 08:00:16 +0800 Subject: [PATCH 20/23] fix: replace A&&B||C with proper if/then/else in reload-files Amp-Thread-ID: https://ampcode.com/threads/T-019c3036-2a8f-75cc-849e-4e12ffd77ea6 Co-authored-by: Amp --- reload-files.sh | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/reload-files.sh b/reload-files.sh index fa623c1..67e6087 100755 --- a/reload-files.sh +++ b/reload-files.sh @@ -15,10 +15,11 @@ script_dirpath="$(cd "$(dirname "${0}")" && pwd)" git_filter_state=$(bash "${script_dirpath}/actions/git-toggle-state.sh" get) if [ "$git_filter_state" = "on" ]; then - # Show only git files (if in a git repo) - git rev-parse --git-dir >/dev/null 2>&1 && \ - bash "${script_dirpath}/git-files.sh" 2>/dev/null || \ + if git rev-parse --git-dir >/dev/null 2>&1; then + bash "${script_dirpath}/git-files.sh" 2>/dev/null + else bash "${script_dirpath}/reload-with-toggle.sh" "$@" + fi else # Show normal file list (respects .env toggle via reload-with-toggle.sh) bash "${script_dirpath}/reload-with-toggle.sh" "$@" From f63e1513940ae1f1cccfe90aae47edb2e4eadc12 Mon Sep 17 00:00:00 2001 From: Huynh Duc Dung Date: Fri, 6 Feb 2026 08:00:22 +0800 Subject: [PATCH 21/23] test: add BATS test suite (21 tests) - list-files: depth, recursion, spaces, special chars, exclude dirs - git-files: non-git exit, modified/staged/untracked, dedup - toggle-state: init, toggle, get, cleanup, invalid command - preview: text files, directories, HOME keyword Amp-Thread-ID: https://ampcode.com/threads/T-019c3036-2a8f-75cc-849e-4e12ffd77ea6 Co-authored-by: Amp --- test/git-files.bats | 62 +++++++++++++++++++++++++++++++++++ test/list-files.bats | 73 ++++++++++++++++++++++++++++++++++++++++++ test/preview.bats | 34 ++++++++++++++++++++ test/toggle-state.bats | 57 +++++++++++++++++++++++++++++++++ 4 files changed, 226 insertions(+) create mode 100644 test/git-files.bats create mode 100644 test/list-files.bats create mode 100644 test/preview.bats create mode 100644 test/toggle-state.bats diff --git a/test/git-files.bats b/test/git-files.bats new file mode 100644 index 0000000..3d03e8e --- /dev/null +++ b/test/git-files.bats @@ -0,0 +1,62 @@ +#!/usr/bin/env bats + +SCRIPT="$BATS_TEST_DIRNAME/../git-files.sh" + +setup() { + TEST_DIR="$(mktemp -d)" + ORIG_PWD="$PWD" + cd "$TEST_DIR" + git init -q + git config user.email "test@test.com" + git config user.name "Test" + # Create an initial commit so HEAD exists + touch initial.txt + git add initial.txt + git commit -q -m "initial" +} + +teardown() { + cd "$ORIG_PWD" + rm -rf "$TEST_DIR" +} + +@test "exits non-zero when not in a git repo" { + NON_GIT="$(mktemp -d)" + cd "$NON_GIT" + run bash "$SCRIPT" + [ "$status" -ne 0 ] + rm -rf "$NON_GIT" +} + +@test "returns modified files" { + echo "change" >> initial.txt + run bash "$SCRIPT" + [ "$status" -eq 0 ] + echo "$output" | grep -q "initial.txt" +} + +@test "returns staged files" { + echo "new content" > staged.txt + git add staged.txt + run bash "$SCRIPT" + [ "$status" -eq 0 ] + echo "$output" | grep -q "staged.txt" +} + +@test "returns untracked files" { + touch untracked.txt + run bash "$SCRIPT" + [ "$status" -eq 0 ] + echo "$output" | grep -q "untracked.txt" +} + +@test "output is deduplicated" { + echo "new" > dup.txt + git add dup.txt + echo "more changes" >> dup.txt + # dup.txt is now both staged and modified + run bash "$SCRIPT" + [ "$status" -eq 0 ] + count=$(echo "$output" | grep -c "dup.txt") + [ "$count" -eq 1 ] +} diff --git a/test/list-files.bats b/test/list-files.bats new file mode 100644 index 0000000..abf3fa3 --- /dev/null +++ b/test/list-files.bats @@ -0,0 +1,73 @@ +#!/usr/bin/env bats + +SCRIPT="$BATS_TEST_DIRNAME/../list-files.sh" + +setup() { + TEST_DIR="$(mktemp -d)" + mkdir -p "$TEST_DIR/subdir/nested" + touch "$TEST_DIR/file1.txt" + touch "$TEST_DIR/subdir/file2.txt" + touch "$TEST_DIR/subdir/nested/file3.txt" + ORIG_PWD="$PWD" + cd "$TEST_DIR" +} + +teardown() { + cd "$ORIG_PWD" + rm -rf "$TEST_DIR" +} + +@test "exits 0 in a normal directory" { + run bash "$SCRIPT" -o + [ "$status" -eq 0 ] +} + +@test "-o flag limits depth to 1 level" { + run bash "$SCRIPT" -o + [ "$status" -eq 0 ] + # Should contain top-level file and subdir + echo "$output" | grep -q "file1.txt" + echo "$output" | grep -q "subdir" + # Should NOT contain nested files + ! echo "$output" | grep -q "file3.txt" +} + +@test "-s flag recurses into subdirectories" { + run bash "$SCRIPT" -s + [ "$status" -eq 0 ] + echo "$output" | grep -q "file1.txt" + echo "$output" | grep -q "file2.txt" + echo "$output" | grep -q "file3.txt" +} + +@test "handles files with spaces in names" { + touch "$TEST_DIR/file with spaces.txt" + run bash "$SCRIPT" -o + [ "$status" -eq 0 ] + echo "$output" | grep -q "file with spaces.txt" +} + +@test "handles files with special characters in names" { + touch "$TEST_DIR/file[1].txt" + touch "$TEST_DIR/file(2).txt" + run bash "$SCRIPT" -o + [ "$status" -eq 0 ] + echo "$output" | grep -q 'file\[1\].txt' + echo "$output" | grep -q 'file(2).txt' +} + +@test "common exclude dirs are excluded from output" { + mkdir -p "$TEST_DIR/node_modules/pkg" + touch "$TEST_DIR/node_modules/pkg/index.js" + mkdir -p "$TEST_DIR/.git/objects" + touch "$TEST_DIR/.git/objects/abc" + + run bash "$SCRIPT" -s + [ "$status" -eq 0 ] + # The fd output should not include files inside node_modules or .git + ! echo "$output" | grep -q "node_modules/pkg/index.js" + ! echo "$output" | grep -q ".git/objects/abc" + # But the directory names themselves get added back + echo "$output" | grep -q "node_modules" + echo "$output" | grep -q ".git" +} diff --git a/test/preview.bats b/test/preview.bats new file mode 100644 index 0000000..9c6a730 --- /dev/null +++ b/test/preview.bats @@ -0,0 +1,34 @@ +#!/usr/bin/env bats + +SCRIPT="$BATS_TEST_DIRNAME/../preview.sh" + +setup() { + TEST_DIR="$(mktemp -d)" + echo "hello world" > "$TEST_DIR/sample.txt" + mkdir -p "$TEST_DIR/mydir" + touch "$TEST_DIR/mydir/a.txt" + touch "$TEST_DIR/mydir/b.txt" +} + +teardown() { + rm -rf "$TEST_DIR" +} + +@test "text file preview works" { + run bash "$SCRIPT" "$TEST_DIR/sample.txt" + [ "$status" -eq 0 ] + echo "$output" | grep -q "hello world" +} + +@test "directory preview works" { + run bash "$SCRIPT" "$TEST_DIR/mydir" + [ "$status" -eq 0 ] + echo "$output" | grep -q "a.txt" + echo "$output" | grep -q "b.txt" +} + +@test "HOME keyword shows home directory listing" { + run bash "$SCRIPT" HOME + [ "$status" -eq 0 ] + [ -n "$output" ] +} diff --git a/test/toggle-state.bats b/test/toggle-state.bats new file mode 100644 index 0000000..8515136 --- /dev/null +++ b/test/toggle-state.bats @@ -0,0 +1,57 @@ +#!/usr/bin/env bats + +SCRIPT="$BATS_TEST_DIRNAME/../actions/toggle-state.sh" + +setup() { + export STATE_FILE="/tmp/cmdk_env_toggle_bats_test_$$" + rm -f "$STATE_FILE" +} + +teardown() { + rm -f "$STATE_FILE" +} + +@test "init on sets state to on" { + run bash "$SCRIPT" init on + [ "$status" -eq 0 ] + [ "$output" = "on" ] +} + +@test "init off sets state to off" { + run bash "$SCRIPT" init off + [ "$status" -eq 0 ] + [ "$output" = "off" ] +} + +@test "toggle flips state from off to on" { + bash "$SCRIPT" init off + run bash "$SCRIPT" toggle + [ "$status" -eq 0 ] + [ "$output" = "on" ] +} + +@test "toggle flips state from on to off" { + bash "$SCRIPT" init on + run bash "$SCRIPT" toggle + [ "$status" -eq 0 ] + [ "$output" = "off" ] +} + +@test "get returns current state" { + bash "$SCRIPT" init on + run bash "$SCRIPT" get + [ "$status" -eq 0 ] + [ "$output" = "on" ] +} + +@test "cleanup removes state file" { + bash "$SCRIPT" init on + run bash "$SCRIPT" cleanup + [ "$status" -eq 0 ] + [ ! -f "$STATE_FILE" ] +} + +@test "invalid command exits with error" { + run bash "$SCRIPT" nonsense + [ "$status" -eq 1 ] +} From 4818f9d3411aaf8426fdb52498cd0c1843d38a30 Mon Sep 17 00:00:00 2001 From: Huynh Duc Dung Date: Fri, 6 Feb 2026 08:00:25 +0800 Subject: [PATCH 22/23] docs: update CONCERNS.md with resolved issues Amp-Thread-ID: https://ampcode.com/threads/T-019c3036-2a8f-75cc-849e-4e12ffd77ea6 Co-authored-by: Amp --- .planning/codebase/CONCERNS.md | 155 +++++++++++++++++++++++++++++++++ 1 file changed, 155 insertions(+) create mode 100644 .planning/codebase/CONCERNS.md diff --git a/.planning/codebase/CONCERNS.md b/.planning/codebase/CONCERNS.md new file mode 100644 index 0000000..270dae3 --- /dev/null +++ b/.planning/codebase/CONCERNS.md @@ -0,0 +1,155 @@ +# Codebase Concerns + +**Analysis Date:** 2026-02-06 +**Last Updated:** 2026-02-06 + +## Tech Debt + +**~~No automated testing framework:~~** βœ… RESOLVED +- Fix: Added BATS test suite in `test/` with 21 tests covering list-files, git-files, toggle-state, and preview scripts + +**Shell compatibility concerns:** +- Issue: Scripts need to work across Bash and Fish shells +- Files: `cmdk-core.sh`, `cmdk.sh`, `cmdk.fish` +- Impact: Portability issues, different feature sets between shells +- Fix approach: Define minimum shell version requirements, add CI tests for both shells +- Status: Shebang mismatches fixed (`list-files.sh` and `preview.sh` now correctly use `#!/usr/bin/env bash`) + +**~~No error handling standardization:~~** βœ… PARTIALLY RESOLVED +- Fix: Added dependency checks for required tools (`fzf`, `fd`, `file`) in `cmdk-core.sh` +- Fix: Added flag validation with clear error messages for unknown flags +- Fix: Added `trap` cleanup in `cmdk-core.sh` for guaranteed state cleanup on exit/error +- Fix: Fixed exit code capture bug (was capturing cleanup exit code, not fzf) +- Fix: Fixed `return` in non-sourced script β†’ `exit 1` +- Remaining: Could add a shared error handling library if more scripts are added + +**~~Missing ShellCheck integration:~~** βœ… ALREADY RESOLVED +- ShellCheck CI already exists in `.github/workflows/shellcheck.yml` +- All scripts now pass ShellCheck cleanly + +## Known Bugs + +**~~Exit code capture bug in cmdk-core.sh:~~** βœ… FIXED +- Was: `exit_code=$?` captured cleanup exit code, not fzf's exit code +- Fix: Exit code now captured immediately after fzf with `|| exit_code=$?` + +**~~`return` used in non-sourced script:~~** βœ… FIXED +- Was: `return` in `cmdk-core.sh` which is invoked via `bash`, not sourced +- Fix: Changed to `exit 1` + +**~~Home excludes not applied in list-files.sh:~~** βœ… FIXED +- Was: `home_exclude_args` was computed but never passed to the `fd` call when in HOME +- Fix: Now conditionally included in fd invocation when `PWD == HOME` + +**~~Incorrect if-then-else in reload-files.sh:~~** βœ… FIXED +- Was: `A && B || C` pattern which is not equivalent to if-then-else +- Fix: Replaced with proper `if/then/else/fi` structure + +## Security Considerations + +**Unquoted variables in scripts:** +- Risk: Word splitting and glob expansion vulnerabilities +- Files: `list-files.sh` (intentional word-splitting for fd args, documented with shellcheck directives) +- Current mitigation: All intentional word-splitting annotated with `# shellcheck disable=SC2086` +- Status: Reviewed and acceptable for controlled internal values + +**~~No input validation:~~** βœ… RESOLVED +- Fix: Added flag validation in `cmdk-core.sh` β€” only `-o`, `-s`, `-e` accepted +- Fix: Unknown flags now produce clear error and exit 1 +- Fix: Validated flags used instead of raw `$*` in fzf commands + +**Environment variable parsing:** +- Risk: Uncontrolled environment variables could affect behavior +- Files: `.env` parsing in core +- Current mitigation: .env file is local only +- Recommendations: Validate/whitelist environment variables + +## Performance Bottlenecks + +**File discovery inefficiency:** +- Problem: `list-files.sh` may scan entire directory trees repeatedly +- Files: `list-files.sh`, `cmdk-core.sh` +- Cause: No caching of file listings +- Improvement path: Add incremental file caching, use git for tracking changes + +**Script sourcing overhead:** +- Problem: Each invocation sources multiple files +- Files: All shell entry points +- Cause: No pre-compilation, full parsing on each run +- Improvement path: Consider compiled shell (shc) for production, profile hot paths + +## Fragile Areas + +**Shell compatibility layer:** +- Files: `cmdk.sh`, `cmdk.fish`, `cmdk-core.sh` +- Why fragile: Different shells have different semantics, behavior divergence +- Safe modification: Create comprehensive tests before changing core +- Test coverage: BATS tests now cover core scripts; cross-shell integration tests still needed + +**Action routing system:** +- Files: `cmdk-core.sh` (action dispatch logic) +- Why fragile: Central routing point, affects all commands +- Safe modification: Write integration tests first, test all actions after changes +- Test coverage: Toggle state tests added; full dispatch testing requires fzf stubbing + +**File discovery utilities:** +- Files: `list-files.sh`, `git-files.sh` +- Why fragile: Depends on specific Unix tools (find, git) +- Safe modification: Dependency checks now added in `cmdk-core.sh` +- Test coverage: BATS tests cover basic scenarios, edge cases (spaces, special chars) + +## Scaling Limits + +**Script interpretation overhead:** +- Current capacity: Suitable for small to medium CLI usage +- Limit: May become slow with very large file sets or deep nesting +- Scaling path: Profile hot paths, consider compiled versions or faster language + +**Memory usage in large operations:** +- Current capacity: Should be fine for typical usage +- Limit: Loading entire file lists into memory could be issue with huge projects +- Scaling path: Implement streaming/incremental processing + +## Dependencies at Risk + +**~~Git dependency (silent failure):~~** βœ… PARTIALLY RESOLVED +- Fix: `git-files.sh` already exits non-zero when not in git repo +- Fix: `reload-files.sh` now uses proper if/then/else for git fallback +- Remaining: Could add `git` to dependency checks if git features are required + +**Unix utilities:** +- Risk: Depends on find, grep, sed, etc. +- Impact: Breaks on systems without standard Unix tools (minimal containers, Windows WSL) +- Migration plan: `cmdk-core.sh` now checks for `fzf`, `fd`, `file` at startup +- Status: `preview.sh` now has fallbacks for optional tools (`bat`β†’`cat`, `tiv`, `pdftotext`, `unzip`) + +## Missing Critical Features + +None identified at this scope level. + +## Test Coverage Gaps + +**~~Core dispatcher logic:~~** βœ… PARTIALLY RESOLVED +- Added: Toggle state tests, list-files tests, git-files tests, preview tests +- Remaining: Full fzf interaction testing would require fzf stubbing + +**Shell-specific integration:** +- What's not tested: Bash-specific vs Fish-specific behavior +- Files: `cmdk.sh`, `cmdk.fish` +- Risk: Commands might work in one shell but not the other +- Priority: High + +**~~Error conditions:~~** βœ… PARTIALLY RESOLVED +- Added: Tests for invalid toggle commands, non-git directory handling +- Remaining: Missing file, permission error, missing tool scenarios +- Priority: Medium + +**~~Edge cases in file operations:~~** βœ… RESOLVED +- Added: Tests for files with spaces and special characters +- Files: `test/list-files.bats` +- Priority: Medium + +--- + +*Concerns audit: 2026-02-06* +*Last fix pass: 2026-02-06* From 73d7b8868c0cbf427a4bd82129d5169fb5d60809 Mon Sep 17 00:00:00 2001 From: Huynh Duc Dung Date: Fri, 6 Feb 2026 08:03:23 +0800 Subject: [PATCH 23/23] docs: add testing section to README Amp-Thread-ID: https://ampcode.com/threads/T-019c3036-2a8f-75cc-849e-4e12ffd77ea6 Co-authored-by: Amp --- README.md | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/README.md b/README.md index ae6cd88..8904f6b 100644 --- a/README.md +++ b/README.md @@ -125,6 +125,23 @@ Feedback -------- Hi HN! I'd love to hear how you're using cmdk, and making it your own. +Testing +------- +cmdk uses [BATS](https://github.com/bats-core/bats-core) (Bash Automated Testing System) for automated tests. + +```sh +brew install bats-core # if not already installed +bats test/ +``` + +Test files: +- `test/list-files.bats` β€” file discovery, depth modes, spaces/special chars, exclude dirs +- `test/git-files.bats` β€” git-changed file detection, deduplication, non-git fallback +- `test/toggle-state.bats` β€” toggle init, flip, get, cleanup +- `test/preview.bats` β€” text/directory/HOME preview + +For manual testing across shells (bash, zsh, fish), see [testing-checklist.md](testing-checklist.md). + TODO ---- - [Allow for favoriting files that pop to the top of the search](https://github.com/mieubrisse/cmdk/issues/5)