Tmux is a powerful terminal multiplexer, and its built-in support for scripting allows you to create new features according to your own workflow.
I spend most of my day in Tmux, at the command line, grepping through codebases and editing files with Vim. I copied and pasted or re-typed file names for a long time before I realized how irritated I was that I couldn’t merely click on a file name and immediately open that file to the given line.
An IDE would have that functionality, and being firmly in the camp of command line as IDE, I set out to right this wrong.
Some Background
Most of the time I use grep with the -n
option, which includes the line number of all matches.
$ grep -n "question" *
./config.rb:42: question = 6 * 9
To enter Tmux’s copy mode, hit your prefix key combination (mine is Ctrl-b) then the [
key. Cursor movement within copy mode depends on whether you have Tmux set to Vi or Emacs key bindings. I use Vi key bindings below. If you’d like to use Emacs key bindings instead, this Superuser answer has a helpful conversion table.
1. Figure Out the Keystrokes
The first step to scripting our new feature is to figure out how to do it manually. In copy mode, navigate to the line of the file you want to open. We need to grab the filename and line number from grep’s output. The colons around the line number serve as convenient landmarks. Here are the steps we need to automate:
- Move the cursor to the beginning of the line.
- Start selecting text.
- Move the cursor to the first colon.
- Move the cursor to the character before the second colon.
- Copy the selected text.
To accomplish this with Vi key bindings, press ^
to jump to the beginning of the line, then press spacebar to start selecting text. Skip to the next colon by pressing f
then :
. Skip to the character before the next colon by pressing t
then :
. To copy the selected text to Tmux’s buffer, hit the enter key.
2. Bind the Keystrokes to a Keyboard Shortcut
We’re not yet ready to complete our feature, but it’s a good time to document what we’ve learned. Let’s trigger these keystrokes with a Tmux key binding. Open your .tmux.conf
file and choose a key to use. We can register the keyboard shortcut with Tmux using the bind
command. I’ve picked g
, meaning Tmux will execute the given command when I press my prefix key combination (Ctrl-b) followed by the g
key.
bind g send "0 f:t:" C-m
Tmux’s send
command replays keystrokes, in this case the series of characters we used to copy the filename and line number. The C-m
at the end is interpreted by Tmux as the enter key.
Save the config, and load up a Tmux session to test it. Run a grep command with the -n
option, then enter copy mode and move the cursor to a line with a file you want to open. When you hit your prefix key combination followed by the g
key, you’ll find Tmux has dropped out of copy mode and returned you to the command line. To make sure it also copied the filename and line number, run the command tmux save-buffer -
. This command will print out the contents of Tmux’s copy buffer, in this case the selection copied by your new key binding.
3. Create a Helper Script
At this point we have the filename and line number as output by grep, with a colon separating the two parts. If your preferred editor won’t accept that format, you’ll need a helper script. Vim accepts a line number separate from the filename and prefixed by a +
sign, so here’s a script that splits our copied contents at the colon and passes them to Vim:
#!/bin/bash
filename=$(echo "$1" | cut -d':' -f1)
lineNumber=$(echo "$1" | cut -d':' -f2)
vim $filename +$lineNumber
Save this script somewhere on your path so you can invoke it anywhere. While I originally named this script vim-from-grepped
, I’ve found it useful enough to shorten the name to vimfg
.
4. Update the Key Binding We Created Earlier
You can test the script by typing vimfg
and hitting your prefix key combination followed by ]
, which will paste the contents of Tmux’s copy buffer. Hit enter and see if the script does what you want. If so, we’re ready to automate this step in our key binding. We’ll add the name of our script to the initial send
command, paste the contents of Tmux’s copy buffer using Tmux’s paste-buffer
command, and hit enter to invoke the script.
bind g send "0 f:t:" C-m "vimfg " ; paste-buffer ; send C-m
Because a semicolon would signify the end of the bind
command, we have to escape both semicolons with backslashes.
With all of these pieces in place you can spin up Tmux, run grep -rn "<pattern>" .
, enter copy mode, move the cursor to the file you want to open, and hit your prefix key combination followed by the g
key. You should then be in Vim on the line number grep found matching your pattern.
Scripting Tmux is a very powerful addition to your command line toolset. If you have other tips for augmenting Tmux, I’d love to hear about them!
I spend all day in tmux & vim too, but why not :grep from within vim, which loads up the quickfix window with the results and lets you use :cnext and :cprev to navigate through the files?
For even better results/performance, set :grepprg to be Ag[
Good tip, Matt. I haven’t used the :grep command. From what I see it opens every file that matches the pattern, correct? Depending on the pattern, that may be quite a few files, many of which I don’t need to open. Do you know if there’s a way to preview the results from within Vim and select particular files to open?
Excellent walk-through! Small note for your consideration: “f: • t:” is unnecessary, as you can simply tell it to move to the character before the *second* colon forward from your current position with “2t:” (=
Elliot, using “2t:” did occur to me, and it works splendidly within Vim, but it appears that Tmux behaves somewhat differently (unless I have something misconfigured, which is possible). Rather than jumping to the character before the second colon, it appears to execute the jump twice, an apparently important difference when using the “t” motion. When the cursor is on the character before a colon, “t:” leaves the cursor where it is. “2f:” successfully jumps to the second colon, and is possibly a viable alternative to using “f:t:”
If you’re using Git, then you may want to take a look at vim-fugitive plugin which has several commands including “:Ggrep “. It gives you shortcuts to navigate through the matches found in your repo’s files or you can open the quickfix (with “:copen”) and jump right into a given file.