diff --git a/.local/bin/tmux-open-path b/.local/bin/tmux-open-path new file mode 100755 index 0000000..95a6683 --- /dev/null +++ b/.local/bin/tmux-open-path @@ -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 + +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 diff --git a/.tmux.conf b/.tmux.conf index c65dbe4..e54fd63 100644 --- a/.tmux.conf +++ b/.tmux.conf @@ -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' diff --git a/Brewfile.apps b/Brewfile.apps index 97c5193..c292261 100644 --- a/Brewfile.apps +++ b/Brewfile.apps @@ -20,6 +20,7 @@ cask "charles" cask "cyberduck" cask "chromium" cask "johannesnagl/tap/showmd", trusted: true +cask "markdown-preview" # AI and agents cask "conductor" diff --git a/README.md b/README.md index 7f86609..7f10034 100644 --- a/README.md +++ b/README.md @@ -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)