diff --git a/README.md b/README.md index df4e97a..80d2ef0 100644 --- a/README.md +++ b/README.md @@ -37,12 +37,16 @@ Copy the `zp` script somewhere on your `PATH`. ## Usage ``` -usage: zp [root ...] +usage: zp [--print] [root ...] zmx-picker - lists zmx sessions, plus repos under any roots given as arguments or $ZP_ROOT (colon-separated) -options: -h/--help, -V/--version +options: + --print Print directory and complete session name, each NUL-terminated, + instead of attaching; cancellation emits nothing + -h, --help Show this help message + -V, --version Print the version ``` For example, to always pick from repositories under `~/src`, set: @@ -89,6 +93,33 @@ Any [zmx] session labels (`zmx set key=value`) are shown alongside each session and are fuzzy-searchable, so typing `key=value` narrows the picker to the sessions that carry that label. +### Selecting without attaching + +Use `zp --print [root ...]` to select a session for another command, such as an +autossh wrapper that reconnects to the same selection. Selecting an entry prints +two NUL-terminated fields to stdout, with no trailing newline: + +1. The directory from which to attach: the repository directory when selecting a + repository, or the directory where `zp` was launched when selecting an + existing session or typing a name. This is not the selected session's + original starting directory or its live `cwd` reported by zmx. +2. The complete session name, including `ZMX_SESSION_PREFIX` when set. Clear + that variable when attaching so zmx does not apply the prefix again. + +Selection does not create or attach a session. Esc or Ctrl-C cancels with exit +status 0 and no output. The picker still supports Ctrl-X to kill sessions. + +In Bash, read the fields directly; command substitution (`$(zp --print)`) cannot +preserve NUL bytes. For example, to attach to the selection: + +```bash +if { IFS= read -r -d '' start_dir && IFS= read -r -d '' session_name; } < <(zp --print); then + (cd "$start_dir" && ZMX_SESSION_PREFIX= zmx attach "$session_name") +fi +``` + +The reads fail on cancellation, so the example does not attach in that case. + ## License zmx-picker is released under the [Zero Clause BSD License](LICENSE) (SPDX: diff --git a/tests/harness.exp b/tests/harness.exp index 6d08bad..75a629d 100755 --- a/tests/harness.exp +++ b/tests/harness.exp @@ -3,9 +3,9 @@ # blocks until a terminal answers, so every expect below responds to # them; without that the picker stalls and looks like zero matches. # -# Usage: harness.exp [keys ...] +# Usage: harness.exp [--print] [keys ...] # Each keys argument is sent after a settle period. Exits with zp's -# exit status. +# exit status. With --print, captures stdout in $ZP_TEST_DIR/selection. log_user 0 @@ -18,16 +18,25 @@ proc idle {secs} { } set zp [lindex $argv 0] -set root [lindex $argv 1] +set argv [lrange $argv 1 end] +set print_mode [expr {[lindex $argv 0] eq "--print"}] +if {$print_mode} { + set argv [lrange $argv 1 end] +} +set root [lindex $argv 0] +set roots {} +if {$root ne ""} { + lappend roots $root +} -if {$root eq ""} { - spawn -noecho $zp +if {$print_mode} { + spawn -noecho sh -c {exec "$@" > "$ZP_TEST_DIR/selection"} sh $zp --print {*}$roots } else { - spawn -noecho $zp $root + spawn -noecho $zp {*}$roots } catch { exec stty rows 40 columns 120 < $spawn_out(slave,name) 2> /dev/null } -foreach keys [lrange $argv 2 end] { +foreach keys [lrange $argv 1 end] { idle 2 send -- $keys } diff --git a/tests/run.sh b/tests/run.sh index deae899..8461b5d 100755 --- a/tests/run.sh +++ b/tests/run.sh @@ -214,5 +214,43 @@ else not_ok '--version and -V report the version' "$out" fi +# Compare files to preserve NUL delimiters and catch any extra stdout. +# assert_print +# An empty expected directory means cancellation should emit no bytes. +assert_print() { + local description=$1 rc=0 + reset_log + rm -f "$ZP_TEST_DIR/selection" + : >"$ZP_TEST_DIR/expected" + if [[ -n $5 ]]; then + printf '%s\0%s\0' "$5" "$6" >"$ZP_TEST_DIR/expected" + fi + run --print "$2" "$3" "$4" || rc=$? + if [[ $rc -eq 0 && -z $(log) ]] && + cmp -s "$ZP_TEST_DIR/expected" "$ZP_TEST_DIR/selection"; then + ok "$description" + else + not_ok "$description" "rc=$rc log=$(log); output bytes: $(od -An -tx1 "$ZP_TEST_DIR/selection" 2>/dev/null)" + fi +} + +ZMX_SESSION_PREFIX='' assert_print '--print emits the caller directory and existing name without attaching' \ + '' 'beta' "$enter" "$PWD" beta +ZMX_SESSION_PREFIX=d. assert_print '--print never re-applies the prefix to an existing name' \ + '' 'gamma' "$enter" "$PWD" d.gamma +ZMX_SESSION_PREFIX='' assert_print '--print emits an unmatched query without attaching' \ + '' 'zzz-new' "$enter" "$PWD" zzz-new +ZMX_SESSION_PREFIX=d. assert_print '--print prefixes an unmatched query' \ + '' 'zzz-new' "$enter" "$PWD" d.zzz-new +ZMX_SESSION_PREFIX=d. assert_print '--print ctrl-n uses the query even with a match highlighted' \ + '' 'bet' "$ctrl_n" "$PWD" d.bet + +print_root="$ZP_TEST_DIR/root with spaces" +mkdir -p "$print_root/omega/.git" +ZMX_SESSION_PREFIX=d. assert_print '--print preserves repo paths with spaces and numbers prefixed sessions' \ + "$print_root" '[repo] omega' "$enter" "$print_root/omega" d.omega.10 +assert_print '--print cancellation exits successfully with no output or attach' \ + '' '' "$esc" '' '' + printf '\n%d passed, %d failed\n' "$pass" "$fail" [[ $fail -eq 0 ]] diff --git a/zp b/zp index 64a71cd..c51d34b 100755 --- a/zp +++ b/zp @@ -243,6 +243,14 @@ next_session() { printf '%s.%s\n' "$base" "$next" } +attach() { + if [[ $mode == print ]]; then + printf '%s\0%s\0' "$PWD" "${ZMX_SESSION_PREFIX:-}$1" + else + zmx attach "$1" + fi +} + select_session() { local output rc query key selected local type value @@ -324,7 +332,7 @@ select_session() { printf 'ctrl-n needs a typed query to name the new session\n' >&2 return 1 fi - zmx attach "$query" + attach "$query" return 0 fi @@ -335,13 +343,13 @@ select_session() { session) # The listed name is already complete; keep zmx from # applying $ZMX_SESSION_PREFIX to it again. - ZMX_SESSION_PREFIX='' zmx attach "$value" + ZMX_SESSION_PREFIX='' attach "$value" ;; repo) ( cd "$value" - zmx attach "$(next_session "$value")" + attach "$(next_session "$value")" ) ;; @@ -356,7 +364,7 @@ select_session() { if [[ -n $query ]]; then # No candidate matched, so use the entered query as a new # session name in the current directory. - zmx attach "$query" + attach "$query" return fi @@ -375,11 +383,22 @@ case ${1:-} in mode='kill' shift ;; +--print) + # Print the selection as NUL-terminated start_dir and session_name + # instead of attaching. session_name is complete, callers should set + # ZMX_SESSION_PREFIX=. + mode='print' + shift + ;; -h | --help) - printf 'usage: %s [root ...]\n\n' "${0##*/}" + printf 'usage: %s [--print] [root ...]\n\n' "${0##*/}" printf 'zmx-picker - lists zmx sessions, plus repos under any roots\n' printf 'given as arguments or $ZP_ROOT (colon-separated)\n\n' - printf 'options: -h/--help, -V/--version\n' + printf 'options:\n' + printf ' --print Print directory and complete session name, each NUL-terminated,\n' + printf ' instead of attaching; cancellation emits nothing\n' + printf ' -h, --help Show this help message\n' + printf ' -V, --version Print the version\n' exit 0 ;; -V | --version)