Paul's Internet Landfill/ 2021/ GNU Screen to Tmux Transition

GNU Screen to Tmux Transition

I have been dependent upon GNU Screen for at least 11 years. I presented my GNU Screen customizations back in 2009, and they have not changed much since. As with so many tools, I am no power user, but I depend upon Screen heavily enough that the Linux commandline is frustrating without it.

Of course, the cool kids don't use GNU Screen. A decade ago the cool kids used Tmux, and now who knows what they use. Because I am a dinosaur, I resisted the transition. Using <ctrl>-b as the prefix was enough to turn me off.

I recently had a change of heart. I was doing some development, which for me means endlessly running web searches for snippets of code I could cut and paste. Once I had a snippet, I no longer wanted the associated Screen window, so I closed it. Unfortunately, this left gaps. Say I had screens 0, 1, 2, and 3 active. Then I closed screens 1 and 2. This would leave screens 0 and 3 active, and the next screen I created would be in slot 1. This was almost always never what I wanted. Instead, I wanted the next fresh window to appear after screen 3.

Some people have come up with hacky solutions for this, but they looked real ugly. It turns out that Tmux has an option that collapses and renumbers windows automatically, so I took the plunge and attempted to switch.

In switching, I had some very specific goals:

Maintaining my keybindings was crucial for two reasons: firstly, I am not sure I will have Tmux available everywhere I use a terminal, but I can usually depend upon access to GNU Screen. Thus I want to be able to switch between the two painlessly. Maybe more importantly: I am an old dog, and I have at least 11 years of GNU Screen keybindings baked into my muscle memory. Even if I am capable of changing, I don't want to.

I did not fully reach my goals, but I got pretty close. What follows are config file snippets and scripts I used to make the transition. Documenting Tmux configurations are a cottage industry, but maybe I have something to offer anyways?

Basic Keybindings

The default location for the Tmux config file is ~/.tmux.conf, but I chose to put my keybinding configuration in ~/bin/tmux-config.conf . All the keybinding configuration (as opposed to the startup configuration) goes here.

The first order of business is to change the prefix from <ctrl>-b to <ctrl>-a:

# Fix muscle memory
unbind C-b
set -g prefix C-a

It turns out that reloading the config file on the fly is helpful, even though I never did this with GNU Screen. I chose r for this keybinding, but R might have been better.

# Reload tmux config
bind-key r source-file ~/bin/tmux-config.conf

I then wanted to match keybindings for cycling through windows, toggling through windows, retitling windows, and going to the beginning of a line the way GNU Screen does:

# Ctrl-a Space should cycle
bind-key Space next-window

# Toggle windows
bind-key C-a last-window

# Ctrl-a A to rename
bind-key A command-prompt "rename-window '%%'"

# Ctrl-a a goes to beginning of line
bind-key a send-prefix

Eliminating Annoyances

It turns out that by default Tmux sets windows to have a terminal type of screen. Presumably this is for compatibility, but it gets in the way when trying to distinguish between the two:

# Set default terminal type to tmux, not screen
set -g default-terminal tmux

GNU Screen supports "panes": splitting screens in half so you can put multiple pages within one screen. I can see how this might be useful in some cases, but I already use tiling window managers, and I hate panes, so I disable the keybinding:

# Panes are a pain
unbind-key %

I also hate the status bar at the bottom of a Tmux session:

# No status window
set -g status off

Although GNU Screen has the idea of sessions, you are generally using only one session at a time. Tmux gives you access to all your sessions at once, and you can switch between them. I do not know yet whether I like this, but it is tough to avoid multiple sessions. What I definitely dislike is that by default if you have two sessions, closing the last window of the first session closes Tmux, and does not get you to the next session:

# If closing the last window in a session switch to another session
# See: https://unix.stackexchange.com/questions/121527/
set-option -g detach-on-destroy off

Selecting Windows

My habit is to use <ctrl>-" (aka "windowlist -b") in Screen to select windows. In order for this to work my screens need to have useful titles.

Tmux has a similar choose-tree option, but it is much too busy, so I simplify it:

# Ctrl-a " should list windows
# You need the single quotes around the double quotes.
bind-key '"' choose-tree -F '#{window_id}: #F#W' -O index

As the comment indicates, the quotation marks around " are mandatory. This may still be too busy for me, but here are what the different flags mean:

One handy keystroke in this choose-tree view is x, which kills the window that is currently highlighted. This can be helpful in cleaning your list of windows, and as far as I know you cannot do this (easily?) in GNU Screen.

There is another technique that would be handier if it worked: t to tag windows, and then X to kill them all at once. But this does not work, possibly because of automatic renaming. It kills the wrong set of windows! (In my opinion this is inexcusable, since every window has a unique ID.)

Speaking of renumbering windows, here is the entire reason I switched to Tmux:

# Renumber windows as we close them
set -g renumber-windows on

I thought I never used the <ctrl>-a 0 through <ctrl>-a 9 shortcuts in GNU Screen to select specific windows, but I was wrong. I will discuss the shortcut I set up for this in the "Startup Config" section.

Reordering Windows

Renumbering windows is almost always the right thing to do, except when I accidentally close a window that I want to be in a particular place. In the bad old GNU Screen days I would open new windows until I filled enough gaps to get to the window I accidentally closed, and then restart the program I had accidentally shut down.

That does not work for renumbered windows, because Tmux closes the gap immediately. There are two solutions to this. The first is to open a new window, restart the program, and then bubble the window up to the right place:

# Move current window up and down
bind-key j swap-window -t -1
bind-key k swap-window -t +1

The keybindings j and k will make old-school vi users mad, but they are fine for me.

Can I move a window to the right place directly? It can be done, but it is not easy. There are a bunch of solutions listed here, but I did not like any of them: https://superuser.com/questions/343572/how-do-i-reorder-tmux-windows

I came up with a different solution. There is a command called new-window which can take an insertion index. This will insert the window after the given index. This creates a space. I can then use swap-window to swap my target window with the dummy, and get rid of the dummy. There are edge cases (I had better not name any of my actual windows "dummy", and we have to treat the topmost window specially) but it seems more sensible to me than the solutions in the superuser.com answer.

Note that Tmux has a move-window command, but it is useless: it won't work unless the target position is already a gap, which will never be the case because I am automatically renumbering windows.

I tried to get this working entirely within the tmux-config.conf file but I could not do it, because the Tmux configuration language does not provide variables or any way to remember window locations. So I gave up (sorry employers) and bound a key to launch a script:

# This lets you move a window to a new index.
# It needs to be in the same session
bind-key h command-prompt -p "which target position?" "run-shell '~/bin/tmux-move-window.sh #{window_id} #I %1'"

The command-prompt asks the user for the target position, which it sets to %1. The #{window_id} is the unique ID of the current window, and the #I is its current position.

The tmux-move-window.sh script is below:

!/bin/bash

# Paul "Worthless" Nijjar, 2021-04-20

# Move a window up to an earlier index
# $1: index of target window
# $2: current (source) position of window
# $3: destination position to move it to

# this cannot be done in tmux.conf because tmux cannot store variables
# for its local commands, so I cannot remember $1 .

# Zero is a special case
if [ $3 -eq 0 ]
then
    real_position=0
else
    real_position=$(( $3 - 1 ))
fi

if [ $2 -le  $3 ]
then
    real_position=$(( $3 ))
fi

tmux new-window -a -t $real_position -n "dummy"
tmux swap-window -s "$1" -t "dummy"

if [ $3 -eq 0 ]
then
    # Move window one up
    tmux swap-window -s "$1" -t -1
fi

tmux kill-window -t "dummy"

Titling Windows

Tmux supports automatically titling the window with the name of the program you are running, but it does not work well at all. If you are running a shell script, the title will be sh, which is not much better than GNU Screen's awful default. So I took a two-pronged approach: have Tmux set the title to the current working directory by default, and use custom scripts to set screen titles for commonly-used programs.

To set the default window titles to the current folder, I used:

# Window titles
# apparently this may be cpu intensive. Yay
set -g automatic-rename on
set -g automatic-rename-format '#{pane_current_path}'

(I do not think that getting the current path is itself expensive.)

To set the titles of screens to specific programs I have a shell script called runwithtitle.sh which lives in my ~/bin folder:

#!/bin/sh


cmd=`basename $0`

#set the screen title
# Escape all whitespace chars (\s : whitespace)
titletext=`echo "$cmd $@" | sed -e 's/\s/\\ /g'`

case $TERM in
    screen*)
        /usr/bin/screen -X title "$titletext"
        ;;
    tmux*)
        /usr/bin/tmux rename-window "$titletext"
        ;;
esac

/usr/bin/$cmd "$@"

case $TERM in
    tmux*)
        # Re-enable automatic renaming
        /usr/bin/tmux set-window-option automatic-rename on
        ;;
esac

Note my use of the $TERM variable here. This is why I need to distinguish between GNU Screen and Tmux terminals.

One important thing to know about Tmux is that if you use the rename-window functionality then by default Tmux will never automatically rename windows again. I thought this was no big deal but it turns out it is: if I name a window "vi somefile" then the window keeps that title even when I stop editing the file. So the final case statement in the script allows renaming.

To use runwithtitle.sh I symlink it to binaries I want:

cd ~/bin
ln -s runwithtitle.sh vi
ln -s runwithtitle.sh w3m
ln -s runwithtitle.sh man

Of course, this only works for binaries in /usr/bin. Maybe there is a better global solution, but this works well enough for me, most of the time.

I have a few other scripts that also set screen titles, such as d, which I use to search DuckDuckGo from the command line:

#!/bin/sh

# Wow this is fragile. I had better not have any tabs
# in the query.

# putting double-quotes around phrases I want to keep does not
# work unless I single-quote expressions. This is because
# of how Bash parses arguments

changedargs=`echo $@ | sed -e "s/ /+/g"`
newtitleargs=`echo $@ | sed -e "s/ /\\ /g"`
escquotes=`echo $newtitleargs | sed -e 's/"/%22/g'`

#set the screen title
  case $TERM in
      screen*)
          /usr/bin/screen -X title "d $newtitleargs"
          ;;
      tmux*)
          tmux rename-window "d $newtitleargs"
          ;;

  esac

/usr/bin/w3m "https://duckduckgo.com/?q=$changedargs"

I am not happy that I am (mostly) copy and pasting the code to set screen titles across scripts, but I have not fixed it yet. It has already bitten me a few times.

The nice thing about these titles is that they include both the name of the binary and the rest of the command line arguments.

Copy and Paste

I was not able to get the keybindings for copy and paste identical to screen. <ctrl>-a [ to enter copy mode and <ctrl>-a ] to paste work fine, but selecting is a pain. In GNU Screen I use the <enter> key to both start and end a selection, but as far as I know Tmux needs me to use different keys to start and end a selection, so I cannot reuse <enter>.

The Tmux solution to this is to use <space> to start a selection and <enter> to complete it, but this totally does not work with my muscle memory. I kept hitting <enter> and exiting copy mode.

Switching the keybindings works a little better for me, but it is not great:

bind-key -T copy-mode-vi Space send-keys -X copy-selection-and-cancel
bind-key -T copy-mode-vi Enter send-keys -X begin-selection

bind-key -T copy-mode Space send-keys -X copy-selection-and-cancel
bind-key -T copy-mode Enter send-keys -X begin-selection

Now <enter> starts the selection. Then when I inevitably press <enter> a second time to finish the selection, nothing gets copied, but I stay in copy mode. I can then realize my mistake, go back over the text I intended to copy, and remember to press <space> and not <enter> a third time. This is not a great solution (often I press <enter> a third or fourth time) but it is much less frustrating than getting kicked out of selection mode and having to start again from scratch.

One difference in copy-and-pasting from GNU Screen: in GNU Screen I can copy and paste some text, and it will paste without a trailing newline; in "vi" copy mode, it will include a trailing newline (see https://github.com/tmux/tmux/issues/61).

The fix is unsatisfying: use Emacs copy mode, and remap keys, because for some reason Emacs copy mode does not include the extra newline (on Tmux 2.8, anyways):

set-window -g mode-keys emacs

# Make emacs mode more like vi
bind-key -T copy-mode '$' send-keys -X end-of-line
bind-key -T copy-mode ^ send-keys -X back-to-indentation
bind-key -T copy-mode q send-keys -X cancel

If you use more sophisticated copy-and-pasting than I do you can map more of the commands to Emacs mode.

Startup Config

When I start GNU Screen I like to have some standard windows always running (my email, some windows for Watcamp data entry, a few text files). Tmux supports this functionality, but you had better not put it in the main tmux-config.conf file, because every time you load that file you will recreate your startup windows. So instead I have a second config file called tmux-init.conf. Here is a simplified version of that file:

source-file ~/bin/tmux-config.conf

new-session -A -s "+stable" -n "Yahoo Mutt" mutt
new-window -n "UW Mutt" uwmutt

new-window -n "todo" /usr/bin/vim /home/pnijjar/todo/todo.txt
new-window -n "/usr/bin/newsboat" newsboat 

new-window -n "kwlug" /usr/bin/w3m https://kwlug.org
select-window -t "+stable:0"

The first thing the file does is source the keybindings file. Then it starts making new windows. First it creates a session called "+stable" running mutt. At first I was trying to do this in two steps:

new-session -A -s "+stable" 
new-window -n "Yahoo Mutt" mutt

but then there would always be a blank window at position 0.

The name is "+stable" because "+" comes earlier than any number or letter in the ASCII table, so will show up first in choose-tree.

Note that to launch vim and w3m I specify the full paths. This is so that I am not using the symlinked versions in ~/bin, which messes up the window titles in frustrating ways (a bunch of titles shift down to the next window!)

For some reason Tmux cares about sessions a lot, so I explicitly made a named session called "+stable" for the windows I want on startup. (A bunch of other windows end up there too. Oh wells.)

The -A of attach-session means "attach to an existing session called +stable if there is one." This should never happen, I think.

As previously hinted, it turns out that I press <ctrl>-a 0 to get to my email, so I have a keybinding in tmux-config.conf for this:

# Go to mail
unbind-key 0
bind-key 0 switch-client -t +stable \; select-window -t +stable:0

This is a bit complicated because I might have many sessions open, but I have learned that when I type <ctrl>-a 0 I always want to go to my mail in the "+stable" session. So this keybinding first switches to that session, and then goes to window 0, which is hopefully my email.

I also made two shell scripts that launch Tmux. The first script launches with my initial windows:

#!/bin/bash

tmux -f ~/bin/tmux-init.conf

and the second script just starts a plain session:

#!/bin/bash

tmux -f ~/bin/tmux-config.conf

Notable Differences

When starting a new screen Tmux reads from ~/.profile and GNU Screen only looks at ~/.bashrc . I actually like this change.

Because every window can see every session, you run into problems where the Tmux screen is the size of the smallest terminal. On the other hand, being able to access Tmux simultaneously from different terminal sessions is kind of neat.

When using choose-tree to select a window, you get a preview of the window contents. This is okay by me, but you can disable the preview if you want.

Misfeatures and Failures

The muscle memory failures for copy and pasting bugs me.

When I use sudo to become root then I want that to show up in the screen title along with the current path of the root user, but I am not sure how to do this. In GNU Screen I had root overwrite $PS1, and then used the value of $PS1 as the screen title, but I would like to avoid this. I can symlink sudo to runwithtitle.sh, but this is not a complete solution.

Tmux seems to leak a lot more information than GNU Screen. For example, there is an extensive copy buffer history.