diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..70515e4 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2014 dequis.org + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..10ed85c --- /dev/null +++ b/README.md @@ -0,0 +1,117 @@ +# tmux-url-select + +`tmux-url-select` is a perl script that integrates tightly with tmux to capture +the current pane buffer, switch to a window with highlighted links, and let you +select the link you want to open/yank. + +It's like the urxvtperls [url-select][1] script or [urlview][2], except that +it's inspired by the interface of the former and the capture-pane method usually +used with the latter. + +Features! + + * Tightly integrated to tmux to prevent flickering when switching to url + selection + * No dependencies (Not even ncurses, so no portability either!) + * Uses colors! Configurable colors! + * Configurable external commands too! + * vi-like keybindings (actually just `j` and `k`) + +[1]: https://github.com/muennich/urxvt-perls/blob/master/url-select +[2]: http://packages.qa.debian.org/u/urlview.html + +## Is it any good? + +[Yes][3] + +[3]: https://news.ycombinator.com/item?id=3067434 + +## Requirements + +Depends on `perl`, `tmux` and `stty`. + +Optional and configurable: `xdg-open` (can be any url opener or browser) and +`xclip` (for yank) + +## Installation + +Place it somewhere in your path and `chmod +x` it. + +## Configuration + +There's a bunch of constants near the top of the file, you can modify them to +your liking. + + use constant COMMAND => 'xdg-open %s'; + use constant YANK_COMMAND => 'echo %s | xclip -i'; + + use constant SHOW_STATUS_BAR => 1; + use constant VERBOSE_MESSAGES => 0; + use constant TMUX_WINDOW_TITLE => 'Select URL'; + + use constant PROMPT_COLOR => "\033[42;30m"; + use constant ACTIVE_LINK_HIGHLIGHT => "\033[44;4m"; + use constant NORMAL_LINK_HIGHLIGHT => "\033[94;1;4m"; + +Probably should add some explanations. Maybe. For now just go ahead and +experiment with stuff. + +## Usage + +Add this to your `.tmux.conf`: + + bind some-key-here run tmux-url-select + +Personally I use "z" which is an unused keybinding that is really close to my +tmux prefix key (`` ` ``) + + bind z run tmux-url-select + +Once you're inside tmux-url-select, keybindings: + + * `j`: down + * `k`: up + * `0`-`9`: select link by number + * `y`: yank (copy to clipboard) + * Enter: open link + * `q`: quit + +You can't use arrow keys because those are more complex than a single ascii +character. + +## Known issues + +If a line with a link has a background color, it will get reset after the link. +I have no idea how to do this without writing a parser of ansi escape codes or +using ncurses properly, so I'm leaving it unfixed as a reminder that as humans +we're all flawed in different ways. + +Might flicker when selecting links because the whole screen is redrawn. It can't +be helped. Works fine for me most of the time. + +Already workarounded, but: Some url openers don't like the fact that we kill the +terminal right after running the process. `gvfs-open` in particular opens my +browser asynchronously and doesn't wait for it, so there's a race condition in +which the terminal often get closed before the browser gets to do anything. +Workarounds for this: + + * `sleep 1` after running the command. The laziest and most generic workaround, + currently applied in the code because it doesn't cause any other issues. + * Prefix your url open command with `setsid`. This makes the child process a + process group leader, so it won't care about getting the terminal closed. + * Prefix with `nohup`. Does something similar but not quite the same as + `setsid`, and didn't work for me, but worth a try! + * Wrap the command with `$(...)`. Weirdest one, I found this trick accidentally + last year and [had to ask stack overflow][4] about it. It makes bash wait for + the whole process tree to finish executing, apparently. + * Prefix with `strace -f -o /dev/null`. Another weird one! This one also seems + to force waiting for all child processes to finish, by attaching to them with + ptrace. It's overkill and that makes it fun. + +[4]: http://stackoverflow.com/questions/16874043/bash-command-substitution-forcing-process-to-foreground + +## FAQ + +Q: Why perl? It's dead and it sucks, cool kids use node.js nowadays. + +A: It's fun. Fun things are fun. diff --git a/tmux-url-select.pl b/tmux-url-select.pl new file mode 100755 index 0000000..4689005 --- /dev/null +++ b/tmux-url-select.pl @@ -0,0 +1,196 @@ +#!/usr/bin/env perl +# +# tmux-url-select +# mit licensed +# + +use strict; +use warnings; + +### config + +use constant COMMAND => 'xdg-open %s'; +use constant YANK_COMMAND => 'echo %s | xclip -i'; + +use constant SHOW_STATUS_BAR => 1; +use constant VERBOSE_MESSAGES => 0; +use constant TMUX_WINDOW_TITLE => 'Select URL'; + +use constant PROMPT_COLOR => "\033[42;30m"; +use constant ACTIVE_LINK_HIGHLIGHT => "\033[44;4m"; +use constant NORMAL_LINK_HIGHLIGHT => "\033[94;1;4m"; + +# other options: +# - blue background, underlined: \033[44;4m +# - 256 color term light blue: \033[38;5;39m +# - bold bright blue: \033[94;1;4m +# - bright blue background: \033[104;4m +# - just underlined: \033[4m + +# regex stolen from urxvtperls url-select.pl +my $url_pattern = qr{( + (?:https?://|ftp://|news://|git://|mailto:|file://|www\.) + [\w\-\@;\/?:&=%\$_.+!*\x27(),~#]+[\w\-\@;\/?&=%\$_+!*\x27()~] +)}x; + +### config end + +my $raw_buffer; +my $buffer; +my $buffer_first_newline_position; +my @matches; +my $match_count; +my $selection = 0; + +# terminal helper functions + +sub clear { + print "\033[H\033[2J"; +} + +sub display_status_bar { + my $is_first_line = shift; + my $position = $is_first_line ? "2;2" : "1;2"; + print sprintf("\033[%sH%s URL select: (%s/%s) [j/k/y/q/enter] \033[0m", $position, PROMPT_COLOR, $selection+1, $match_count); +} + +sub display_highlighted_buffer { + my $i = 0; + my $is_first_line = 0; + my $cb = sub { + if ($i++ == $selection) { + $is_first_line = 1 if ($+[0] < $buffer_first_newline_position); + return ACTIVE_LINK_HIGHLIGHT."$1\033[0m"; + return; + } + return NORMAL_LINK_HIGHLIGHT."$1\033[0m" if NORMAL_LINK_HIGHLIGHT; + return $1; + }; + print $buffer =~ s/($url_pattern)/&$cb()/ger; + return $is_first_line; +} + +sub display_stuff { + clear(); + my $is_first_line = display_highlighted_buffer(); + display_status_bar($is_first_line) if SHOW_STATUS_BAR; +} + +# tmux command helpers + +sub tmux_display_message { + system 'tmux', 'display-message', shift; +} + +sub tmux_switch_to_last { + system 'tmux', 'last-window'; +} + +sub tmux_select_my_window { + system "tmux", "select-window", "-t", TMUX_WINDOW_TITLE; +} + +sub tmux_capture_pane { + system "tmux", "capture-pane", "-eJ"; +} + +sub tmux_get_buffer { + return `tmux show-buffer`; +} + +sub tmux_open_inner_window { + system "tmux", "new-window", "-dn", TMUX_WINDOW_TITLE, "$0 inner"; +} + +# other shell helpers + +sub enable_canonical_mode { + # "canonical mode" to read char by char, thanks roger. + system "stty", "-icanon"; +} + +sub single_quote_escape { + return "'".(shift =~ s/\'/%27/gr)."'"; +} + +# actions + +sub fix_url { + my $url = shift; + # some silly url openers think ^www. urls are files + return "http://".$url if $url =~ /^www\./; + return $url; +} + +sub launch_url { + my $url = fix_url(shift); + tmux_switch_to_last(); + system sprintf(COMMAND, single_quote_escape($url)); + tmux_display_message("Launched ". $url) if VERBOSE_MESSAGES; + + # shitty workaround for race conditions + # gvfs-open doesn't like when we kill the terminal + # and i'm not going to setsid it + sleep 1; +} + +sub yank_url { + my $url = fix_url(shift); + tmux_switch_to_last(); + system sprintf(YANK_COMMAND, single_quote_escape($url)); + tmux_display_message("Yanked ". $url) if VERBOSE_MESSAGES; +} + +# main functions + +sub main_inner { + $raw_buffer = tmux_get_buffer(); + $buffer = $raw_buffer =~ s/\n$//r; + $buffer_first_newline_position = index($raw_buffer, "\n"); + + @matches = ($buffer =~ /$url_pattern/g); + $match_count = @matches; + exit 1 if !$match_count; + + $selection = $#matches; + + display_stuff(); + + enable_canonical_mode(); + + # switch to the tmux-url-select window now to avoid 'flickering' + tmux_select_my_window(); + + # main loop + while(defined($_ = getc)) { + $selection++ if /j/; + $selection-- if /k/; + $selection = ($_-1) if /[0-9]/; + $selection %= $match_count; + yank_url($matches[$selection]) if /y/; + return launch_url($matches[$selection]) if /\n/; + return if /q/; + display_stuff(); + } +} + +sub main { + tmux_capture_pane(); + + @matches = tmux_get_buffer() =~ /$url_pattern/g; + $match_count = @matches; + + if (!$match_count) { + tmux_display_message("No URLs"); + exit 0; + } + + # open window here, backgrounded + tmux_open_inner_window(); +} + +if (!@ARGV) { + main(); +} elsif ($ARGV[0] eq "inner") { + main_inner(); +}