Adding a live word count to the modeline

Yesterday, I decided I wanted a live word count in my modeline. Specifically for when I’m editing Markdown files. I hadn’t touched the modeline, or any KakScript, in quite a while, so I wrote this all out as a blog post for myself. Hopefully, this will help me keep some of this stuff in my head a little longer. And maybe someone reading this will also find it useful.

If you have any feedback or criticism on the blog post, I welcome it. Here’s the link:

https://benjaminwil.info/weblog/kakoune-modeline-word-count/

3 Likes

It took me a little while to figure out what was going on with the command FIFO and response FIFO interaction, but it makes sense. I might have written the buffer to a temporary file, or copied the buffer to a scratch buffer, piped the whole thing to wc, then yanked the result, but your way is just as good and probably more straightforward.

One thing that did confuse me was under the heading “Conditionally adding this conditional functionality”. In your example code you have evaluate-commands %sh{ ... } and inside the shell block, you launch another shell just to run echo? Couldn’t you just do something like:

try %{
    source "%val{client_env_PWD}/.kakrc"
} catch %{
    echo -debug cannot autoload "%val{client_env_PWD}/.kakrc"
}

…and skip the shells entirely?

2 Likes

Thanks so much for the feedback, @Screwtapello! I intend to edit and improve this blog post. Do you mind if I credit/thank you in it?

Interesting, I hadn’t considered using a scratch buffer. I may want to try that strategy for some other things in the future.

I do think using the Kakoune-managed FIFOs is pretty straightforward, and maybe more “secure”? (I could be mistaken. It seems like using $kak_response_fifo means that the tempfiles are deleted as soon as the shell expansion has completed, whereas using mkfifo may leave them on disk for some time.)

Thanks for pointing out that issue with my .kakrc loader. I actually think I want to revisit that for a couple of reasons:

  1. I like your proposed version better, obviously.
  2. I actually don’t want the cannot load "<filename>" debug message to be displayed if the file doesn’t exist; I only want it to be displayed if the file exists but could not be loaded due to an error inside of it.

I am not sure if I knew about %val{client_env_<variable>}, and that’s going to be really nice. I just quickly adapted this loader from the loader I use for getting all my Kakoune runtime configuration (i.e. a find -L "$kak_runtime/autoload" -name "*.kak" -exec ...), which I believe needs to be inside of a %sh{}. If anyone can think of a clever way to do a full runtime *.kak load without a shell expansion, let me know.

See the full $kak_runtime loader; kinda off-topic
evaluate-commands %sh{
    find -L "$kak_runtime/autoload" -name "*.kak" -exec \
        sh -c 'echo "try %{ source $1 } \
             catch %{ echo -debug cannot autoload $1 }"' _ {} \;
}

Do you mind if I credit/thank you in it?

I don’t mind at all!

It seems like using $kak_response_fifo means that the tempfiles are deleted as soon as the shell expansion has completed, whereas using mkfifo may leave them on disk for some time.

That is true. If you use mkfifo, you can clean them up yourself with the trap command, but it’s convenient to let Kakoune do the cleanup.

I only want it to be displayed if the file exists but could not be loaded due to an error inside of it.

Ah, Kakoune indeed doesn’t provide a way to test for the existence of a file, apart from launching a shell to run test -f or whatever. So I don’t think it’s possible to meet your new functional requirements without at least one shell process.

If anyone can think of a clever way to do a full runtime *.kak load without a shell expansion, let me know.

The system-wide kakrc uses the find shell command to load the standard library, so that’s not a bad way to do it.

Thanks again for your feedback. I made some small tweaks to the article as a result.

I’ve been using this without issue for over a week now. It’s nice. If anything, I would like to generalize the abstraction for conditionally adding other things to my modeline easily. :slightly_smiling_face:

1 Like

I’m wondering if you cannot get rid of the shell call by using a long living process and writing to a fifo.

@alexherbo2 Don’t I still need the shell process for wc? I’m curious if you have an example of what you mean, even if it’s for some other piece of functionality.

@alexherbo2 @benjaminwil

If the idea is to remove shell calls (and I do think that in some cases, this is a good idea), why not stay inside Kakoune and select the whole buffer, then all words (i.e., \S+) in a draft context? Word count will be available as %val{selection_count} (unless the selection command fails, of course, in which case word count should be 0).

EDIT: The idea would be to have a dedicated option, word_count, and update it on a hook. The updating command would try to select %, then \S+, and (if succeeding), set word_count to %val(selection_count); otherwise (i.e., on catch), set word_count to 0.

2 Likes

@ftonneau Ah, great idea. I was able to swap out the version in my blog post to one that pretty much does what you suggested:

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 %{
    try %{
        evaluate-commands -draft %{
            execute-keys '%s\S+<ret>'
            execute-keys ':set-option buffer modeline_buf_word_count_formatted '\
                '" [%val{selection_count}] "<ret>'
        }
    } catch %{
      set-option buffer modeline_buf_word_count_formatted ' [0] '
    }
}

hook global WinSetOption filetype=markdown %{
    hook buffer InsertIdle .* %{ update-modeline-buf-word-count }
    hook buffer NormalIdle .* %{ update-modeline-buf-word-count }
}

I think it would be nice to:

  • Use an int option for word_count instead of building a formatted string.
  • Not need to use execute-keys to specify the set-option in the draft context.

But it seems like both of those things aren’t easy to do. (At least until I brush up on more KakScript?)

It’s nice that even a document with 400,000 words in it, this method is just as performant as the wc --words was. I would like to update my blog post with this other method of achieving this functionality. @ftonneau is it okay if I credit you for the help, as well?

Glad to read your information about performance.

About your questions: adapting your notation, I solved them below for you :slight_smile:

declare-option -hidden int modeline_buf_word_count 0
set-option -add global modelinefmt ' - W:%opt{modeline_buf_word_count}'

define-command update-modeline-buf-word-count %{
    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=.* %{
    hook buffer InsertIdle .* update-modeline-buf-word-count
    hook buffer NormalIdle .* update-modeline-buf-word-count
}

As you can see, there is no need to declare the word-count option as a string; an int is fine because the modeline itself can take care of string formatting. Neither is there any need to prefix set-option buffer... with execute-keys. set-option ... is perfectly fine as a command, that you can insert as is into your tryblock just as you did in the catch block.

A few comments on execute-keys ...: personally, I prefer to cut down on quoting. So, I removed the single quotes, but for this to work I had to replace the literal % by its keyname, <percent> (otherwise Kakoune’s parser will complain). For a list of valid keynames, see :doc mappings. Also, it is legal to insert blanks between keynames, as I did to increase legibility.

Yes, feel free to credit :-).

Btw 1: I replaced the markdown filetype by a regex matching anything, as I was in a hurry to test the script. You’ll probably want to replace .*by markdown.

Btw 2: If you want word counts to be available only in markdown documents, you should probably move set-option -add global modelinefmt into hook global WinSetOption ... EDIT: in that case, globalshould be replaced by buffer.

1 Like

@ftonneau Thanks so much. It’s valuable for me to see these kinds of examples.

Btw 2: If you want word counts to be available only in markdown documents, you should probably move set-option -add global modelinefmt into hook global WinSetOption ... EDIT: in that case, globalshould be replaced by buffer.

Yeah, this is so much nicer than conditionally setting an empty string via an option declared as a string :smiling_face_with_tear:

 declare-option -hidden int modeline_buf_word_count 0
-set-option -add global modelinefmt ' - W:%opt{modeline_buf_word_count}'

# ...

 hook global WinSetOption filetype=markdown %{
+    set-option -add buffer modelinefmt ' - W:%opt{modeline_buf_word_count}'
     hook buffer InsertIdle .* update-modeline-buf-word-count
     hook buffer NormalIdle .* update-modeline-buf-word-count
 }
1 Like

I significantly updated the post to describe both my initial strategy and the much-simpler strategy using %val{selection_count}. Thanks again, everyone, for the feedback.

2 Likes