Pykak: Script Kakoune with Python. Over 10x lower latency than using %sh{}

When creating Kakoune plugins, I frequently felt guilty starting an entire process using %sh{} to accomplish a task as simple as multiplying two numbers. This is what inspired me to create pykak, a plugin that allows plugin authors to script Kakoune with Python. The implementation relies on IPC instead of %sh{} (besides the initial call to start the pykak server).

Here’s an example that sorts selections:

def sort-sels %{ python %{
    sels = sorted(valq('selections'))
    keval('reg dquote %s; exec R' % quote(sels))
}}

And here’s an example plugin: GitHub - tomKPZ/counted.kak: Alternative key counts for Kakoune

Please give it a try and let me know what you think. I’d love to get some feedback!

14 Likes

This is pretty amazing, congrats! I am also learning a lot by inspecting the way you implemented things.

One question: Can some your custom fifo logic be replaced by $kak_command_fifo and $kak_response_fifo introduced recently? I can’t say I understand your implementation well enough to say myself but I was curious to see no mention.

2 Likes

Hey, you made kakoune-smooth-scroll! I used your socket code in pykak, thanks for the code!

Command/response fifos are only available during %sh evaluations and get cleaned up when %sh ends. Since the python command shouldn’t use %sh, we create our own fifos when the pykak server is started.

1 Like

Impressive. I love the use of (two!) fifos to keep communicating with kakoune synchronously and that arbitrary kakoune data can be queried inside it. I have tried to make something like this so many times and have never been satisfied. I was so inspired by your work that I had to make my own spin on it. I reimplemented it as a library that can be imported in python. This way kakoune commands can be defined using decorated python functions, like this:

import libpykak as k
@k.cmd
def sort_sels(): 
    '''Sort the selection contents.'''
    sels = sorted(k.valq('selections'))
    # Put the selections into the default paste register,
    # then execute `R` to replace the selections.
    k.keval('reg dquote %s; exec R' % k.quote(sels))

When k.cmd is run the connection is initialized using the environment variable kak_session. It can be explicitly initialized by first running k.KakConnection.init("my-session").

The python function can be reimplemented using this:

import libpykak as k 
@k.cmd
def python(*args):
    args = list(args)
    code = args.pop()
    exec(code, globals(), {'args': args})

I have added the other details of exposing the same api and caching the python code objects in my repo:

6 Likes

That looks really cool, I especially like the use of a decorator to define kak functions from python! With that change, I’d be able to write counted.kak in pure python.

Have you considered upstreaming your changes?

I wish I understood how it works. I’m trying to wrap my head around it, but no luck so far. Why there are two FIFOs for example. All the Kakoune script magic does not help :sweat_smile:

Beside me being dummy: this is a great plugin. Seems to be the best way to avoid escaping hell when writing complex plugins. Performance is a cherry on top. I should experiment with it more.

This is very useful, thanks.
We’re moving towards doing something similar in kakoune-lsp. The trick with two alternating fifos is great; we used to always delete and recreate the same fifo but that touches the disk on systems where /tmp is not tmpfs.
Unfortunately lsp has some scenarios where a fifo read will hang so it’s still using a shell process, to avoid data loss (if it hangs the user can ctrl-c out). Since we already use a shell we can also use that to add a content-length header to each message, so no need for the alternating fifofs.
A shell process on InsertIdle should be very much tolerable, in my testing it was the disk access that was slower (strangely even when just writing to a fifo) on virtualized+underpowered systems.

Hmm maybe we don’t even need the content-length header since sh 'echo >fifo' means the fifo writer is a unique process everytime, so the kernel stream will not be shared; there is no crosstalk between multiple sh processes writing to the fifo; and hopefully also not the in the reader (who has a loop doing blocking reads from the fifo).

Is it feasible to add ctrl-c functionality to reading from the fifo direcly in kak?

On latest master, <c-g> cancels a fifo-open. Not relevant anymore for kakoune-lsp which moved to nonblocking fifos because there were some problems, especially on macOS.