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:
- Keep all the functionality I rely upon with GNU Screen
- Keep all the keybindings I rely upon the same.
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:
#{window_id}
: The unique window ID, like@32
.#F
: Window flags. This will be!
if there was a bell in the window,*
if this is the currently active window, and-
if this was the last window.#W
: the window title.-O index
: How to order the tree. This can beindex
,name
, ortime
.
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.
I also use the built-in <ctrl>-a s
to choose a particular session
without listing the associated windows. This can cut down on scrolling
when many windows are open.
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"
Setting Session Paths
When starting a new session it seems that new windows open in your home directory. Sometimes I want all new windows to open in some other folder. The following snippet achieves this:
# Ctrl-a u will change the current directory
bind-key u command-prompt -p "Which directory?" "attach-session -t . -c %1"
I think you have to use a full path for the directory name. The
command re-attaches the current session, but the -c
changes the
default directory for new windows.
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
Naming Root Windows
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. Update: Using the GNU Screen
solution works, but I do not know why. In the /root/.bashrc
I have:
# Set the screen title
case $TERM in
screen*|tmux*)
# This is the escape sequence ESC k ESC \
SCREENTITLE='\[\ekroot:\w\e\\\]'
;;
*)
SCREENTITLE=''
;;
esac
PS1="${SCREENTITLE}${PS1}"
and for some reason the config line in '~/bin/tmux-config.conf`:
set -g automatic-rename-format '#{pane_current_path}'
seems to pick this up? I am not understanding how this happens.
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.
Tmux seems to leak a lot more information than GNU Screen. For example, there is an extensive copy buffer history.
The navigation screen for commands <ctrl>-a ?
is awful. You have to
use page up and page down to navigate -- pressing the space bar just
exits the listing.