I have been using Kakoune for a long time, and I want to talk about the User Experience (UX). When it comes down to editors / productivity platforms, people tend to confuse UX with User Interface (UI). Both are related, of course, but having a great UX doesn’t mean the UI is “great” and having a great UI doesn’t mean the UX is “great”: it’s all a matter of perspective.

In this blog entry, I want to talk about UX more specifically, because I do think it’s more important than UI. I will try to show that the default UX of Kakoune is incredible and that you can very quickly create a super expressive and pragmatic programmer environment. It will cover:

Once again, I’d like to greet and thank @Taupiqueur for sharing his thoughts and joy about Kakoune.

What I mean with UI and UX

UI means “anything interfacing the user to the system.” It’s both the visual depiction of the service (the menu, the colors, the fonts, etc.) and the way you interact with the system (with the keyboard, by clicking on buttons, when a system event happens, etc.).

UX means “how the user experiences the system.” For instance, something that is not UI at all but enhances the UX is having a way to filter data in a system with high volume of events with a tag. An even better UX is having fuzzy filtering with any tags. Etc.

There are many possible UI implementations for a given UX item: implementing filtering can be done with a select box in a GUI, but the UX is not great because the user is presented with a set of finite choices, and if there are many, it’s pretty annoying to scroll down the list to find the one we want. Even with this bad UX design, most of select box implementations (e.g. web) allow to press keys to jump to the entry in the select options — most of the time, you don’t see what you type -> bad UX for the user. Another possible UI implementation would be to use a free text box that would filter based on its content — it could even be live for an even better UX. Etc. etc.

Now, would you prefer a nice looking GUI with the select box, or a blander UI but with the fuzzy search box? Clearly, in terms of UX, the second option is much better. But now, imagine a GUI with the fuzzy search box. It would be pretty similar to the blander UI in terms of UX, but it would look (much) better… which is likely to enhance the UX as well!

Small disgression: TUI vs. GUI

TUI: Text User Interface, which is a program mimicking traditional GUIs in the terminal.

GUI: Graphics User Interface, the typical window-based applications you run on your machine.

Before jumping to the Kakoune content, I just want to disgress slightly to talk about something I often see something that itches me a bit: many individuals seem to say that a TUI is often written in a way that optimizes UX and GUI the UI, and hence, oftentimes, using a TUI feels much better. I agree with this (this is the reason why I’m using editors and tools inside my terminal instead of a dedicated GUI, even though I think the UI is worse, for many reasons: no pixel-perfect alignment of things; no direct integration of the application at the OS level, it has to go through the terminal and shell; etc. etc.).

However, is there anything forcing a GUI not to provide the same kind of interactions as a TUI? I don’t think so. If you make your TUI keyboard-oriented, everything you do is just listening to keyboard events provided by whatever terminal protocol / library you use… which could be done exactly the same way in a GUI.

I do think that we should be able to make a GUI as good as a TUI in terms of UX, and some programs did it. For instance, emacs can run both as a TUI and a GUI (and today, people recommend actually using the GUI). I think that could be the topic of another blog article. Let’s go back to the original matter.

Kakoune UX

Kakoune, upon installation, already provides a lot of good things in terms of UX. But as you get more productive with it, you will face some problems. For instance, the first one I came across (pretty quickly being honest, coming from Helix) was surrounding: adding, deleting and replacing surrounding delimiters. Where you need a plugin to do that in the Vim world, in Kakoune, it’s another topic. Some plugins exist to do exactly that, but honestly, read along.

Selections in Kakoune — which are so much more than “iT’s JUsT LikE muLtIcURsOrs oR a nOOb WaY Of DoINg RegEX in vIM” — change the Vim interpretation of appending and preppending. In Vim, i will start inserting on the cursor, whereas a will start inserting after the cursor. In Kakoune, i inserts at the beginning of each selection and a inserts at the end of each selection. That’s already a better UX, and it allows to do many things out of the box.

For instance, since we have the power to insert at the beginning and at the end of each selection… then pressing i' should insert a quote at the beginning of the selection… and a' will do the same at the end! So you can already have a somewhat working surrounding add operation by typing i'<esc>a'!

If you are adventurous, you can read the documentation of <a-;>, which allows to leave insert mode to normal mode for a single command, and come back to insert mode. You can use this to replicate what we did above without the <esc> key: i'<a-;>a'. Magic.

It’s a bit annoying to have to type all that, though, right? So instead, we could make a command! Commands in Kakoune are really simple, but they require reading a bit about them. I recommend the following reads if you want to dig in a bit:

So let’s wrap that sequence of keys in a command. Instead of using i and a, we are going to use P and p, which, as the name implies, paste from the default register. p pastes at the end of the each selection and P pastes at the beginning of each selection.

Surrounding pairs

What is great is that the default register, ", is selection-aware: its content will be different from one selection to another. Said otherwise, there is one " register for each selection. Hence, we can write this:

define-command -override my-surround-add -params 2 %{
  evaluate-commands -draft -save-regs '"' %{
    set-register '"' %arg{1}
    execute-keys -draft P
    set-register '"' %arg{2}
    execute-keys -draft p
  }
}

And here you go. You can now invoke :my-surround-add ( )<ret> to add parenthesis around your selections, for instance.

Kakoune has the concept of user modes, which is a nice feature allowing to declare a keyset (that will be displayed by the help in the bottom right of your screen) when entered.

declare-user-mode my-surround-add

We can make one with a bunch of mappings in that user mode:

map global my-surround-add (   ':my-surround-add ( )<ret>'         -docstring 'surround with parenthesis'
map global my-surround-add )   ':my-surround-add ( )<ret>'         -docstring 'surround with parenthesis'
map global my-surround-add [   ':my-surround-add [ ]<ret>'         -docstring 'surround with brackets'
map global my-surround-add ]   ':my-surround-add [ ]<ret>'         -docstring 'surround with brackets'
map global my-surround-add {   ':my-surround-add { }<ret>'         -docstring 'surround with curly brackets'
map global my-surround-add }   ':my-surround-add { }<ret>'         -docstring 'surround with curly brackets'
map global my-surround-add <   ':my-surround-add < ><ret>'         -docstring 'surround with angle brackets'
map global my-surround-add >   ':my-surround-add < ><ret>'         -docstring 'surround with angle brackets'
map global my-surround-add "'" ":my-surround-add ""'"" ""'""<ret>" -docstring 'surround with quotes'
map global my-surround-add '"' ":my-surround-add '""' '""'<ret>"   -docstring 'surround with double quotes'
map global my-surround-add *   ':my-surround-add * *<ret>'         -docstring 'surround with asteriks'
map global my-surround-add _   ':my-surround-add _ _<ret>'         -docstring 'surround with undescores'

Obviously, that’s not all; we would need to delete delimiters and to replace them. Deleting is actually even more straightforward. Kakoune has some native mappings to select everything inside or around a set of delimiters:

So, pressing <a-a>( (or <a-a>), same thing) will select everything around the cursor up to the next pair of parenthesis.

Note: I have personally remapped that to mi and ma, but that collides with the default meaning of the m key in Kakoune, so I will use the native mapping here instead.

We can then simply use the previous i and a command mentioned before to start editing at the beginning and end of the selection. i<del> will start insert mode at the beginning of the selection and will remove a character (left delimiter) and a<backspace> will insert at the end of the selection and remove the previous character (right delimiter). Eh, that looks like so simple it’s almost stupid. But that’s what makes Kakoune so damn good: it’s simple to reason about:

define-command -hidden my-surround-delete-key -params 1 %{
  execute-keys -draft "<a-a>%arg{1}i<del><esc>a<backspace><esc>"
}

define-command my-surround-delete %{
  on-key %{
    my-surround-delete-key %val{key}
  }
}

Because Kakoune composes really well, you can already imagine that you should be able to use the previous commands and mappings. And indeed:

define-command my-surround-replace %{
  on-key %{
    surround-replace-sub %val{key}
  }
}

define-command -hidden my-surround-replace-sub -params 1 %{
  on-key %{
    evaluate-commands -no-hooks -draft %{
      execute-keys "<a-a>%arg{1}"

      # select the surrounding pair and add the new one around it
      enter-user-mode my-surround-add
      execute-keys %val{key}
    }

    # delete the old one
    my-surround-delete-key %arg{1}
  }
}

Revisiting grepping

Ah… who has never had the problem of trying to locate something in a codebase without really knowing where to start. That happens a lot to me when working on a front-end project and taking an issue asking to fix a random page, that is broken. Usually, LSPs don’t help to discover things that are based on the final product. For instance, if you see the checkout page broken — like a <div> is missing an attribute or a tag is misplaced in the DOM, no LSP will help you locate the code that needs to be fixed. Instead, you need other tools.

What I like to do is looking at the page and looking for what I call unique tokens. For instance, a short sentence that might appear only on that page, or modal. Or a header, a title, etc. Then, using that information, I usually grep the code base. The problem is that, doing that using grep or ripgrep as CLI is pretty boring, and not very practical. Indeed, if you get many results, you are likely to try to reduce the result set by constraing more your regex. Once you have some files, you usually look in your terminal scrollback buffer until you find something interesting, then open that file in your editor.

People using something like IntelliJ products, or VS Code, or some plugins with emacs or vim might have a way to perform the search from within their editors, but again, that’s not composability: it’s extensibility, and I explained in a previous article why it’s not a good design to me.

Kakoune, on the other side, went the composition route. grep, ripgrep, etc. are all amazing tools. Kakoune comes with a bunch of what it calls tools, which are basically Kakoune commands shipped with the editor. Among those, there is one that is of interest here: the :grep command. The :grep command forwards its arguments to the underlying %opt{grepcmd} (which defaults to something like grep -RHn). Hence, :grep foo will run grep -RHn foo in a shell behind the scene, then the result will be output in a *grep* buffer. That buffer will get special highlightings, along with some keybindings, all of that provided by the grep.kak tool. If tried the command, you might have noticed that it’s basically a list of lines of the form:

<path>:<line>:<column>:<content-of-the-line>

Then, how do you think grep.kak implements _pressing <ret> jumps to the line, column and file of the line under the cursor? The cursor can be anywhere on the line. Well, it’s simple: execute-keys again! gh will put your cursor at the beginning of the line. Then, you can use f: or t: to jump to the next : — there is no need to parse anything, we can just programmatically interact with the editor!

Here, ghT:"py will go the beginning of the line, select the path and yank it to the p register. We can then do 2l to move the cursor to beginning of the <line> number, and T:"ly will yank the line number to the l register. 2l again to move to the <column> section, ten T:"cy to yank the column number to the c register. And we have everything we need. We can then just simply run the edit -existing %reg{p} %reg{l} %reg{c} command:

aoc-18.md:1:1:lol

define-command -override my-jump-for-current-line %{
  evaluate-commands -save-regs clp %{
    execute-keys -draft 'ghT:"py2lT:"ly2lT:"cy'
    edit -existing %reg{p} %reg{l} %reg{c}
  }
}

A couple of comments here:

The :grep tool implements something probably very similar to this, but it uses %opt{grepcmd}, which you can change to whatever you like. I personally use:

set-option global grepcmd 'rg --column --smart-case --sort path'

Kakoune lacks pickers… or does it?

Something that is very important having around is being able to locate files very quickly. Most editors today ship with a way to locate files:

Kakoune has a default powerful completion engine. Pressin :edit (or, for short, :e ) and then typing something will auto-complete the file in the current directory with a somewhat fuzzy algorithm. If you select a directory and type the trailing /, it will list the content of that directory and will auto complete its children. It’s a pretty nice way to start moving around with the vanilla editor.

However, Kakoune doesn’t ship more than this, because, well, it’s composable. You can use any tool you like to locate files, and then compose them with Kakoune. I personally really like fd (a Rust rewrite of find). For instance, fd --type file will locate all the files in the current directory. Piping that to a fuzzy finder would allow to jump to any file in the current directory. And Kakoune supports that. It has several commands for that, but the one you will be interested at first is prompt. It prompts the user for some text and pass the entered text to the provided commands in the %val{text} variable. It supports some switches, and one of interest for us is -shell-script-candidates. That switch accepts a shell command to execute, reading from its standard output asynchronously and displaying into the completion items. For instance, try running the following command:

prompt -shell-script-candidates ls file: %{ info "you selected %val{text}" }

It runs ls, gets the output and allows you to fuzzy complete the result. Now consider this:

prompt -shell-script-candidates 'fd --type file' open: %{ edit -existing %val{text} }

And bam. Here you have it. Fuzzy picker in Kakoune. You can map that to a command, or wrap it in a command:

define-command my-file-picker %{
  prompt -shell-script-candidates 'fd --type file' open: %{ edit -existing %val{text} }
}

If — like many people — you want that to be run when you type SPC f:

map global user f ':my-file-picker<ret>'

You can do the same thing with anything, really. Some plugins like kak-lsp uses that to show document symbols, etc.

System clipboard

By default, Kakoune comes up with registers you can yank to. However, oftentimes, you need to yank and paste from the system cliboard. Kakoune doesn’t have a “system register” as in Vim. Instead, as with the rest, you have to compose some tools with Kakoune to get that feature. And it’s pretty simple. You need to know how to yank some text in CLI. Two situations:

And then, there is nothing much more to do. We can wrap those in two commands agnostic of the platform by using the uname utility:

declare-option str extra_yank_system_clipboard_cmd %sh{
  test "$(uname)" = "Darwin" && echo 'pbcopy' || echo 'xclip'
}

declare-option str extra_paste_system_clipboard_cmd %sh{
  test "$(uname)" = "Darwin" && echo 'pbpaste' || echo 'xsel -ob'
}

And the actual commands, using the ! key (to run a shell command with the current selection piped as standard input and replace selections with its output) and <a-!> (which runs a shell command and ignore its output):

define-command extra-yank-system -docstring 'yank into the system clipboard' %{
  execute-keys -draft "<a-!>%opt{extra_yank_system_clipboard_cmd}<ret>"
}

define-command extra-paste-system -docstring 'paste from the system clipboard' %{
  execute-keys -draft "!%opt{extra_paste_system_clipboard_cmd}<ret>"
}

I personally map those as in Helix:

map global user y ':extra-yank-system<ret>'  -docstring 'yank to system clipboard'
map global user p ':extra-paste-system<ret>' -docstring 'paste selections from system clipboard'

Bonus: some tools I’ve been making

Because the UX in Kakoune is amazing, and because it composes so well, I have made a couple of binaries and Kakoune commands to enhance the experience. So far (23rd December of 2023), I have wrote those

All in all, I’m really happy with my current Kakoune setup, and as time passes, I realize it’s a nice interactive editing platform so far. For instance, at work, I have started making a k8s.kak to highlight and run commands on Kubernetes cluster from within Kakoune; and it works pretty well.

Have fun and keep hacking around!


↑ The Kakoune philosophy
kakoune
Sat Dec 23 14:15:00 2023 UTC