From Tmux to Kitty

Pixelated Andrew Pixelated Andrew
~16 min read

For the past several years I had been avoiding the Kitty terminal for same reason I was interested in it: the prospect of ditching my old friend Tmux.

While I really enjoy customizing the ever-living heck out of my dev setup, I’m a huge fan of stability. I’ve been using the Vim+Tmux combo exclusively for well over a decade at this point (that’s Vim with a V, not an N) and I know it very well. I usually go one to two years between doing any major work on it and there is no shortage of other things I would rather be doing than trying out new editors and terminals. But I’m also a fan of simplicity and maintaining separate Tmux and Alacritty configs doesn’t quite fit that bill. So Kitty always appealed to me, I just knew it would mean dedicating a bunch of time learning it and making sure I could get all the must-have features from Tmux, or find suitable alternatives.

Through a combination of being recently unemployed and having an Alacritty update break things, I decided it was time to at least give Kitty a try. This article serves to document the process of getting it to parity with what I had in Tmux. I will also share some initial thoughts with a quick overview for those who need it. Please note: I work almost exclusively locally so I never really used session restoration in Tmux. If you were hoping to find an answer to that here, you will not.

Table of Contents

Initial thoughts / Overview

I’ve been using Kitty for a little over a month now and am overall quite happy with it. The first obvious quality of life improvement is that cutting out a whole piece of tech simplifies configuration quite a bit. With Alacritty+Tmux, I had to maintain two separate configs in two different formats. Alacritty used YAML (well, now TOML-only which is one of the things that broke my update) and requires sending raw escape codes to Tmux. Tmux has its own config DSL which isn’t bad, but it’s full of cryptic single-letter flags. Kitty also has its own config DSL but it reads extremely well. For example, here is my config for moving window around:

map kitty_mod+ctrl+h move_window left
map kitty_mod+ctrl+j move_window bottom
map kitty_mod+ctrl+k move_window top
map kitty_mod+ctrl+l move_window right

It’s all in plain English which makes coming back to it later much easier. kitty_mod is a setting that lets you pick a Kitty modifier which can be a combination of any keys. A simple + is used to combine keys. Key sequences are denoted with >. For example, let’s say you wanted to emulate Tmux’s ctrl+b prefix, that would look like this:

kitty_mod ctrl+a
map kitty_mod>h move_window left

I’m on macos and have always just used cmd as my terminal key–even when I was on iTerm which required some ugly trickery—but I like to set kitty_mod anyway:

kitty_mod super

Another nice thing is that Kitty is clearly made with Vim in mind. Tmux had a minor annoyance in that it introduces from mismatched terminology—What Vim calls a window, Tmux calls a pane, and what Vim calls a tab, Tmux calls a window. This wasn’t the biggest deal, but it’s nice no longer having to make the mental swap. Kitty’s config even uses the same strange line continuations that vimscript does: a backslash on the following line. Most would not consider this to be a good thing, and in fact vim9script has done away with it, but I still appreciate it :) You will see this in action below.

While Kitty is super customizable through its base config language alone, you can take it way further through Python scripts called “kittens.” In fact, a large portion of Kitty is written in Python. Having managed to avoid this language for the ~35 years of computer use this has probably been the biggest downside of Kitty so far but hey, it’s by no means a deal-breaker. My brother uses it a lot so it’s been a good opportunity to learn a bit more so we’ll be a little bit more on the same wavelength when we chat. I’ll show you a kitten I wrote at the end of this post.

I’m not a big mouse-user, but I am by no stretch anti-mouse. Using it through Tmux was always a bit of pain as it would first have to switch to copy mode and selection could be a tad clunky. Since Kitty isn’t using a multiplexer, it just works as you would expect in any terminal. Honestly, though, this is pretty minor as it’s not that bad Tmux.

Finally, it’s a nice touch that Kitty lets you change its icon by simply putting a png called kitty.app.png in your config dir. This makes it easy to keep under source control. There are some nice icons out there but I got inspired to make my own. It looks like this:

A black macos icon width a thick white boarder.  There is an ascii cat: >^_^< where the first > stands out like a terminal prompt.

It’s not as flashy as some of the other available ones, but I like it. Feel free to use it if you do too!

That’s enough initial thoughts, let’s getting to config’ing.

Seamless Window Navigation

Possibly the most crucial thing for me is the ability to seamlessly move my cursor between Kitty and Vim windows with the same keybindings. Tmux has Vim Tmux Navigator and thankfully, Kitty has Vim Kitty Navigator .

Vim Kitty Navigator is a kitten along with a Vim plugin. If you use vim-plug then you can install them both in one go like so:

Plug 'knubie/vim-kitty-navigator', {'do': 'cp ./*.py ~/.config/kitty/'}

This copies two kitten files into your Kitty config dir and creates the classic ctrl+h/j/k/l Vim mappings for moving between windows. You then need to add mappings to your Kitty config. I prefer to use my kitty_mod for this especially since I frequently use the native functions of both ctrl+h and ctrl+l at the command line (backspace and clearscreen, respectively). It also frees up some prime homerow real estate for other Vim mappings.

So instead I create some pretty esoteric Vim mappings will never get in my way:

let g:kitty_navigator_no_mappings = 1

nnoremap <silent> <c-`>h :KittyNavigateLeft<cr>
nnoremap <silent> <c-`>j :KittyNavigateDown<cr>
nnoremap <silent> <c-`>k :KittyNavigateUp<cr>
nnoremap <silent> <c-`>l :KittyNavigateRight<cr>

Then I have the following to my Kitty config:

map kitty_mod+h kitten pass_keys.py left   ctrl+\`>h
map kitty_mod+j kitten pass_keys.py bottom ctrl+\`>j
map kitty_mod+k kitten pass_keys.py top    ctrl+\`>k
map kitty_mod+l kitten pass_keys.py right  ctrl+\`>l

Window Zoom

Kitty doesn’t have window zoom in name, but it does has a simple way to make it happen. Like Tmux, Kitty has layouts and one of those is the stack layout where windows within a tab are stacked on top of one another. So switching between stack and any other layout is equivalent to zoom! Let’s get this working:

First, you actually have to opt into the layouts you want. I just use splits which is the one you use when you want full control. So I enable that along with stack:

enabled_layouts splits,stack

From here, the simplest way forward is by using Kitty’s toggle_layout mappable action.

map kitty_mod+z toggle_layout stack

However, this doesn’t does quite get us to how it works in Tmux. Upon entering the stack layout, the window is re-drawn with its currently visible text anchored to the top leaving your prompt floating somewhere near the middle of the screen. Kitty has a very convenient action called scroll_prompt_to_bottom as well as another useful one called combine which lets you, well, combine arbitrary actions. So our desired behaviour can be achieved like so:

map kitty_mod+z combine : toggle_layout stack : scroll_prompt_to_bottom

The :s here can be any character (or characters!) you want to use as an action separator.

Lastly, I like to have an indicator when a tab has been zoomed. At the moment I just use a # beside the tab’s name. For this we can use tab_title_template. The value is parsed as a Python f string so we can put Python expressions within {}s in our tab title template. Mine looks like this:

tab_title_template "{' #' if layout_name == 'stack' else '  '}{fmt.fg.red}{bell_symbol}{fmt.fg.tab}{title}  "

Resizing Windows

Tmux has the concept of “directional resizing” where you can resize panes up, down, left, and right. Kitty takes an alternate approach in that where you make a window taller, shorter, wider, and narrower. This paradigm made me realize I could simplify things a by just having a two mappings, one for taller and one for wider, so I tried it for a few hours. Normally I can get used to new things like this but there were a little too many new things happening. It also made me aware of just how often I resize windows! I may try and re-train myself in the future but for the time being I just wanted what I had in Tmux back. So thanks to github user @chancez for creating the relative resize kitten .

Copy that file into your Kitty config directory then added the following config:

map kitty_mod+shift+h kitten relative_resize.py left
map kitty_mod+shift+j kitten relative_resize.py down
map kitty_mod+shift+k kitten relative_resize.py up
map kitty_mod+shift+l kitten relative_resize.py right

This doesn’t work perfectly. For example, If you have three horizontal splits, resizing the middle one moves in the opposite direction you’d expect and resizing the bottom one also moves the middle one. In practice I never have this situation so this works perfectly for me.

Broadcasting to all windows

This isn’t something I use daily or even weekly, but I do use it often enough that I didn’t want to work around not having it. Kitty ships with a kitten to do this and, once again, it works a bit differently. While Tmux lets you enter “pane sync mode” where you type at any available prompt, Kitty opens up a dedicated window with a non-shell prompt that does nothing other than broadcast its input to all other windows. You close it when you’re done.

This isn’t quite as “slick” as Tmux’s version but it turns out I like it quite a bit better. In Tmux I would add some very bright yellow highlights to my tabline to indicate that I was in this mode but even then I would sometimes forget to switch it off. This is pretty much impossible with the dedicated window.

By default, the text will broadcast to all windows, even ones in other tabs. I don’t ever want this so here’s how to confine it to only the current tab. I also use esc to quit:

map kitty_mod+shift+s launch --allow-remote-control
  \ kitty +kitten broadcast
  \ --match-tab state:focused
  \ --end-session esc

Fonts and Symbols

I use a Nerd Font and only a very minimal set of standard UTF-8 symbols as indicators. I’m not going to dive into it but there are issues where some symbols don’t render. This blog post explains it and recommends installing the Nerd Symbol font. It also gives you a whole list of character codes to map to it—Kitty gives you per-character control over which font to use which is pretty cool! Since UTF-8 suits me just fine, I have a very simple font config:

font_family BlexMono Nerd Font

symbol_map U+017F-U+1869F Menlo

This uses BlexMono for the Latin alphabet, including diacritics, and Menlo for everything else.

If your needs are different, this is a very helpful page .

Copy mode

This is where things start to get hairy for many people because, by design, Kitty does not have copy mode.

It’s an often-requested feature , but the author stands his ground. His reasoning is actually pretty sound: why implement such a feature when you could just use Vim. This would be great if there was a universally dead simple and cross-platform way to make it work. Digging into this issue was exhausting. It seems there are possibly good solutions for NeoVim but not necessarily for Vim (rude).

I’ve ultimately been flip-flopping between two solutions. One is Kitty Grab . It’s not quite as good as Tmux’s copy mode—the Vim motions are a little wonky and it lacks any kind of indication that you are in this mode (though I’m sure I could figure out how to add that) but it works. The other option, which I currently have enabled, is the standard recommendation of using Vim but, as mentioned previously, it wasn’t simple to get working. I found a solution buried in the previously linked github issue and ended up with this:

scrollback_pager vim -u NONE -
  \ -c 'w! /tmp/kitty_scrollback' 
  \ -c 'term ++curwin cat /tmp/kitty_scrollback'
  \ -c "set clipboard=unnamed"
  \ -c "hi Normal ctermbg=235"
  \ -c "nnoremap Y y$"
  \ -c "tnoremap i ZQ"

Then map a scrollback key like so:

map kitty_mod+i show_scrollback

It’s a bit convoluted and requires writing a tmp file, but it is what it is! A quick explanation of what’s going on here:

There is some weirdness with it—cat adds an extra blank line at the end (not a big deal but some people were complaining about it) and if there is a lot of scrollback, it can be jarring while it loads it all in but overall it works.

I am aware of vimpager but I couldn’t get it to work 🤷

Otherwise, since just regular scrollback works without a pager, I have cmd+ctrl+y and cmd+ctrl+e to scroll up and down (mimicking Vim) and, as previously mentioned, highlighting with the mouse is a pleasant experience since we’re not in a specific copy mode. So this is always an option too if you don’t completely shun the mouse.

Layouts and (lack of) Sessions

As mentioned earlier, I don’t use predefined “auto” layouts, nor did I use Tmux’s session restoration. For this reason, this section will veer more into my personal workflow and out of scope of Kitty-Tmux parity. If that doesn’t interest you, you can safely skip to the conclusion or, you know, just stop reading here!

While I don’t use auto-layouts, I do always work in one of manual layouts. I sometimes make temporary new windows, but I prefer to have full control over where those are. My two mappings for creating Kitty windows are:

map kitty_mod+_ launch --location=hsplit --cwd=current
map kitty_mod+| launch --location=vsplit --cwd=current

These are very similar to my Vim mappings:

nnoremap <silent>  _ :split<CR>
nnoremap <silent> \| :vsplit<CR>

When it comes to session restoration, I really only care about my Vim sessions and for that, I have been a very long time user of obession.vim . When it comes to terminal window layouts, I’m usually able to work very compactly in one tab per project.

All my web projects have this layout:

~ ~ ~ ~ ~ ~ $ s i e e r x v > e r o u t p u t

Vim is in the top. I have a server log running in the bottom right corner and since I exclusively work on Elixir projects these days, that also doubles as a REPL. On the bottom left for misc things. Sometimes I open a dedicated REPL in there, or a db client—though I mostly use DadBod and DadBodUI —and of course I often just run commands there when I want easy reference to their output. I do also sometimes use Vim’s terminal if I’m feeling saucy and of course use :!.

For non-web dev stuff, I use a 2-window tab like so:

~ ~ ~ ~ ~ ~ $

And finally I sometimes use just one window, though I don’t need to show you what that looks like.

To get myself a new tab for a project in any of these layouts, I made myself a kitten. I invoke it with one of cmd+1, cmd+2, or cmd+3 for one, two, or three windows, respectively. It prompts me for a project’s path, or partial path, then in a new tab starts opening each window and calling autojump with the path. It also auto-opens Vim in the main window as I almost never don’t want that.

Sometimes I have to give autojump more context to get the right directory. For example, I have a directory called playground/ full of subdirectories for many different programming languages I like to try things out in. So if I want a new tab in playground/elixir I’ll type play elixir, but I’d rather the tab just be called elixir. Using : as a delimiter, I can provide a name for the tab, eg: play elixir:elixir. I considered splitting on space and taking the last token as the name but I decided I wanted the extra control for now.

The kitten looks like this:

def main(_args):
    location_and_maybe_name = input('New tab: ')

    return location_and_maybe_name

def handle_result(args, location_and_maybe_name, target_window_id, boss):
    num_windows = int(args[1])
    pieces = location_and_maybe_name.split(':')
    location = pieces[0]

    if len(pieces) > 1:
        title = pieces[1]
    else:
        title = location

    window = boss.window_id_map.get(target_window_id)

    if window is not None:
        boss.call_remote_control(window, ('action', 'new_tab'))
        boss.call_remote_control(window, ('action', 'set_tab_title', title))
        _set_win(boss, window, location)

        if num_windows > 1:
            boss.call_remote_control(window, ('launch', '--type=window', '--location=hsplit'))
            _set_win(boss, window, location)

            if num_windows > 2:
                boss.call_remote_control(window, ('launch', '--type=window', '--location=vsplit'))
                _set_win(boss, window, location)
                boss.call_remote_control(window, ('action', 'previous_window'))

            boss.call_remote_control(window, ('action', 'previous_window'))
            boss.call_remote_control(window, ('action', 'resize_window taller 20'))
            boss.call_remote_control(window, ('send-text', 'e\n')) # `e` is my terminal alias for vim

def _set_win(boss, window, location):
    if location != "":
        boss.call_remote_control(window, ('send-text', f'autojump {location}\n'))

Then in my Kitty config I have the following:

map kitty_mod+1 kitten new_tab.py 1
map kitty_mod+2 kitten new_tab.py 2
map kitty_mod+3 kitten new_tab.py 3

The whole thing is quite bone-headed, really. My initial version shelled out to autojump then passed the returned path to Kitty’s launch command to create each window but it didn’t work outside of development mode. I’m assuming it has to do with this issue .

As stated earlier, I don’t know Python very well and there is a still a lot for me to learn about Kitty when it comes to even grokking how scripting it actually works. For that reason I’m not going to do a deep dive into what’s going on here, I just wanted to share the kitten.

Conclusion

Hopefully this helped you get Kitty more on par with your Tmux setup. My only real sticking point is scrollback which I’m hoping I can find a better solution for at some point, but overall I’m super happy with Kitty and hoping to stick with it for the next several years at least. I also still have a lot to get used to in terms of Kitty’s scripting model but that will come with time. For now I’m at a good place with it right now so it’ll likely be a while before I dive in again. But we’ll see!

Appendix

Seamless Window Navigation

Install: Vim Kitty Navigator

" vimrc

let g:kitty_navigator_no_mappings = 1

nnoremap <silent> <c-`>h :KittyNavigateLeft<cr>
nnoremap <silent> <c-`>j :KittyNavigateDown<cr>
nnoremap <silent> <c-`>k :KittyNavigateUp<cr>
nnoremap <silent> <c-`>l :KittyNavigateRight<cr>
# Kitty config

map kitty_mod+h kitten pass_keys.py left   ctrl+\`>h
map kitty_mod+j kitten pass_keys.py bottom ctrl+\`>j
map kitty_mod+k kitten pass_keys.py top    ctrl+\`>k
map kitty_mod+l kitten pass_keys.py right  ctrl+\`>l

Window Zoom

enabled_layouts splits,stack
map kitty_mod+z combine : toggle_layout stack : scroll_prompt_to_bottom
tab_title_template "{' #' if layout_name == 'stack' else '  '}{fmt.fg.red}{bell_symbol}{activity_symbol}{fmt.fg.tab}{title}  "

Resizing Windows

Copy this kitten into your Kitty config dir then add the following to your config file:

map kitty_mod+shift+h kitten relative_resize.py left
map kitty_mod+shift+j kitten relative_resize.py down
map kitty_mod+shift+k kitten relative_resize.py up
map kitty_mod+shift+l kitten relative_resize.py right

Broadcast to all Windows

map kitty_mod+shift+s launch --allow-remote-control
  \ kitty +kitten broadcast
  \ --match-tab state:focused
  \ --end-session esc

Symbols

font_family BlexMono Nerd Font

symbol_map U+017F-U+1869F Menlo

Copy Mode

Either use Kitty Grab or use Vim as a pager:

scrollback_pager vim -u NONE -
  \ -c 'w! /tmp/kitty_scrollback' 
  \ -c 'term ++curwin cat /tmp/kitty_scrollback'
  \ -c "set clipboard=unnamed"
  \ -c "hi Normal ctermbg=235"
  \ -c "nnoremap Y y$"
  \ -c "tnoremap i ZQ"

map kitty_mod+i show_scrollback