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.

Update: After posting this to the Kakoune user forums, I got some great feedback. (Thank you Screwtape and François). In response to the feedback, I’m updating this post to describe two strategies for achieving this functionality:

  1. By executing native Kakoune commands to count word selections in a draft buffer context.
  2. By shelling out and using the wc utility to count words.

My first attempt at this was the second strategy. It’s simple enough, albeit rough around the edges. It remains a nice, simple example of how powerful Kakoune’s integration with the shell can be. But seeing François’s shell-less solution is an even better example of how powerful Kakoune can be, fullstop.

Conditionally adding this conditional functionality

Before I start, I know that 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:

define-command load-local-config -hidden \
    -docstring "Loads the .kakrc in project root directory if it exists." \
    -hidden \
%sh{
    local_kak_config="$(pwd)/.kakrc"

    [ -f "$local_kak_config" ] &&  \
        sh -c "echo 'try %{ source \"$local_kak_config\" } \
            catch %{ echo -debug cannot autoload \"$local_kak_config\" }'" _ {} \;
}

hook global KakBegin .* %{ load-local-config }

Strategy 1

I’ll start with the first strategy: executing native Kakoune commands to count word selections in a draft buffer context.

The full script

Before I go through building this functionality line by line, here’s the full script:

declare-option -hidden int modeline_buf_word_count 0

define-command update-modeline-buf-word-count -hidden %{
    try %{
        evaluate-commands -draft %{
            execute-keys <percent> s\S+ <ret>
            set-option buffer modeline_buf_word_count %val{selection_count}
        }
    } catch %{
        set-option buffer modeline_buf_word_count 0
    }
}

hook global WinSetOption filetype=markdown %{
    set-option -add buffer modelinefmt ' [%opt{modeline_buf_word_count}] '
    hook buffer InsertIdle .* update-modeline-buf-word-count
    hook buffer NormalIdle .* update-modeline-buf-word-count
}

Declaring and using a new option

First, I declare a new hidden integer option for tracking the word count per buffer. Because I only want to count words in buffers with the Markdown filetype, I only make use of this option in buffers (using the buffer scope for set-option) that have that filetype set:

declare-option -hidden int modeline_buf_word_count 0

hook global WinSetOption filetype=markdown %{
    set-option -add buffer modelinefmt ' [%opt{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.)

If something about the initial word count calculation fail, the option is set to 0 so something is displayed for Markdown buffers.

Calculating the live word count

I want a separate selection for each word in the document. I can then count those selections and call that the word count. Using Kakoune’s default key mappings, I could do this in a few keystrokes:

  1. Select the entire document (%).
  2. Enter selection mode (s).
  3. Prepare a regular expression to capture each word as a selection: \S+.
  4. Hit </kbd> to execute the selection I prepared.

Now, using the selection_count value expansion would give me the count of selections (the word count).

Using execute-keys I can automate this as part of a command to run whenever I want the word count. So, I execute these keys and then set the option I created to be the value of %val{selection_count}:

define-command update-modeline-buf-word-count %{
    evaluate-commands -draft %{
        execute-keys <percent> s\S+ <ret>
        set-option buffer modeline_buf_word_count %val{selection_count}
    }
}

I can already test this command by running it manually. To finish up, I mark the command as -hidden since I would never manually call it. (I’ll call it automatically via hooks later.) I can also wrap this in a try/catch so that, if the commands fail, I don’t end up with a stale word count and at least have an obvious 0 value:

define-command update-modeline-buf-word-count -hidden %{
    try %{
        evaluate-commands -draft %{
            execute-keys <percent> s\S+ <ret>
            set-option buffer modeline_buf_word_count %val{selection_count}
        }
    } catch %{
        set-option buffer modeline_buf_word_count 0
    }
}

Live updating with hooks

Now that I’ve seen the functionality work, I can run the command via Kakoune hooks. I already created a global hook for when the filetype has been set to Markdown. Since that’s the right scope, I’d like to add scopes within this scope that count words whenever I’ve stopped typing, in both the normal and insert modes:

hook global WinSetOption filetype=markdown %{
    # From before:
    set-option -add buffer modelinefmt ' [%opt{modeline_buf_word_count}] '

    # Adding:
    hook buffer InsertIdle .* %{ update-modeline-buf-word-count }
    hook buffer NormalIdle .* %{ update-modeline-buf-word-count }
}

Strategy 2

Here’s the second strategy: shelling out and using the wc utility to count words. I’ll use FIFOs to communicate back and forth between Kakoune and the shell.

Compared to the first strategy, the functionality created via this strategy has a bit too much string hackery for me. That said, it’s still a good example of how to use Kakoune’s FIFO and shell expansion functionality.

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, I 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, I’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:

  1. Write the contents of the active buffer to a temporary FIFO file.
  2. Count the words in the temporary file.
  3. 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. I'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. I just
# need to count the words in it.
count="$(wc --words < "$kak_response_fifo")"

# And finally, I 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.

Last updated