feat(tmux): ctrl+click a file path to open it by type

tmux-open-path takes the path under the mouse, strips :line:col, #L, @ and file:// decorations, and resolves it against the pane directory and then its git root. Markdown opens in Markdown Preview (added to Brewfile.apps), images, PDFs and directories in yazi, and everything else in nvim at the clicked line, both in a split beside the pane.
This commit is contained in:
David Ibia
2026-10-10 22:12:36 +01:00
parent 5fbdaa9810
commit d92041b6dd
4 changed files with 75 additions and 1 deletions
+67
View File
@@ -0,0 +1,67 @@
#!/usr/bin/env zsh
# Open the file path under the mouse in a tmux pane, routed by type:
# markdown -> Markdown Preview (GUI)
# images, pdf, dirs -> yazi in a split pane (image preview via passthrough)
# anything else -> nvim in a split pane, jumping to :line when present
#
# Called from a tmux mouse binding:
# tmux-open-path <pane_id> <pane_current_path> <mouse_x> <mouse_line>
emulate -L zsh
setopt extendedglob
pane=$1 cwd=$2 x=$3 line=$4
fail() { tmux display-message -d 2000 "open-path: $1"; exit 1 }
[[ -n $pane && -n $cwd && $x == <-> ]] || fail "usage: tmux-open-path PANE CWD X LINE"
# Grab the run of path-like characters around the clicked column. Whitespace,
# quotes, brackets and box-drawing borders end a token.
local stop=$' \t"\'`()[]{}<>|│┃║─━'
(( i = x + 1 )) # mouse_x is 0-based, zsh strings are 1-based
(( i <= ${#line} )) || fail "nothing under cursor"
[[ $stop == *${line[i]}* ]] && fail "nothing under cursor"
(( s = i )); while (( s > 1 )) && [[ $stop != *${line[s-1]}* ]]; do (( s-- )); done
(( e = i )); while (( e < ${#line} )) && [[ $stop != *${line[e+1]}* ]]; do (( e++ )); done
token=${line[s,e]}
# Normalise: file:// URLs, @mentions, trailing punctuation, :line[:col], #Lnn.
token=${token#file://}
token=${token#@}
token=${token%%[.,;:!?]##}
lnum=
if [[ $token == (#b)(*)\#L(<->)* ]]; then
token=$match[1] lnum=$match[2]
elif [[ $token == (#b)(*):(<->):<-> || $token == (#b)(*):(<->) ]]; then
token=$match[1] lnum=$match[2]
fi
[[ -n $token ]] || fail "nothing under cursor"
# Resolve against the pane's directory, then the enclosing git root.
token=${~token} # expand a leading ~
# (not "path": zsh ties that name to $PATH)
file=
if [[ $token == /* ]]; then
[[ -e $token ]] && file=$token
else
for base in $cwd $(git -C $cwd rev-parse --show-toplevel 2>/dev/null); do
[[ -e $base/$token ]] && { file=$base/$token; break }
done
fi
[[ -n $file ]] || fail "no such file: $token"
file=${file:A}
if [[ -d $file ]]; then
tmux split-window -h -t $pane -c $file "yazi ${(q)file}"
exit
fi
case ${file:e:l} in
md|markdown|mdx|mdown|mkd|mkdn)
open -a "Markdown Preview" $file ;;
png|jpg|jpeg|gif|webp|avif|svg|bmp|tif|tiff|heic|ico|pdf)
tmux split-window -h -t $pane -c ${file:h} "yazi ${(q)file}" ;;
*)
tmux split-window -h -t $pane -c $cwd "nvim ${lnum:++$lnum} ${(q)file}" ;;
esac
+5
View File
@@ -142,6 +142,11 @@ bind-key -T copy-mode-vi 'C-\' select-pane -l
set -g set-clipboard on
set -as terminal-features ',*:clipboard'
# Ctrl+click a file path in any pane (omo, shell, logs) to open it:
# markdown in Markdown Preview, images/pdf/dirs in yazi, the rest in nvim.
# yazi and nvim split beside the clicked pane. See ~/.local/bin/tmux-open-path.
bind -n C-MouseDown1Pane run-shell -b "~/.local/bin/tmux-open-path #{q:pane_id} #{q:pane_current_path} #{mouse_x} #{q:mouse_line}"
#
# Initialize TMUX plugin manager (keep this line at the very bottom of tmux.conf)
run '~/.tmux/plugins/tpm/tpm'
+1
View File
@@ -20,6 +20,7 @@ cask "charles"
cask "cyberduck"
cask "chromium"
cask "johannesnagl/tap/showmd", trusted: true
cask "markdown-preview"
# AI and agents
cask "conductor"
+2 -1
View File
@@ -158,7 +158,7 @@ Key features:
### Development Tools
- **tmux** (`.tmux.conf`): Terminal multiplexer with custom key bindings; sets the terminal title to the session name, after the omo status marker when there is one
- **tmux** (`.tmux.conf`): Terminal multiplexer with custom key bindings; sets the terminal title to the session name, after the omo status marker when there is one. Ctrl+click a file path in any pane (omo, a shell, logs) to open it with `tmux-open-path`: markdown in Markdown Preview, images, PDFs and directories in yazi, anything else in nvim at the clicked `:line`. yazi and nvim open in a split beside the clicked pane
- **Zed** (`.config/zed/keymap.json`): Editor key bindings. Settings stay local because they reference machine-specific agent paths
- **tmux server** (`.tmux-server.conf`): Lightweight tmux config for remote servers
- **Git**: Global gitignore patterns (`.rgignore`, `.gitignore`)
@@ -232,6 +232,7 @@ Custom scripts and binaries are stored in `.local/bin/`:
- `claude-tmux`: Claude integration with tmux
- `cldir`: Change directory with Claude context
- `owt`: Git worktree workspaces for coding agents (see [Agent workspaces](#agent-workspaces-owt))
- `tmux-open-path`: Backs tmux's Ctrl+click. Takes the path under the mouse, strips `:line:col`, `#L10`, `@` and `file://`, resolves it against the pane's directory and then its git root, and routes it by type: markdown to Markdown Preview (`cask "markdown-preview"` in `Brewfile.apps`), images, PDFs and directories to yazi, the rest to nvim
- `omo-notify`: Sound, tab marker and notification for OmO's hooks. Ghostty posts the notification, so clicking it opens the session's tab and pane
- `omo-status`: Coloured marker in front of the Ghostty tab title for each tmux session running omo: 🔴 stopped with an error, 🟡 waiting for your answer, 🟢 finished, 🔵 working. A tab with several agents shows the most urgent one; focusing the tab clears red, amber and green, and undoes any title you gave the tab by hand so it always shows its session name
- `docker-nuke`: Force-restart a wedged Docker Desktop, capturing diagnostics to `~/.local/state/docker-nuke/` first (`--dry-run` to preview, `--no-restart` to leave it down, `--deep` for root helpers)