feat(tmux): improve tmux ssh behavior
This commit is contained in:
374
bin/ssh-menu
374
bin/ssh-menu
@@ -1,53 +1,353 @@
|
|||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
function _ssh_menu_preview {
|
# pick a host from ~/.ssh/config and every file it includes
|
||||||
file=$1; shift
|
# ssh-menu connect to the selection
|
||||||
host=$1; shift
|
# ssh-menu -s print the selection: "<host>" or "<host>\tluks" (ctrl-u)
|
||||||
|
|
||||||
# show host block from ssh config
|
# exported: the preview and the list reload run in child shells spawned by fzf
|
||||||
awk -v h="$host" '
|
export SSH_MENU_CONFIG="${SSH_MENU_CONFIG:-$HOME/.ssh/config}"
|
||||||
$0 == "Host " h { found=1 }
|
|
||||||
found && /^[^ \t]/ && $0 != "Host " h { exit }
|
|
||||||
found
|
|
||||||
' "$file"
|
|
||||||
|
|
||||||
echo ""
|
# reachability results are cached per host and profile so browsing the list
|
||||||
|
# does not re-probe (and re-log on) the same server; ctrl-r forgets one host
|
||||||
|
export SSH_MENU_CACHE="${XDG_RUNTIME_DIR:-/tmp}/ssh-menu-${USER:-$(id -un)}"
|
||||||
|
export SSH_MENU_CACHE_TTL="${SSH_MENU_CACHE_TTL:-3600}"
|
||||||
|
# seconds to wait for an sshd banner: a plain tcp connection answers within one;
|
||||||
|
# a ProxyCommand such as an iap tunnel needs a few to come up first
|
||||||
|
export SSH_MENU_TIMEOUT="${SSH_MENU_TIMEOUT:-1}"
|
||||||
|
export SSH_MENU_PROXY_TIMEOUT="${SSH_MENU_PROXY_TIMEOUT:-3}"
|
||||||
|
[[ -d $SSH_MENU_CACHE ]] || mkdir -m 700 "$SSH_MENU_CACHE"
|
||||||
|
|
||||||
# resolve hostname for reachability check
|
# config plus every file named on Include lines; globs expanded, relative
|
||||||
hostname=$(awk -v h="$host" '
|
# paths resolved against ~/.ssh, unreadable files skipped
|
||||||
$0 == "Host " h { found=1; next }
|
function _ssh_menu_files {
|
||||||
found && /^[^ \t]/ { exit }
|
local line paths path file
|
||||||
found && /HostName/ { print $2 }
|
printf '%s\n' "$SSH_MENU_CONFIG"
|
||||||
' "$file")
|
grep -iE '^[[:space:]]*Include[[:space:]]' "$SSH_MENU_CONFIG" 2>/dev/null \
|
||||||
|
| while read -r line; do
|
||||||
if command -v host &>/dev/null; then
|
paths=${line#*[Ii]nclude}
|
||||||
host "${hostname:-$host}"
|
for path in $paths; do
|
||||||
echo ""
|
path=${path/#\~/$HOME}
|
||||||
fi
|
[[ $path != /* ]] && path="$HOME/.ssh/$path"
|
||||||
|
for file in $path; do
|
||||||
ping -c 1 -W 1 "${hostname:-$host}" &>/dev/null \
|
[[ -r $file ]] && printf '%s\n' "$file"
|
||||||
&& echo "reachable" \
|
done
|
||||||
|| echo "not reachable"
|
done
|
||||||
|
done
|
||||||
}
|
}
|
||||||
|
|
||||||
export -f _ssh_menu_preview
|
# one line per alias: file, alias, state, group, unlock marker
|
||||||
|
# group is the fragment name; a "# unlock" comment inside a Host block, or on
|
||||||
|
# the line directly above it, marks the host as having an initrd sshd
|
||||||
|
function _ssh_menu_hosts {
|
||||||
|
local file group
|
||||||
|
while read -r file; do
|
||||||
|
group=$(basename "$file")
|
||||||
|
group=${group#ssh-fragment-}
|
||||||
|
awk -v file="$file" -v group="$group" '
|
||||||
|
function flush() {
|
||||||
|
for (i = 0; i < n; i++)
|
||||||
|
printf "%s\t%s\t%s\t%s\n", file, hosts[i], group, (marks > 0 ? "unlock" : "")
|
||||||
|
n = 0; marks = 0
|
||||||
|
}
|
||||||
|
# unset would compare equal to 0, i.e. "the line before line 1"
|
||||||
|
BEGIN { marker_line = -1 }
|
||||||
|
/^[[:space:]]*[Hh]ost[[:space:]]/ {
|
||||||
|
# a marker on the line directly above belongs to this block; the
|
||||||
|
# previous block keeps any markers of its own
|
||||||
|
carry = marker_line == NR - 1
|
||||||
|
if (carry) marks--
|
||||||
|
flush()
|
||||||
|
marks = carry
|
||||||
|
for (i = 2; i <= NF; i++) if ($i !~ /[*?!]/) hosts[n++] = $i
|
||||||
|
next
|
||||||
|
}
|
||||||
|
/^[[:space:]]*[Mm]atch[[:space:]]/ { flush(); next }
|
||||||
|
/^[[:space:]]*#[[:space:]]*unlock/ { marks++; marker_line = NR }
|
||||||
|
END { flush() }
|
||||||
|
' "$file"
|
||||||
|
done < <(_ssh_menu_files | awk '!seen[$0]++') \
|
||||||
|
| sort -t $'\t' -k3,3 -k2,2 \
|
||||||
|
| _ssh_menu_with_state
|
||||||
|
}
|
||||||
|
|
||||||
|
# $1 host, $2 now (epoch): one word summarising the cached default-profile
|
||||||
|
# probe, "?" when nothing fresh is cached. reads the cache only, never probes
|
||||||
|
function _ssh_menu_state {
|
||||||
|
local host=$1 now=$2 file="$SSH_MENU_CACHE/$1__default" line
|
||||||
|
[[ -f $file ]] || { echo "?"; return; }
|
||||||
|
(( now - $(stat -c %Y "$file") < SSH_MENU_CACHE_TTL )) || { echo "?"; return; }
|
||||||
|
line=$(<"$file")
|
||||||
|
case $line in
|
||||||
|
"jump "*" reachable"*) echo hop ;;
|
||||||
|
"jump "*) echo down ;;
|
||||||
|
reachable*) echo up ;;
|
||||||
|
unreachable*) echo down ;;
|
||||||
|
*) echo - ;;
|
||||||
|
esac
|
||||||
|
}
|
||||||
|
|
||||||
|
# insert the cached state as the third field: file, alias, state, group, marker.
|
||||||
|
# fzf keeps each field's trailing delimiter and the last field has none, so the
|
||||||
|
# state must not be last or --with-nth would glue it to its neighbour
|
||||||
|
function _ssh_menu_with_state {
|
||||||
|
local now file host group marker
|
||||||
|
now=$(date +%s)
|
||||||
|
while IFS=$'\t' read -r file host group marker; do
|
||||||
|
printf '%s\t%s\t%s\t%s\t%s\n' "$file" "$host" "$(_ssh_menu_state "$host" "$now")" "$group" "$marker"
|
||||||
|
done
|
||||||
|
}
|
||||||
|
|
||||||
|
# the raw Host block for an alias, comments included
|
||||||
|
function _ssh_menu_block {
|
||||||
|
local file=$1 host=$2
|
||||||
|
awk -v h="$host" '
|
||||||
|
/^[[:space:]]*[Hh]ost[[:space:]]/ {
|
||||||
|
found = 0
|
||||||
|
for (i = 2; i <= NF; i++) if ($i == h) found = 1
|
||||||
|
}
|
||||||
|
/^[[:space:]]*[Mm]atch[[:space:]]/ { found = 0 }
|
||||||
|
found
|
||||||
|
' "$file"
|
||||||
|
}
|
||||||
|
|
||||||
|
# an ssh server announces itself before the client sends anything, so a bare
|
||||||
|
# tcp connection (or a running ProxyCommand) yields the banner without any
|
||||||
|
# authentication and therefore without touching the agent or the yubikey
|
||||||
|
|
||||||
|
# $1 host, $2 port: prints the sshd banner if the port answers with one
|
||||||
|
function _ssh_menu_tcp_banner {
|
||||||
|
# shellcheck disable=SC2016 # $1/$2/$3 are the inner bash's positional args
|
||||||
|
timeout "$SSH_MENU_TIMEOUT" bash -c '
|
||||||
|
exec 3<>"/dev/tcp/$1/$2" || exit 1
|
||||||
|
IFS= read -t "$3" -r banner <&3 || exit 1
|
||||||
|
printf "%s\n" "$banner" | tr -d "\r"
|
||||||
|
' _ "$1" "$2" "$SSH_MENU_TIMEOUT" 2>/dev/null
|
||||||
|
}
|
||||||
|
|
||||||
|
# $1 expanded ProxyCommand: the remote banner arrives on its stdout. stdin is an
|
||||||
|
# open, silent pipe (not /dev/null): a tunnel command may exit on stdin EOF
|
||||||
|
# before the banner arrives. the sleep lives in a process substitution, not the
|
||||||
|
# pipeline, so it is not waited for: the result returns as soon as the first
|
||||||
|
# line arrives, or after the proxy timeout
|
||||||
|
function _ssh_menu_proxy_banner {
|
||||||
|
timeout "$SSH_MENU_PROXY_TIMEOUT" bash -c "$1" < <(sleep "$((SSH_MENU_PROXY_TIMEOUT + 1))") 2>/dev/null | head -n1 | tr -d '\r'
|
||||||
|
}
|
||||||
|
|
||||||
|
# ssh reading the same config ssh itself would; -F only when overridden, since
|
||||||
|
# -F also drops /etc/ssh/ssh_config
|
||||||
|
function _ssh_menu_sshflags {
|
||||||
|
[[ $SSH_MENU_CONFIG != "$HOME/.ssh/config" ]] && printf '%s\n' -F "$SSH_MENU_CONFIG"
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
function _ssh_menu_ssh {
|
||||||
|
local flags=()
|
||||||
|
mapfile -t flags < <(_ssh_menu_sshflags)
|
||||||
|
# shellcheck disable=SC2029 # plain argument passthrough, no remote command built here
|
||||||
|
ssh "${flags[@]}" "$@"
|
||||||
|
}
|
||||||
|
|
||||||
|
function _ssh_menu_resolve {
|
||||||
|
local host=$1 tag=$2
|
||||||
|
_ssh_menu_ssh -G ${tag:+-P "$tag"} -- "$host" 2>/dev/null
|
||||||
|
}
|
||||||
|
|
||||||
|
# effective config for one profile plus a (cached) reachability probe
|
||||||
|
function _ssh_menu_probe {
|
||||||
|
local host=$1 tag=$2 resolved
|
||||||
|
if ! resolved=$(_ssh_menu_resolve "$host" "$tag") || [[ -z $resolved ]]; then
|
||||||
|
echo "ssh -G failed for $host${tag:+ (-P $tag)}"
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
|
||||||
|
# identityfile only when the profile pins one; ssh -G lists all defaults otherwise
|
||||||
|
awk '
|
||||||
|
$1 == "identitiesonly" && $2 == "yes" { pinned = 1 }
|
||||||
|
$1 ~ /^(hostname|port|user|tag|proxyjump|proxycommand|userknownhostsfile)$/ { print }
|
||||||
|
$1 == "identityfile" && pinned { print }
|
||||||
|
' <<<"$resolved"
|
||||||
|
echo ""
|
||||||
|
_ssh_menu_reach_cached "$host" "$tag" "$resolved"
|
||||||
|
}
|
||||||
|
|
||||||
|
function _ssh_menu_age {
|
||||||
|
local s=$1
|
||||||
|
if (( s < 60 )); then echo "${s}s"
|
||||||
|
elif (( s < 3600 )); then echo "$(( s / 60 ))m"
|
||||||
|
else echo "$(( s / 3600 ))h"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# $1 host: drop cached results for every profile of that host
|
||||||
|
function _ssh_menu_forget {
|
||||||
|
rm -f -- "$SSH_MENU_CACHE/$1__"*
|
||||||
|
}
|
||||||
|
|
||||||
|
# $1 host, $2 unlock marker: forget and re-probe now, so the list can be
|
||||||
|
# reloaded with the fresh state immediately instead of on the next timer tick
|
||||||
|
function _ssh_menu_reprobe {
|
||||||
|
_ssh_menu_forget "$1"
|
||||||
|
_ssh_menu_probe "$1" "" >/dev/null
|
||||||
|
[[ $2 == unlock ]] && _ssh_menu_probe "$1" luks >/dev/null
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
# $1 host, $2 tag, $3 ssh -G output: reachability line, served from cache while fresh
|
||||||
|
function _ssh_menu_reach_cached {
|
||||||
|
local host=$1 tag=$2 resolved=$3 file age result
|
||||||
|
file="$SSH_MENU_CACHE/${host}__${tag:-default}"
|
||||||
|
if [[ -f $file ]]; then
|
||||||
|
age=$(( $(date +%s) - $(stat -c %Y "$file") ))
|
||||||
|
if (( age < SSH_MENU_CACHE_TTL )); then
|
||||||
|
printf '%s (cached %s ago, ctrl-r re-probes)\n' "$(<"$file")" "$(_ssh_menu_age "$age")"
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
result=$(_ssh_menu_reach "$host" "$tag" "$resolved")
|
||||||
|
printf '%s\n' "$result" > "$file"
|
||||||
|
printf '%s\n' "$result"
|
||||||
|
}
|
||||||
|
|
||||||
|
# $1 host, $2 tag, $3 ssh -G output: probe the endpoint, one line of result
|
||||||
|
function _ssh_menu_reach {
|
||||||
|
local host=$1 tag=$2 resolved=$3 hostname port user proxycommand proxyjump banner
|
||||||
|
local hop hopuser hophost hopport hopresolved hopopts sshflags cmd
|
||||||
|
hostname=$(awk '$1 == "hostname" { print $2 }' <<<"$resolved")
|
||||||
|
port=$(awk '$1 == "port" { print $2 }' <<<"$resolved")
|
||||||
|
user=$(awk '$1 == "user" { print $2 }' <<<"$resolved")
|
||||||
|
proxycommand=$(awk '$1 == "proxycommand" { $1 = ""; sub(/^ /, ""); print }' <<<"$resolved")
|
||||||
|
proxyjump=$(awk '$1 == "proxyjump" && $2 != "none" { print $2 }' <<<"$resolved")
|
||||||
|
|
||||||
|
if [[ -n $proxyjump ]]; then
|
||||||
|
# the target is only reachable through the hop, and opening a forwarded
|
||||||
|
# channel needs an authenticated session there. if a ControlMaster to
|
||||||
|
# the hop is already alive we ride it (no auth, no agent); otherwise
|
||||||
|
# only the hop's own port is probed
|
||||||
|
hop=${proxyjump%%,*}
|
||||||
|
hopuser=""
|
||||||
|
[[ $hop == *@* ]] && hopuser=${hop%%@*}
|
||||||
|
hop=${hop##*@}
|
||||||
|
hopport=""
|
||||||
|
if [[ $hop == *:* ]]; then
|
||||||
|
hopport=${hop##*:}
|
||||||
|
hop=${hop%%:*}
|
||||||
|
fi
|
||||||
|
# the ControlPath hash covers user and port, so match the jump's exactly
|
||||||
|
hopopts=(${hopuser:+-l "$hopuser"} ${hopport:+-p "$hopport"})
|
||||||
|
if _ssh_menu_ssh -O check "${hopopts[@]}" -- "$hop" >/dev/null 2>&1; then
|
||||||
|
# timeout runs a binary, not a shell function, hence the explicit flags
|
||||||
|
mapfile -t sshflags < <(_ssh_menu_sshflags)
|
||||||
|
banner=$(timeout "$SSH_MENU_TIMEOUT" ssh "${sshflags[@]}" -o BatchMode=yes -o IdentityAgent=none -o ControlMaster=no \
|
||||||
|
"${hopopts[@]}" -W "$hostname:$port" -- "$hop" < <(sleep "$((SSH_MENU_TIMEOUT + 1))") 2>/dev/null | head -n1 | tr -d '\r')
|
||||||
|
if [[ $banner == SSH-* ]]; then
|
||||||
|
echo "reachable via $hop ($hostname:$port, $banner)"
|
||||||
|
else
|
||||||
|
echo "unreachable via $hop ($hostname:$port)"
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
hopresolved=$(_ssh_menu_resolve "$hop" "")
|
||||||
|
hophost=$(awk '$1 == "hostname" { print $2 }' <<<"$hopresolved")
|
||||||
|
hopport=${hopport:-$(awk '$1 == "port" { print $2 }' <<<"$hopresolved")}
|
||||||
|
if banner=$(_ssh_menu_tcp_banner "$hophost" "$hopport"); then
|
||||||
|
echo "jump $hop reachable ($hophost:$hopport, $banner); target not probed: no master (ssh -O check ${hopopts[*]} $hop)"
|
||||||
|
else
|
||||||
|
echo "jump $hop unreachable ($hophost:$hopport)"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
elif [[ -n $proxycommand ]]; then
|
||||||
|
# running an ssh-based proxy would authenticate to the hop through the agent
|
||||||
|
# matches "ssh", "/path/to/ssh", "sh -c 'ssh ..." but not "sshuttle" or "foo-ssh"
|
||||||
|
if [[ $proxycommand =~ (^|[^[:alnum:]_.-])ssh([[:space:]]|$) ]]; then
|
||||||
|
echo "proxycommand runs ssh; not probed (would need the agent)"
|
||||||
|
else
|
||||||
|
# ProxyCommand accepts exactly %%, %h, %n, %p and %r (ssh_config(5) TOKENS);
|
||||||
|
# %% first so it cannot form a token
|
||||||
|
cmd=${proxycommand//%%/$'\x01'}
|
||||||
|
cmd=${cmd//%h/$hostname}
|
||||||
|
cmd=${cmd//%p/$port}
|
||||||
|
cmd=${cmd//%r/$user}
|
||||||
|
cmd=${cmd//%n/$host}
|
||||||
|
cmd=${cmd//$'\x01'/%}
|
||||||
|
banner=$(_ssh_menu_proxy_banner "$cmd")
|
||||||
|
if [[ $banner == SSH-* ]]; then
|
||||||
|
echo "reachable via proxycommand ($banner)"
|
||||||
|
else
|
||||||
|
echo "unreachable via proxycommand"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
elif banner=$(_ssh_menu_tcp_banner "$hostname" "$port"); then
|
||||||
|
echo "reachable ($hostname:$port, $banner)"
|
||||||
|
else
|
||||||
|
echo "unreachable ($hostname:$port)"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# $1 file, $2 alias, $3 unlock marker
|
||||||
|
function _ssh_menu_preview {
|
||||||
|
local file=$1 host=$2 marker=$3
|
||||||
|
_ssh_menu_block "$file" "$host"
|
||||||
|
echo ""
|
||||||
|
echo "--- ssh -G"
|
||||||
|
_ssh_menu_probe "$host" ""
|
||||||
|
if [[ $marker == unlock ]]; then
|
||||||
|
echo ""
|
||||||
|
echo "--- ssh -G -P luks"
|
||||||
|
_ssh_menu_probe "$host" luks
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
export -f _ssh_menu_files _ssh_menu_hosts _ssh_menu_state _ssh_menu_with_state \
|
||||||
|
_ssh_menu_block _ssh_menu_tcp_banner _ssh_menu_proxy_banner _ssh_menu_sshflags _ssh_menu_ssh _ssh_menu_resolve \
|
||||||
|
_ssh_menu_age _ssh_menu_forget _ssh_menu_reprobe _ssh_menu_reach_cached _ssh_menu_reach _ssh_menu_probe _ssh_menu_preview
|
||||||
|
|
||||||
|
# the state column is passive: it only reflects the cache, which the preview
|
||||||
|
# fills as hosts are visited. a periodic reload keeps the column current; fzf
|
||||||
|
# older than 0.73 lacks the every() event, so it is added only when supported
|
||||||
|
function _ssh_menu_reload_opts {
|
||||||
|
# exit 0 = option accepted and matched; an unknown event exits 2
|
||||||
|
if printf 'x\n' | fzf --bind 'every(2):ignore' --filter x >/dev/null 2>&1; then
|
||||||
|
printf '%s\n' '--bind' 'every(2):reload-sync(_ssh_menu_hosts)' '--track' '--id-nth' '2'
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# prints "<host>" or "<host>\tluks"
|
||||||
function _ssh_menu_select {
|
function _ssh_menu_select {
|
||||||
rg -N --no-heading '^Host .*' ~/.ssh \
|
local key host marker reload_opts=()
|
||||||
| sed 's/Host \(.*\)/\1/' | sort \
|
mapfile -t reload_opts < <(_ssh_menu_reload_opts)
|
||||||
| SHELL=$(which bash) fzf \
|
{
|
||||||
--delimiter ':' \
|
read -r key
|
||||||
--with-nth 2 \
|
IFS=$'\t' read -r _ host _ _ marker
|
||||||
--preview "_ssh_menu_preview {1} {2}" \
|
} < <(_ssh_menu_hosts \
|
||||||
--preview-label "ssh config info" \
|
| SHELL=$(command -v bash) fzf \
|
||||||
--border-label "ssh-menu" \
|
--delimiter $'\t' \
|
||||||
| cut -d ":" -f2
|
--with-nth 2..5 \
|
||||||
|
--nth 1,3 \
|
||||||
|
--tabstop 24 \
|
||||||
|
"${reload_opts[@]}" \
|
||||||
|
--expect ctrl-u \
|
||||||
|
--bind 'ctrl-r:execute-silent(_ssh_menu_reprobe {2} {5})+refresh-preview+reload-sync(_ssh_menu_hosts)' \
|
||||||
|
--header 'ENTER connect, CTRL-U unlock (luks), CTRL-R re-probe' \
|
||||||
|
--preview '_ssh_menu_preview {1} {2} {5}' \
|
||||||
|
--preview-label 'host' \
|
||||||
|
--border-label 'ssh-menu')
|
||||||
|
|
||||||
|
[[ -z $host ]] && return 0
|
||||||
|
if [[ $key == ctrl-u ]]; then
|
||||||
|
printf '%s\tluks\n' "$host"
|
||||||
|
else
|
||||||
|
printf '%s\n' "$host"
|
||||||
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
case "${1:-}" in
|
case "${1:-}" in
|
||||||
-s) _ssh_menu_select ;;
|
-s) _ssh_menu_select ;;
|
||||||
*)
|
*)
|
||||||
selected="$(_ssh_menu_select)"
|
IFS=$'\t' read -r host profile < <(_ssh_menu_select)
|
||||||
[[ -n $selected ]] && ssh "$selected"
|
[[ -z $host ]] && exit 0
|
||||||
|
if [[ $profile == luks ]]; then
|
||||||
|
exec ssh -P luks "$host"
|
||||||
|
else
|
||||||
|
exec ssh "$host"
|
||||||
|
fi
|
||||||
;;
|
;;
|
||||||
esac
|
esac
|
||||||
|
|||||||
@@ -1,24 +1,47 @@
|
|||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
if [[ $# -eq 1 ]]; then
|
# open (or switch to) a tmux session running ssh to a host
|
||||||
selected=$1
|
# tmux-ssher [host [luks]]
|
||||||
|
|
||||||
|
if [[ $# -ge 1 ]]; then
|
||||||
|
host=$1
|
||||||
|
profile=${2:-}
|
||||||
else
|
else
|
||||||
selected="$(~/.config/bin/ssh-menu -s)"
|
IFS=$'\t' read -r host profile < <(~/.config/bin/ssh-menu -s)
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if [[ -z $selected ]]; then
|
if [[ -z $host ]]; then
|
||||||
exit 0
|
exit 0
|
||||||
fi
|
fi
|
||||||
|
|
||||||
selected_name="ssh_$selected"
|
# session names cannot contain . or :
|
||||||
|
name=${host//[.:]/_}
|
||||||
|
|
||||||
if [[ -z $TMUX ]] && ! tmux list-sessions 2>/dev/null; then
|
# an unlock session is short-lived and must not collide with a live ssh session
|
||||||
tmux new-session -s "$selected_name" ssh "$selected"
|
if [[ $profile == luks ]]; then
|
||||||
exit 0
|
session="unlock_$name"
|
||||||
|
args=(-P luks "$host")
|
||||||
|
else
|
||||||
|
session="ssh_$name"
|
||||||
|
args=("$host")
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if ! tmux has-session -t="$selected_name" 2>/dev/null; then
|
# on failure keep the window until enter, so the error stays readable (e.g.
|
||||||
tmux new-session -ds "$selected_name" ssh "$selected"
|
# publickey denied because the yubikey is not inserted); tmux.conf's
|
||||||
|
# detach-on-destroy then returns to the previous session. bash, not the tmux
|
||||||
|
# default shell, so read -p behaves
|
||||||
|
cmd="bash -c 'ssh \"\$@\" || { rc=\$?; echo; read -rp \"ssh exited \$rc, press enter to close \"; }' _$(printf ' %q' "${args[@]}")"
|
||||||
|
|
||||||
|
if ! tmux has-session -t="$session" 2>/dev/null; then
|
||||||
|
tmux new-session -ds "$session" -n "$host" "$cmd"
|
||||||
|
# tmux.conf's c / " / % bindings read this to open more panes on the same host
|
||||||
|
if [[ $profile != luks ]]; then
|
||||||
|
tmux set-option -t "=$session:" @ssh_host "$host"
|
||||||
|
fi
|
||||||
fi
|
fi
|
||||||
|
|
||||||
tmux switch-client -t "$selected_name"
|
if [[ -z $TMUX ]]; then
|
||||||
|
tmux attach-session -t "=$session"
|
||||||
|
else
|
||||||
|
tmux switch-client -t "=$session"
|
||||||
|
fi
|
||||||
|
|||||||
@@ -13,6 +13,10 @@ set -g prefix C-s
|
|||||||
set -g base-index 1
|
set -g base-index 1
|
||||||
set -g focus-events on
|
set -g focus-events on
|
||||||
set -g update-environment "SSH_AUTH_SOCK SSH_CONNECTION SSH_TTY DISPLAY"
|
set -g update-environment "SSH_AUTH_SOCK SSH_CONNECTION SSH_TTY DISPLAY"
|
||||||
|
# when a session dies under the client (e.g. a failed ssh in tmux-ssher) go to
|
||||||
|
# the most recently used session instead of detaching. not "previous": that
|
||||||
|
# path crashes the whole server on 3.7b+ (tmux issue 5344, fixed after 3.7c)
|
||||||
|
set -g detach-on-destroy off
|
||||||
|
|
||||||
# status bar
|
# status bar
|
||||||
set -g status-style 'bg=default fg=default'
|
set -g status-style 'bg=default fg=default'
|
||||||
@@ -37,6 +41,13 @@ bind j select-pane -D
|
|||||||
bind h select-pane -L
|
bind h select-pane -L
|
||||||
bind l select-pane -R
|
bind l select-pane -R
|
||||||
|
|
||||||
|
# windows and panes: inside an ssh session (@ssh_host set by tmux-ssher) new
|
||||||
|
# panes open on the same host over the multiplexed connection.
|
||||||
|
# new-window/split-window do not expand formats in their command, run-shell does
|
||||||
|
bind c if-shell -F "#{@ssh_host}" 'run-shell "tmux new-window -t \"#{session_name}\" -n \"#{@ssh_host}\" \"ssh #{@ssh_host}\""' "new-window"
|
||||||
|
bind '"' if-shell -F "#{@ssh_host}" 'run-shell "tmux split-window -v -t \"#{pane_id}\" \"ssh #{@ssh_host}\""' "split-window -v"
|
||||||
|
bind % if-shell -F "#{@ssh_host}" 'run-shell "tmux split-window -h -t \"#{pane_id}\" \"ssh #{@ssh_host}\""' "split-window -h"
|
||||||
|
|
||||||
# reload
|
# reload
|
||||||
bind r source-file "$XDG_CONFIG_HOME/tmux/tmux.conf"
|
bind r source-file "$XDG_CONFIG_HOME/tmux/tmux.conf"
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user