How to write an asynchronous plugin?

I’m attempting to write my first plugin - followname - which tracks files as they get moved or renamed. I’ve done the hard part of making a program which watches for rename events using inotify and outputs the new name to stdout when they occur, but I’m having trouble getting Kakoune to interpret that information.

At first I thought it would be as simple as this:

hook global BufCreate .* %{
    evaluate-commands %sh{
        $HOME/src/followname/followname $kak_buffile | awk '{print "edit "$0}'
    }
}

But of course the %sh expansion can’t get evaluated until the program terminates, so I learned about $kak_command_fifo which seemed like the answer. But this is as far as I manged to get:

define-command followname '
    nop %sh{
        $HOME/src/followname/followname $kak_buffile | while read line; do
        	echo edit \"$line\" > $kak_command_fifo
        	echo info \"moved to $line\" > $kak_command_fifo
        done
    }
'

When I run this command I can see it working, Kakoune jumps to the new file whenever I rename it, but it locks interaction with waiting for shell command to finish. (It also spawns a new process every time the file is renamed, but that’s another matter)
I found this post from 2019 which lists some hacks for running processes in the background, with that I was able to get this monstrocity:

define-command followname '
    nop %sh{ {
        $HOME/src/followname/followname $kak_buffile | while read line; do
        	echo "eval -client $kak_client edit \"$line\"" | kak -p ${kak_session}
        	echo "eval -client $kak_client info \" moved to $line\"" | kak -p ${kak_session}
        done
    } > /dev/null 2>&1 < /dev/null & }
'

Which almost works; the “info” part doesn’t seem to work, and I have to run the command manually as when I run it from a BufCreate hook, $kak_client appears to be empty. It also creates a new buffer every time the file is renamed and leaves to old one open, which isn’t ideal. And there’s probably at least one other dumb thing that I haven’t noticed yet.

I’m sure what I’m trying to do isn’t as complicated as I’m making it, but I don’t know enough about Kakoune to figure it out.

%val{client} is documented as running in window scope (there can be many windows per buffer). The docs are unclear, but I assume BufCreate runs in either buffer or global scope. Could you use %val{client_list} instead? e.g. send a command to each client that edits the new file if %val{buffile} is old filename.

By the way, I ran into this exact problem last week — it seems that most of the screen doesn’t want to update until the script is finished, even if you send commands over the fifo.

Welcome, and good luck with your plugin!

Unfortunately, I think you’ve run headfirst into… well, I won’t say a “weakness” but a “design constraint” of Kakoune. Kakoune is designed for synchronous plugins, with hooks that fire, make some change, then stop.

The plugins that are genuinely asynchronous, like the LSP integration, typically have an architecture like this:

  • the kakrc file launches the daemon, passing in $kak_session
  • the daemon prints a bunch of Kakoune configuration that sets up all the hooks the daemon needs
  • eventually the hooks fire, and the loaded config sends information to the daemon over a FIFO
  • the daemon decides what it wants to do with that information, and sends commands back to Kakoune with kak -p and the session identifier it started with

That might seem like overkill for what you’re trying to do, and maybe it is, but it’s a pattern that’s reliable.

…the “info” part doesn’t seem to work…

I’m not sure what’s going on there, I’m afraid.

…I have to run the command manually as when I run it from a BufCreate hook, $kak_client appears to be empty.

Yeah, a buffer doesn’t necessarily have a client associated with it. If you run kak foo.txt bar.txt baz.txt then three buffers are created, but only foo.txt is visible, so only foo.txt is associated with a particular client - the others are just hanging around off-stage.

You probably want WinDisplay, which triggers when a buffer is made visible in a client, although it happens every time the buffer is made visible.

It also creates a new buffer every time the file is renamed and leaves to old one open, which isn’t ideal.

Rather than edit (which creates a new buffer editing the named file) you probably want rename-buffer, which tells Kakoune that the current buffer actually refers to a different file path. If the file at the new path is actually the the same file as the old path, just renamed, then I think it will accept this, even if the buffer has unsaved changes, rather than trying to reload the buffer from disk.

1 Like

Here’s an example.

  1. Open a new terminal and run sh followname.sh
  2. Open a new terminal and run kak -n -e "source followname.kak"
  3. Type :edit some_file<ret> and see some_file being renamed as some_file.renamed_by_followname

followname.kak

hook g BufCreate ".*" %{
  echo -to-file "followname.fifo" -quoting "shell" "your_followname_message_type" "%val{session}" "%val{bufname}"
}

followname.sh

set -e
mkfifo "followname.fifo" "followname.kak_buflist.fifo" "followname.kak_client_list.fifo"
trap "rm -vf 'followname.fifo' 'followname.kak_buflist.fifo' 'followname.kak_client_list.fifo'" EXIT
echo "followname.fifo created"
echo "followname.kak_buflist.fifo created"
echo "followname.kak_client_list.fifo created"
echo "followname started"
while :
do
  echo "waiting for message..."
  eval set -- "$(cat 'followname.fifo')"
  message_type=$1
  case "$message_type" in
    "your_followname_message_type")
      kak_session=$2
      kak_bufname=$3
      echo "your request is: 'your_followname_message_type'"
      echo "got '$kak_session' as session name"
      echo "got '$kak_bufname' as buffer name"
      printf "echo -end-of-line -to-file '%s' -quoting 'shell' -- %%val{buflist}; echo -end-of-line -to-file '%s' -quoting 'shell' -- %%val{client_list}" "followname.kak_buflist.fifo" "followname.kak_client_list.fifo" |
      kak -p "$kak_session"
      echo "your buffer list is:"
      cat "followname.kak_buflist.fifo"
      echo "your client list is:"
      cat "followname.kak_client_list.fifo"
      printf "eval -buffer '%s' -verbatim rename-buffer -- '%s'" "$kak_bufname" "$kak_bufname.renamed_by_followname" |
      kak -p "$kak_session"
      printf "old_name=%s\\nnew_name=%s\\n" "$kak_bufname" "$kak_bufname.renamed_by_followname"
      ;;
    *)
      echo "Unknown request: '$message_type'"
      ;;
  esac
done
1 Like

WinDisplay does work, however not perfectly. It spawns a new followname process every time I change buffer. And using rename-buffer sometimes renames the *debug* buffer if I happen to have it open. Neither of those things are likely to come up very much since I don’t tend to switch buffers from within a Kakoune window, but they’d definitely keep me from wanting to publish the plugin.

The reason echo "eval -client $kak_client info \" moved to $line\"" | kak -p ${kak_session} didn’t work is because you can’t backslash escape strings in Kakoune, I ended up having to come up with a crazy workaround that I won’t divulge lest some poor soul copies it thinking it’s an actual solution. It seems unreasonably difficult until I realize that I’m embedding kakounescript inside shell script inside kakounescript, which is kind of crayon-eating behavior anyway.

Oh, and the processes don’t close when I exit kakoune. It seems like I’m going to be running into these kinds of bugs forever if Kakoune doesn’t have the ability to manage the lifetimes or processes for me.

There’s a few different concerns:

  • when the background process gets started
  • where the background process sends the “you got renamed” notification
  • where the “this buffer got renamed” notification gets displayed
  • when the background process gets shut down

For startup and shutdown, as you’ve noticed, Kakoune does not have any built-in support for long-lived helpers. Issue 4127 has some ideas for this, but it seems nobody’s come up with a really solid, convincing proposal yet, so you’ll probably have to track the lifetime manually. I believe kak-lsp configures itself to be launched for every Kakoune session, but automatically shuts down if it’s idle for half an hour, so if Kakoune crashes it cleans itself up.

Alternatively, you might make your long-lived helper write its PID to a file somewhere, and have a KakEnd hook that sends SIGTERM to that PID to shut it down.

For sending the “you got renamed” notification, one of the problems is that a buffer gets renamed, but the buffer may not be visible in any client, or it may be visible in many. Rather than trying to send a message with eval -client which might or might not happen to go anywhere useful, you probably want to send a message with eval -buffer which will execute in the context of the buffer, no matter what client it’s in. The inotify message should give you enough information to determine the new and old paths, so you can do eval -buffer $old_path rename-buffer $new_path to send the message to the right place.

For displaying the “this buffer got renamed” notification, I don’t have a good answer. I don’t know of a straightforward way to send a message from a buffer to the zero-or-more clients that might be displaying that buffer. And honestly, I think that might be a bit intrusive for my tastes, except while debugging. And for debugging purposes, “echo -debug renamed $old_path to $new_path” might be better than trying to pop up an info window, since there’s automatically a log of such messages that can be searched and checked to make sure everything’s in order.

2 Likes

Personally I’d use WinDisplay to send a message to followname that a buffer got in view and let it open a prompt to confirm renaming if any, similarly to autoreload for external file modifications. The actual tracking of files is done in other hooks.

Kakoune already displays a yellow popup when file corresponding to a buffer is modified on disk, probably already using inotify or equivalent. I’m always reluctant to ask for more function in Kakoune but if it’s straightforward to extend Kakoune’s file watch to cover renames perhaps it’s legitimate to put this function in Kakoune, given the gyrations required to do it through the Kakoune programming interfaces. That being said, I don’t usually edit files that are being renamed or moved by processes other than myself, When I move or rename it’s usually deliberately to create a backup copy so I don’t need notification. In fact I didn’t realize Kakoune ignored moves and renames… I would be interested to know more about the scenario generating the renames and moves.