From 5a6626cada5419afefd6a720f73654566946499a Mon Sep 17 00:00:00 2001 From: Kaiiiiiiiii <2761362118@qq.com> Date: Tue, 3 Feb 2026 13:36:15 +0800 Subject: [PATCH 1/7] fix: guard empty interactive completion results --- src/shell.rs | 14 ++++++++++++++ templates/bash.txt | 23 ++++++++++++----------- templates/fish.txt | 1 + 3 files changed, 27 insertions(+), 11 deletions(-) diff --git a/src/shell.rs b/src/shell.rs index 8812b1c..3864f74 100644 --- a/src/shell.rs +++ b/src/shell.rs @@ -65,6 +65,13 @@ mod tests { .stderr(""); } + #[apply(opts)] + fn bash_init_guards_empty_completion(cmd: Option<&str>, hook: InitHook, echo: bool, resolve_symlinks: bool) { + let opts = Opts { cmd, hook, echo, resolve_symlinks }; + let source = Bash(&opts).render().unwrap(); + assert!(source.contains("[[ -n \"${__zoxide_result}\" ]] || return")); + } + #[apply(opts)] fn bash_shellcheck(cmd: Option<&str>, hook: InitHook, echo: bool, resolve_symlinks: bool) { let opts = Opts { cmd, hook, echo, resolve_symlinks }; @@ -141,6 +148,13 @@ mod tests { .stderr(""); } + #[apply(opts)] + fn fish_init_guards_empty_completion(cmd: Option<&str>, hook: InitHook, echo: bool, resolve_symlinks: bool) { + let opts = Opts { cmd, hook, echo, resolve_symlinks }; + let source = Fish(&opts).render().unwrap(); + assert!(source.contains("and test -n \"$result\"")); + } + #[apply(opts)] fn fish_fishindent(cmd: Option<&str>, hook: InitHook, echo: bool, resolve_symlinks: bool) { let opts = Opts { cmd, hook, echo, resolve_symlinks }; diff --git a/templates/bash.txt b/templates/bash.txt index 86a2884..6b82949 100644 --- a/templates/bash.txt +++ b/templates/bash.txt @@ -178,18 +178,19 @@ if [[ ${BASH_VERSINFO[0]:-0} -eq 4 && ${BASH_VERSINFO[1]:-0} -ge 4 || ${BASH_VER # If there is a space after the last word, use interactive selection. elif [[ -z ${COMP_WORDS[-1]} ]]; then # shellcheck disable=SC2312 - __zoxide_result="$(\command zoxide query --exclude "$(__zoxide_pwd)" --interactive -- "{{ "${COMP_WORDS[@]:1:${#COMP_WORDS[@]}-2}" }}")" && { - # In case the terminal does not respond to \e[5n or another - # mechanism steals the response, it is still worth completing - # the directory in the command line. - COMPREPLY=("${__zoxide_z_prefix}${__zoxide_result}/") + __zoxide_result="$(\command zoxide query --exclude "$(__zoxide_pwd)" --interactive -- "{{ "${COMP_WORDS[@]:1:${#COMP_WORDS[@]}-2}" }}")" || return + [[ -n "${__zoxide_result}" ]] || return - # Note: We here call "bind" without prefixing "\builtin" to be - # compatible with frameworks like ble.sh, which emulates Bash's - # builtin "bind". - bind -x '"\e[0n": __zoxide_z_complete_helper' - \builtin printf '\e[5n' >/dev/tty - } + # In case the terminal does not respond to \e[5n or another + # mechanism steals the response, it is still worth completing + # the directory in the command line. + COMPREPLY=("${__zoxide_z_prefix}${__zoxide_result}/") + + # Note: We here call "bind" without prefixing "\builtin" to be + # compatible with frameworks like ble.sh, which emulates Bash's + # builtin "bind". + bind -x '"\e[0n": __zoxide_z_complete_helper' + \builtin printf '\e[5n' >/dev/tty fi } diff --git a/templates/fish.txt b/templates/fish.txt index a13edc7..64d1a88 100644 --- a/templates/fish.txt +++ b/templates/fish.txt @@ -101,6 +101,7 @@ function __zoxide_z_complete # If the last argument is empty, use interactive selection. set -l query $tokens[2..-1] set -l result (command zoxide query --exclude (__zoxide_pwd) --interactive -- $query) + and test -n "$result" and __zoxide_cd $result and builtin commandline --function cancel-commandline repaint end From 97f89e3ee3e5da235eef27eff6cc1ae92b500784 Mon Sep 17 00:00:00 2001 From: Kaiiiiiiiii <2761362118@qq.com> Date: Tue, 3 Feb 2026 13:44:20 +0800 Subject: [PATCH 2/7] style: format guard tests --- src/shell.rs | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/src/shell.rs b/src/shell.rs index 3864f74..f3d5edd 100644 --- a/src/shell.rs +++ b/src/shell.rs @@ -66,7 +66,12 @@ mod tests { } #[apply(opts)] - fn bash_init_guards_empty_completion(cmd: Option<&str>, hook: InitHook, echo: bool, resolve_symlinks: bool) { + fn bash_init_guards_empty_completion( + cmd: Option<&str>, + hook: InitHook, + echo: bool, + resolve_symlinks: bool, + ) { let opts = Opts { cmd, hook, echo, resolve_symlinks }; let source = Bash(&opts).render().unwrap(); assert!(source.contains("[[ -n \"${__zoxide_result}\" ]] || return")); @@ -149,7 +154,12 @@ mod tests { } #[apply(opts)] - fn fish_init_guards_empty_completion(cmd: Option<&str>, hook: InitHook, echo: bool, resolve_symlinks: bool) { + fn fish_init_guards_empty_completion( + cmd: Option<&str>, + hook: InitHook, + echo: bool, + resolve_symlinks: bool, + ) { let opts = Opts { cmd, hook, echo, resolve_symlinks }; let source = Fish(&opts).render().unwrap(); assert!(source.contains("and test -n \"$result\"")); From 6b71292dd1924439ab6916c217dfd511a8e99563 Mon Sep 17 00:00:00 2001 From: Kaiiiiiiiii <2761362118@qq.com> Date: Tue, 3 Feb 2026 13:54:49 +0800 Subject: [PATCH 3/7] style: align bash guard with shfmt --- src/shell.rs | 2 +- templates/bash.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/shell.rs b/src/shell.rs index f3d5edd..38f0388 100644 --- a/src/shell.rs +++ b/src/shell.rs @@ -74,7 +74,7 @@ mod tests { ) { let opts = Opts { cmd, hook, echo, resolve_symlinks }; let source = Bash(&opts).render().unwrap(); - assert!(source.contains("[[ -n \"${__zoxide_result}\" ]] || return")); + assert!(source.contains("[[ -n ${__zoxide_result} ]] || return")); } #[apply(opts)] diff --git a/templates/bash.txt b/templates/bash.txt index 6b82949..ef1ff87 100644 --- a/templates/bash.txt +++ b/templates/bash.txt @@ -179,7 +179,7 @@ if [[ ${BASH_VERSINFO[0]:-0} -eq 4 && ${BASH_VERSINFO[1]:-0} -ge 4 || ${BASH_VER elif [[ -z ${COMP_WORDS[-1]} ]]; then # shellcheck disable=SC2312 __zoxide_result="$(\command zoxide query --exclude "$(__zoxide_pwd)" --interactive -- "{{ "${COMP_WORDS[@]:1:${#COMP_WORDS[@]}-2}" }}")" || return - [[ -n "${__zoxide_result}" ]] || return + [[ -n ${__zoxide_result} ]] || return # In case the terminal does not respond to \e[5n or another # mechanism steals the response, it is still worth completing From 018299fb4899dd0c8967c0a9427fb1dcf8d12f99 Mon Sep 17 00:00:00 2001 From: Kaiiiiiiiii <2761362118@qq.com> Date: Sat, 7 Mar 2026 15:31:18 +0800 Subject: [PATCH 4/7] test: scope bash guard assertion to configured cmd --- src/shell.rs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/shell.rs b/src/shell.rs index 38f0388..566b64b 100644 --- a/src/shell.rs +++ b/src/shell.rs @@ -74,7 +74,11 @@ mod tests { ) { let opts = Opts { cmd, hook, echo, resolve_symlinks }; let source = Bash(&opts).render().unwrap(); - assert!(source.contains("[[ -n ${__zoxide_result} ]] || return")); + if cmd.is_some() { + assert!(source.contains("[[ -n ${__zoxide_result} ]] || return")); + } else { + assert!(!source.contains("[[ -n ${__zoxide_result} ]] || return")); + } } #[apply(opts)] From d9494204ac78bba67bbcfa068fd077349cc808bb Mon Sep 17 00:00:00 2001 From: Kaiiiiiiiii <2761362118@qq.com> Date: Sat, 7 Mar 2026 16:18:13 +0800 Subject: [PATCH 5/7] fix: make empty interactive selections exit silently --- src/cmd/query.rs | 25 ++++++++++++++++--------- 1 file changed, 16 insertions(+), 9 deletions(-) diff --git a/src/cmd/query.rs b/src/cmd/query.rs index 6539c2e..d526b9b 100644 --- a/src/cmd/query.rs +++ b/src/cmd/query.rs @@ -1,11 +1,11 @@ use std::io::{self, Write}; -use anyhow::{Context, Result}; +use anyhow::{Context, Result, bail}; use crate::cmd::{Query, Run}; use crate::config; use crate::db::{Database, Epoch, Stream, StreamOptions}; -use crate::error::BrokenPipeHandler; +use crate::error::{BrokenPipeHandler, SilentExit}; use crate::util::{self, Fzf, FzfChild}; impl Run for Query { @@ -31,16 +31,23 @@ impl Query { fn query_interactive(&self, stream: &mut Stream, now: Epoch) -> Result<()> { let mut fzf = Self::get_fzf()?; - let selection = loop { - match stream.next() { - Some(dir) if Some(dir.path.as_ref()) == self.exclude.as_deref() => continue, - Some(dir) => { - if let Some(selection) = fzf.write(dir, now)? { - break selection; + let selection = match (|| -> Result { + loop { + match stream.next() { + Some(dir) if Some(dir.path.as_ref()) == self.exclude.as_deref() => continue, + Some(dir) => { + if let Some(selection) = fzf.write(dir, now)? { + break Ok(selection); + } } + None => break fzf.wait(), } - None => break fzf.wait()?, } + })() { + Ok(selection) if selection.is_empty() => bail!(SilentExit { code: 1 }), + Ok(selection) => selection, + Err(err) if err.to_string() == "no match found" => bail!(SilentExit { code: 1 }), + Err(err) => return Err(err), }; if self.score { From 9260d433886937503677edbde9a81f56c03ecfc3 Mon Sep 17 00:00:00 2001 From: Kaiiiiiiiii <2761362118@qq.com> Date: Sat, 7 Mar 2026 16:18:14 +0800 Subject: [PATCH 6/7] test: cover empty interactive selections --- tests/query_interactive.rs | 50 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 50 insertions(+) create mode 100644 tests/query_interactive.rs diff --git a/tests/query_interactive.rs b/tests/query_interactive.rs new file mode 100644 index 0000000..8d3abd1 --- /dev/null +++ b/tests/query_interactive.rs @@ -0,0 +1,50 @@ +#![cfg(not(windows))] + +use std::env; +use std::fs; +use std::os::unix::fs::PermissionsExt; + +use assert_cmd::Command; + +fn fake_path(script: &str) -> (tempfile::TempDir, String) { + let dir = tempfile::tempdir().unwrap(); + let path = dir.path().join("fzf"); + fs::write(&path, script).unwrap(); + let mut perms = fs::metadata(&path).unwrap().permissions(); + perms.set_mode(0o755); + fs::set_permissions(&path, perms).unwrap(); + let path_env = format!("{}:{}", dir.path().display(), env::var("PATH").unwrap_or_default()); + (dir, path_env) +} + +#[test] +fn interactive_query_is_silent_when_fzf_reports_no_match() { + let home = tempfile::tempdir().unwrap(); + let (_bin_dir, path_env) = fake_path("#!/bin/sh\ncat >/dev/null\nexit 1\n"); + + Command::cargo_bin("zoxide") + .unwrap() + .env("HOME", home.path()) + .env("PATH", path_env) + .args(["query", "--interactive"]) + .assert() + .code(1) + .stdout("") + .stderr(""); +} + +#[test] +fn interactive_query_is_silent_when_fzf_returns_empty_output() { + let home = tempfile::tempdir().unwrap(); + let (_bin_dir, path_env) = fake_path("#!/bin/sh\ncat >/dev/null\nexit 0\n"); + + Command::cargo_bin("zoxide") + .unwrap() + .env("HOME", home.path()) + .env("PATH", path_env) + .args(["query", "--interactive"]) + .assert() + .code(1) + .stdout("") + .stderr(""); +} From 86b96b72343839a0bd84b6e7e29bf9d1c980d4bb Mon Sep 17 00:00:00 2001 From: Kaiiiiiiiii <2761362118@qq.com> Date: Sat, 7 Mar 2026 16:21:01 +0800 Subject: [PATCH 7/7] style: format interactive query regression tests --- tests/query_interactive.rs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/tests/query_interactive.rs b/tests/query_interactive.rs index 8d3abd1..f523e25 100644 --- a/tests/query_interactive.rs +++ b/tests/query_interactive.rs @@ -1,8 +1,7 @@ #![cfg(not(windows))] -use std::env; -use std::fs; use std::os::unix::fs::PermissionsExt; +use std::{env, fs}; use assert_cmd::Command;