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.
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!
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, 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:
:doc commands to know about all the commands.
Search for ^define-command.:doc execeval, which explains what
evaluate-commands and execute-keys do.
Especially, you will want to read the part of
-draft.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.
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.
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:
<a-i> to select inside.<a-a> to select outside.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
miandma, but that collides with the default meaning of themkey 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}
}
}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:
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:
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:
-save-regs clp saves the content of the
c, l and p register before
evaluating the commands, then restores those registers afterwards.
That is required since we copy a bunch of data to those registers,
but maybe the user is already using them. -save-regs
allows us to specificy a set of registers we locally need, but we
do not want those registers to bleed outside.-draft makes the command evaluation run in
disposable context. That prevents our selections from being
changed — execute-keys runs a couple of move and goto
commands here, while we don’t really want the cursor to move.-existing fails if the file doesn’t exist.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:
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:
It runs ls, gets the output and allows you to
fuzzy complete the result. Now consider this:
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:
You can do the same thing with anything, really. Some plugins
like kak-lsp uses that to show document symbols,
etc.
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:
pbcopy and
pbpaste to respectively copy and paste from/to the
system clipboard.xclip and
xsel -ob commands to do the same.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'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
- TODO, - WIP,
- DONE, - WONTDO, - IDEA,
etc. are highlighted with specific faces.<ret> jump to the line under the cursor (or
<esc> goes back to where you were buffer). I
also included a reduce mode that helps with reducing grep
buffers or any buffer that can be directly modified.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!