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