Adding a live word count to the Kakoune modeline
I wanted to conditionally add the current buffer’s word count to Kakoune’s modeline. I thought I’d write down how I got this working in case you’re new to Kakoune or you’re interested to learn a bit more about how Kakoune and the shell interact with each other.
Conditionally adding this conditional functionality
I only care about the word count in some of my projects. So, for now, instead
of autoloading this functionality globally, I’ll add it to a .kakrc in the
root of the projects where I want this. In my user’s Kakoune configuration
file I load project-specific configuration like this:
evaluate-commands %sh{
local_kak_config="$(pwd)/.kakrc"
sh -c "echo 'try %{ source \"$local_kak_config\" } \
catch %{ echo -debug cannot autoload \"$local_kak_config\" }'" _ {} \;
}
The full script
I’ll take you through this line by line. But before I do, here’s the entirety of the script:
declare-option -hidden str modeline_buf_word_count_formatted ''
set-option -add global modelinefmt '%opt{modeline_buf_word_count_formatted}'
define-command update-modeline-buf-word-count -hidden %{
set-option buffer modeline_buf_word_count_formatted %sh{
echo "eval -no-hooks -verbatim write \"$kak_response_fifo\"" > \
"$kak_command_fifo"
count="$(wc --words < "$kak_response_fifo")"
printf " [${count}] "
}
}
hook global WinSetOption filetype=markdown %{
hook buffer InsertIdle .* %{ update-modeline-buf-word-count }
hook buffer NormalIdle .* %{ update-modeline-buf-word-count }
}
The only real caveat is that I’m not using any community plugins like
powerline.kak. Just the
built-in modelinefmt.
Declaring and using a new option
First we declare a new hidden string option called
modeline_buf_word_count_formatted and append it to the built-in modeline:
declare-option -hidden str modeline_buf_word_count_formatted ''
set-option -add global modelinefmt '%opt{modeline_buf_word_count_formatted}'
The option will have an empty string value to start. Soon, we’ll set the
option per buffer so that each window’s modeline will display the correct word count.
I call the option _formatted because it will contain any spaces and
decorative characters as well as the word count.
Calculating the live word count
It would be quite simple to update modeline_buf_word_count_formatted
whenever the current buffer is saved. But I wondered how much more complicated
it would be to get the live word count without a write to the active file.
Kakoune’s shell
expansions
provide access to a bunch of editor values and options as shell variables
prefixed with $kak_. That includes the absolute path to the current buffer:
$kak_buffile. To get the static, non-live word count, it’s simple to pipe the
persisted buffer contents into wc:
wc --words < "$kak_buffile"
In just two lines of shell script wrapped in two lines of KakScript, I now have the static word count for the file:
set-option buffer modeline_buf_word_count_formatted %sh{
count="$(wc --words < "$kak_buffile")"
printf " [${count}] "
}
It’s now just a matter of wrapping this in a command I can call to test:
define-command call-me %{
set-option buffer modeline_buf_word_count_formatted %sh{
count="$(wc --words < "$kak_buffile")"
printf " [${count}] "
}
}
It shouldn’t be much more complicated to get the live word count. I just
need a way to pass the contents of the open Markdown buffer to wc rather
than the contents of the persisted buffer file.
Kakoune’s command and response
FIFOs
feature provides the missing functionality. The shell expansion also provide
the $kak_response_fifo and $kak_command_fifo for the expansion’s
lifecycle. The documentation I’ve linked to really does most of the heavy
lifting already; it’s just a matter of applying the provided shell script
snippet to a word count. In short:
- Write the contents of the active buffer to a temporary FIFO file.
- Count the words in the temporary file.
- Display the formatted count in the modeline via the new option I defined.
In just three lines of shell script:
# Prepare for Kakoune to evaluate the write command. This will write
# the active buffer to the response FIFO. We'll take the literal command
# and pass it to Kakoune's command FIFO so that Kakoune can run the command
# for us.
echo "eval -no-hooks -verbatim write \"$kak_response_fifo\"" > \
"$kak_command_fifo"
# Now, the response FIFO contains the contents of our active buffer. We
# just need to count the words in it.
count"$(wc --words < "$kak_response_fifo")"
# And finally, we can print the formatted word count.
printf " [${count}] "
And again, all I need to do is wrap this in a command that sets the option:
define-command update-modeline-buf-word-count %{
set-option buffer modeline_buf_word_count_formatted %sh{
echo "eval -no-hooks -verbatim write \"$kak_response_fifo\"" > \
"$kak_command_fifo"
count="$(wc --words < "$kak_response_fifo")"
printf " [${count}] "
}
}
A small tip: if your shell expansion isn’t working how you expect, you can set
set -x and see the evaluation of each line of the script in your Kakoune’s
*debug* buffer.
Once I confirmed this was working how I expected, I added a -hidden flag
to the command because I should never need to manually call it or have it
exposed in the list of Kakoune commands.
Live updating with hooks
That’s pretty much it. Now that I have a working, hidden command, I can run the command via Kakoune hooks to update the word count in an automated way.
Because I only care about the word count for Markdown files, I set up a global hook scoped to Markdown files only. The global hook then initializes a hook per in-scope buffer to run the command. For me, it makes sense to run the command whenever I’ve stopped typing, in both normal and insert modes:
hook global WinSetOption filetype=markdown %{
hook buffer InsertIdle .* %{ update-modeline-buf-word-count }
hook buffer NormalIdle .* %{ update-modeline-buf-word-count }
}
I had to read the hooks documentation carefully, as not all hooks take
<option_name>=<new_value> scopes. (filetype is an option that provides
exactly the right scope in this case.)
A lot of the custom functionality I’ve added to Kakoune follows similar patterns to this. So, if you’re new to Kakoune, or KakScript, I hope that this example functionality can get you closer to the thing you want to add to Kakoune.