Karl Schultheisz

How I Make Terminal Sessions Productive

Published (updated: ) in Uncategorized. Tags: , .

Call me old fashioned, but in an era of IDEs written in JavaScript reaching peak popularity among developers, I still use a terminal to get everything done. In contrast to IDEs, which attempt to own the main productivity experience and defer customization to plugins, being productive in the terminal hinges on hand-selecting your tools and knowing how to use them.

Here are five ways I stay productive in the terminal.


Many people use tmux to enhance the productivity of their terminal sessions. Tmux’s most basic commands involve splitting panes and creating new windows. These functions allow multiple shells to run simultaneously, which can make SSH or Linux console sessions on a headless server or embedded system much more productive. However, tmux’s subtler features enable even further gains in productivity. Among them:

Split off a new pane in the current directory

Often, while running a text editor in a single-pane window, I need to open a new shell in the same working directory. In vi-like editors (such as vim and vis), run :!tmux sp -h in normal mode to spawn a new pane to the right of the current pane. If you know how to do this in Emacs or another popular terminal editor, let me know, and I’ll include it here.

Move a pane from another window to the current window

To make this convenient, add bind-key j joinp -h to your tmux configuration.

Switch to the pane you want to move and mark it with <prefix> m. Switch to the destination window and use <prefix> j to relocate the marked pane to the right of the current pane.

Break off a pane into its own window

The reverse operation is also useful, and requires no special configuration. Just use <prefix> ! to transform the current pane into a new window.

Search for text in the current buffer

Like vi, tmux employs a modal user interface, where the function of certain keys can change. I put off understanding it for years, but recently, in a fit of frustration, I sorted it out.

Aside from the “default” mode (which is akin to vi’s “normal” mode), tmux has a “copy” mode. Don’t be mislead by the name: it is also used for scrolling back through history and searching for text. Complicating this picture, copy mode offers two “key tables” to choose from: copy-mode, the default, provides Emacs-style bindings; and copy-mode-vi, for vi-style bindings. I don’t understand the Emacs bias. Moreover, the Emacs mode has no keybinding for searching through text, the function I typically want when I enter “copy” mode!

A clearer name for this mode would be “review” or “buffer”, because it is the only way that one can look at previous output. In any case, why doesn’t Emacs mode have a keybinding for search? I suspect there are many users who don’t know that tmux can search through panes.

When I finally switched to copy-mode-vi by adding set -g mode-keys vi to my configuration, life got a lot better. If you use this configuration, enter copy mode with <prefix> [. Just like in vi, search for text with /.

Select, copy, and paste text from a buffer

Once you’re in copy mode, you can move the cursor per the usual keybindings. Start a selection with the spacebar. Copy it with return. Paste with <prefix> ].

Write an interactive terminal session to a file

I’ve been troubleshooting a quality of service (QoS) problem with my GL-AR300M router, which runs OpenWRT. I attempted to diagnose the issue through an interactive terminal session, but hadn’t thought about how I’d share the output with others who could help me. I could have used script(1) (“make transcript of terminal session”), but it works less well than one might hope, as it records control characters and escape sequences. We just want to share what we see.

Tmux to the rescue. It takes two steps:

  1. Capture the entire buffer with <prefix> :capture-pane -S -.
  2. Save the buffer to a file with <prefix> :save-buffer filename.txt, where filename.txt is the name of the file you want to save. When specifying a relative path, it is relative to $HOME.

Here’s a more streamlined version, to be placed in your configuration, bound to <prefix> S:

# quickly save pane to file
bind-key S command-prompt -p "Save pane to file:" "capture-pane -S - ; save-buffer %%"

Be notified of test failures in another window

When visual-bell is enabled, tmux will highlight the window in the status line when the \a character is printed to the terminal. This can be used to flag a window when tests fail.

I often use entr to run tests whenever source files change. (I treat entr itself in a later section.) Sometimes test printouts are long, so they compete with source code for screen space. The tests can be moved to another window, but then one has to switch to that window to see the test results. With visual-bell, a test script like ./test || printf '\a' will notify you of test failures.

Iosevka Term

I’ve experimented with a number of different monospace fonts over the years. While I prefer the look of Fira Mono, the virtue of Iosevka Term is its horizontal compactness. When I have to cram my 27-inch monitor (or 13-inch laptop display) full of tmux or text editor panes, I keep coming back to Iosevka.

Aesthetically, Iosevka is more consistent than Inconsolata while keeping some of the roundness. At larger sizes, it rewards one with subtle cusps and curves in k, v, x, and y. Its tall characters preserve legibility without using up precious horizontal space. I find it lacking in distracting glyphs.


A programmer’s text editor guides them through thick and thin. It has to be ready for anything. In 2018, I switched permanently to vis, having been a vim user since 2014.

Vis incorporates the best elements of vi and sam. Upon its first release, it claimed to be “80% of vim in 1% of the code”. For that reason alone, it caught my attention. It’s been easy to patch it to add new features, like a command to switch the layout between horizontal and vertical. I’ve had a good experience contributing these patches upstream.

Vis has kept my attention by its cleanness and consistency, manifested in the choice of Lua as both a configuration language and plugin API, and in the pithiness of its feature set.

I use the ctags and vis-go plugins, which ease navigating large or unfamiliar codebases. I also wrote a small command to format paragraphs:

vis:map(vis.modes.NORMAL, 'fp', function()
  -- format paragraph


When developing software, entr is useful for speeding up the edit-compile-run loop. It has only one job: run an arbitrary command when files change. When used in combination with tmux and a fast build system, it approximates the IDE experience.

While one can write many useful one-liners with entr, it becomes the most useful when executing a script. This allows you to edit the triggered action without having to kill the entr process. For example:

find . -name '*.go' | entr -c ./test

Where the script test might contain

go vet
go test -race -coverprofile=c.out || printf '\a'
go tool cover -html=c.out

The first interactive shells emerged in the 1960s, and have pretty much always been used for both automating repetitive tasks and interacting with a computer in real time. The challenge of staying productive in the terminal is the challenge of working in a historical user interface idiom.

The terminal has evolved much since the mid 20th century, of course, but has picked up a lot of baggage. We have resources that do not constrain us to the designs that have persisted, and constraints that challenge these designs. But it remains difficult to break with the old paradigm because of our tendency to commit the sunk cost fallacy; to value what exists over what is possible.

Thus, I do not want to be seen as a terminal chauvinist simply because I have found a set of tools that make life a bit easier. I delight in the irony of something like PowerShell, the object-oriented shell for Windows, written by a Unix person, who wanted to bring the power of Unix-style automation to the GUI-centric operating system. The advantage of PowerShell lies in its lack of serialization between command stages: data passes between commands in fully structured form. This might be the sort of thing Rich Hickey would advocate in order to keep data models, not code, at the center of programming.