From 89e84d2883956a328a919ee307c3dcbe2130c9f9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gerardo=20Santove=C3=B1a?= Date: Sat, 9 May 2026 23:24:20 -0600 Subject: [PATCH] Open plain tmux URLs without disabling mouse mode Ghostty can detect ordinary URL text only when tmux does not consume the mouse event. Because mouse mode is required, tmux now keeps URL punctuation in mouse_word, opens both OSC 8 hyperlinks and plain URL words through a small helper, and forwards non-link clicks back to tmux normally. Constraint: Mouse mode must stay enabled for scrolling, pane selection, and mouse-aware TUIs. Constraint: URL clicks need to work for plain terminal text, not only OSC 8 hyperlinks. Rejected: Disable tmux mouse mode | it would restore terminal-native clicking but break tmux mouse workflows. Rejected: Open every mouse_word | non-URL clicks must preserve normal tmux behavior. Confidence: high Scope-risk: narrow Directive: Keep the MouseDown1Pane fallback path forwarding to tmux when the click is not on a URL. Tested: git diff --cached --check Tested: make check Not-tested: Manual click in a freshly reloaded tmux client. Co-authored-by: OmX --- Makefile | 3 ++- tests/test_dotfiles.bats | 28 +++++++++++++++++++++++++--- tmux/open-url.sh | 20 ++++++++++++++++++++ tmux/tmux.conf | 6 ++++-- 4 files changed, 51 insertions(+), 6 deletions(-) create mode 100755 tmux/open-url.sh diff --git a/Makefile b/Makefile index bb1b2ba..b67db5f 100644 --- a/Makefile +++ b/Makefile @@ -5,7 +5,8 @@ SHELL := /bin/bash INSTALL_SCRIPT := scripts/install-enhanced.sh TEST_SCRIPT := scripts/test-install.sh SECURITY_SCRIPT := scripts/security-check.sh -SHELL_SCRIPTS := $(INSTALL_SCRIPT) $(TEST_SCRIPT) $(SECURITY_SCRIPT) +TMUX_OPEN_URL_SCRIPT := tmux/open-url.sh +SHELL_SCRIPTS := $(INSTALL_SCRIPT) $(TEST_SCRIPT) $(SECURITY_SCRIPT) $(TMUX_OPEN_URL_SCRIPT) BATS_TESTS := tests/test_dotfiles.bats .PHONY: help syntax lint security test test-bats check install install-dry install-no-backup quick-install backup brew clean diff --git a/tests/test_dotfiles.bats b/tests/test_dotfiles.bats index 48ca2c6..ea363d9 100644 --- a/tests/test_dotfiles.bats +++ b/tests/test_dotfiles.bats @@ -111,18 +111,40 @@ EOF @test "tmux and ghostty preserve clickable URLs" { grep -q 'terminal-features.*,xterm-ghostty:extkeys:hyperlinks' "$DOTFILES_DIR/tmux/tmux.conf" + grep -q '^set -g word-separators " "$' "$DOTFILES_DIR/tmux/tmux.conf" grep -q '^bind-key -n MouseDown1Pane if -F "#{mouse_hyperlink}"' "$DOTFILES_DIR/tmux/tmux.conf" - grep -q 'command -v open' "$DOTFILES_DIR/tmux/tmux.conf" - grep -q 'command -v xdg-open' "$DOTFILES_DIR/tmux/tmux.conf" + grep -q 'open-url.sh "#{q:mouse_hyperlink}"' "$DOTFILES_DIR/tmux/tmux.conf" + grep -q 'open-url.sh "#{q:mouse_word}"' "$DOTFILES_DIR/tmux/tmux.conf" grep -q '^link-url = true$' "$DOTFILES_DIR/ghostty/config" } +@test "tmux URL opener normalizes plain links safely" { + local fake_bin="$TEST_TMPDIR/fake-bin" + local opened_url="$TEST_TMPDIR/opened-url" + mkdir -p "$fake_bin" + + cat > "$fake_bin/open" < "$opened_url" +EOF + chmod +x "$fake_bin/open" + + run env PATH="$fake_bin:$PATH" "$DOTFILES_DIR/tmux/open-url.sh" '"https://example.com/path?x=1".' + [ "$status" -eq 0 ] + [ "$(cat "$opened_url")" = "https://example.com/path?x=1" ] + + rm -f "$opened_url" + run env PATH="$fake_bin:$PATH" "$DOTFILES_DIR/tmux/open-url.sh" 'www.example.com/docs,' + [ "$status" -eq 0 ] + [ "$(cat "$opened_url")" = "https://www.example.com/docs" ] +} + @test "tmux config syntax is valid" { if ! command -v tmux >/dev/null 2>&1; then skip "tmux not available" fi - run tmux -f /dev/null source-file -n "$DOTFILES_DIR/tmux/tmux.conf" + run tmux -L "dotfiles-bats-$$" -f /dev/null start-server \; source-file -n "$DOTFILES_DIR/tmux/tmux.conf" \; kill-server [ "$status" -eq 0 ] } diff --git a/tmux/open-url.sh b/tmux/open-url.sh new file mode 100755 index 0000000..40ca215 --- /dev/null +++ b/tmux/open-url.sh @@ -0,0 +1,20 @@ +#!/usr/bin/env bash +set -euo pipefail + +url="${1:-}" + +# tmux can hand us surrounding punctuation when it extracts a plain-text URL. +# Strip common wrappers/trailing sentence punctuation before opening. +url="$(printf '%s' "$url" | sed -E "s/^[][[:space:]<({[\"'\`]+//; s/[][[:space:]>)}\"'\`.,;:!?]+$//")" + +case "$url" in + http://*|https://*|mailto:*|file://*|ssh://*|git://*) ;; + www.*) url="https://$url" ;; + *) exit 0 ;; +esac + +if command -v open >/dev/null 2>&1; then + exec open "$url" +elif command -v xdg-open >/dev/null 2>&1; then + exec xdg-open "$url" +fi diff --git a/tmux/tmux.conf b/tmux/tmux.conf index 0eb9bea..f81470f 100644 --- a/tmux/tmux.conf +++ b/tmux/tmux.conf @@ -3,9 +3,11 @@ set-option -as terminal-features ",xterm-ghostty:extkeys:hyperlinks" set-option -g extended-keys always set-option -g extended-keys-format csi-u set -g mouse on +# Keep URL punctuation inside tmux mouse_word so plain-text links can be opened. +set -g word-separators " " -# Open OSC 8 hyperlinks directly when clicked, but keep normal tmux mouse behavior elsewhere. -bind-key -n MouseDown1Pane if -F "#{mouse_hyperlink}" { run-shell -b 'url=#{q:mouse_hyperlink}; if command -v open >/dev/null 2>&1; then open "$url"; elif command -v xdg-open >/dev/null 2>&1; then xdg-open "$url"; fi' } { select-pane -t =; send-keys -M } +# Open OSC 8 hyperlinks and plain-text URLs directly when clicked, but keep normal tmux mouse behavior elsewhere. +bind-key -n MouseDown1Pane if -F "#{mouse_hyperlink}" { run-shell -b '~/.config/tmux/open-url.sh "#{q:mouse_hyperlink}"' } { if -F "#{m/ri:^(https?://|mailto:|file://|ssh://|git://|www\.),#{mouse_word}}" { run-shell -b '~/.config/tmux/open-url.sh "#{q:mouse_word}"' } { select-pane -t =; send-keys -M } } # Forward Shift+Enter as CSI-u so Codex can insert a newline instead of submitting. bind-key -n S-Enter send-keys -H 1b 5b 31 33 3b 32 75