POC: Configuring Kakoune in Guile

What if Kakoune was turned into a Lisp machine?

I modified this code to connect Kakoune to a guile script that sets up a repl on startup. So the basic setup in Kakoune is the following:

declare-option str guile_socket
declare-option -hidden str guile_pid
declare-option -hidden str guile_output_file

eval %sh{
	# kak_client
	# kak_session
	guile_output=$(mktemp)
	printf %s%s\\n "set-option global guile_output_file " "$guile_output"
	~/kakoune.scm >> $guile_output 2>&1 &
	pid=$!
	printf %s\\n "set-option global guile_pid '$pid'
		hook -once -group guile global KakEnd .* 'guile-stop-repl $pid'"
}
eval %{
	edit -fifo %opt{guile_output_file} guile-compile-log
}
eval %{
	edit -scratch guile
}


define-command -params 0..1 guile-stop-repl %{
	evaluate-commands %sh{
		if [[ -z $1 ]] ;then
			kill $kak_opt_guile_pid
		else
			kill $1
		fi
		rm $kak_opt_guile_socket
		printf %s\\n "echo $kak_opt_guile_pid
		set-option global guile_pid ''
		set-option global guile_socket ''"
	}
}

define-command guile-evaluate -params 1 -docstring \
  "Evaluates the given string in the context of the current guile session" %{
	guile-write-to-buffer %sh{
		# kak_client
		# Allow us to expand variables like $kak_client.
		escaped=$(echo "$1" | sed 's/"/\\"/g')
		# Expand and preserve quotes 
		cmd=$(eval "printf \"%s\" \"$escaped\"")

		printf '%s\n' "$cmd" | socat - UNIX-CLIENT:$kak_opt_guile_socket | 
			tail +9 | sed 's/\$[0-9]* = \(.*\)/\1/g'
	}
}

define-command guile-write-to-buffer -params 1 %{
	evaluate-commands -buffer guile -save-regs '"' %{
		set-register '"' %arg(1)
		execute-keys 'gep'
	}
}

define-command guile-evaluate-selection %{
	guile-evaluate %val{selection}
}

This script invokes a scheme script on startup named ~/kakoune.scm, what we want now is to create that script and have it start a repl server that we can communicate with. Here is a base to get started

#!/usr/bin/env guile
!#

;; Access variables
(use-modules (srfi srfi-98))
;; Pipes
(use-modules (ice-9 popen))
(use-modules (ice-9 ports))
(use-modules (ice-9 rdelim))
(use-modules (srfi srfi-28))
(use-modules (srfi srfi-98))
(use-modules (ice-9 threads))
(use-modules (system repl server))

(define kak-session
  (getenv "kak_session"))

(define (send-to-kakoune msg)
  (let* ((port (open-pipe (string-append "kak -p " kak-session) OPEN_WRITE)))
    (display (format "msg: ~a" msg))
    (display msg port)
    (newline port)
    (close-port port)))

(define (echo-debug str)
 (send-to-kakoune (format "echo -debug GUILE: ~a" str))
 (newline))

(define (echo str)
 (send-to-kakoune (format "echo ~a" str))
 (newline))

(define (main)
  (define socket-name (tmpnam))
  (send-to-kakoune (format "set-option global guile_socket ~a" socket-name))
  (echo-debug (format "Running for session ~a" kak-session))  
  (run-server (make-unix-domain-server-socket #:path socket-name)))

(main)

You’ll need to chmod +x this script to be able to execute it. The script stores the kak session so the repl is able to communicate back with Kakoune. send-to-kakoune is the basic function we can use, for example:

(send-to-kakoune "execute-keys -client $kak_client ghihello")

When evaluated will prepend hello to the string, $kak_client gets expanded with the escaping we do earlier.

From here, we can set up a DSL to run commands:

(define (kak! . args)
  ;; Flatten everything into strings and join with spaces
  (let ((cmd (string-join (map kakoune-arg->string args) " ")))
    (send-to-kakoune cmd)))

(define (kak-block lst)
  ;; Wraps content in `%{ ... }`
  `(%block ,lst))

(define (raw val)
  `(%raw ,val))

(define (kakoune-arg->string arg)
  (cond
   ;; #:option => -option
   ((keyword? arg) (string-append "-" (keyword->string arg)))

   ;; Block content
   ((and (pair? arg) (eq? (car arg) '%block))
    (string-append "%{ "
                   (string-join (map kakoune-arg->string (cadr arg)) " ")
                   " }"))

   ;; (raw "hello") => hello
   ((and (pair? arg) (eq? (car arg) '%raw))
    (cadr arg))

   ;; "hello" => "hello"
   ((string? arg) (string-append "\"" arg "\""))

   ;; 'echo => "echo"
   ((symbol? arg) (symbol->string arg))

   ;; 5 => "5"
   ((number? arg) (number->string arg))

   (else (format #f "~a" arg))))

And now we can define commands directly in guile

(kak! 'echo #:debug "Hello from Guile")
(kak! 'face  'global 'Default (raw "rgb:000000,default"))
(kak! 'define-command
              #:override #:params 0
              'hello
	      (kak-block (list 'echo "Hello from Guile!")))

These can then be evaluated with guile-evaluate-selection.

4 Likes

Luar already supports a Lisp so you could make a small change there to support Guile.

That’s amazing! Do you think it’d possible, e.g. to ditch out the standard rc/ scripts and rewrite a leaner and more performant stdrc to Guile? I can also imagine it must be very smooth for Guix.

You could rewrite the standard rc/ scripts to use Guile and it’d be really straight-forward with the DSL I showed but then you’re not really “leveraging” guile.

You’d want to use the guile-evaluate command in Kakoune to invoke functions from Guile, and then Guile would send results back using send-to-kakoune. This interaction could be made seamless with macros: you’d create a macro that defines a function in guile and sends the message define-command guile-evaluate (the-function args) to kakoune.

Here is a macro that supports arguments

;; This should also be extended to also allow passing
;; Along $kak_environment variables, like $kak_client, or $kak_opt_x.
(define-syntax define-kakoune-command
  (syntax-rules ()
   ((_  (name args ...) body ...)
    (begin
      ;; Define the Guile function
     (define (name args ...) body ...)

     ;; Define a kakoune command that invokes the guile function 
     ;; using guile-evaluate
     (let* ((command-name (symbol->string 'name))
            (arg-list '(args ...))
            (param-count (number->string (length arg-list)))
            (arg-refs 
             (map (lambda (i)
                    ;; Result: ""%arg{1}"", double quotes escapes
                    (string-append "\"\"" "%arg{" (number->string i) "}\"\""))
                  (iota (length arg-list) 1)))
            (guile-call 
             (string-append "(" command-name
                            (if (null? arg-refs)
                                ""
                                (string-append " " (string-join arg-refs " ")))
                            ")"))
            (cmd (string-append
                  "define-command -override -params " param-count " " command-name
                  " %{ guile-evaluate \"" guile-call "\" }")))
       (send-to-kakoune cmd))))))

;; And now we can do this to communicate.
(define-kakoune-command (greet name)
 (send-to-kakoune (string-append "echo " "-debug " name "!")))

And yeah Guix was my primary motivation for trying this out. You also have very simple IPC in Alacritty and sway, so it’s similarly trivial to have Guile talk with those systems too. So potentially you can have a “Lisp machine” that talks with Kakoune, Guix, Alacritty, and Sway and are configured together.

2 Likes

I can see an advantage of Guix over NixOS here that it’s possible to use an actual programming language (Guile) to define both the system’s config and also as an integration language between runtime services. While Nix serves only as a configuration language, for better or worse.

By the chance, is it easy to setup something like GitHub - nix-community/impermanence: Modules to help you handle persistent state on systems with ephemeral root storage [maintainer=@talyz] in Guix?

Apologies for an offtopic, but interesting to know.

1 Like

I haven’t really attempted it, though a cursory google leads me to believe it’s possible but not as mature as Nixos

1 Like

So I have an updated version now that uses FIFO to avoid starting a shell script on each invocation.

https://git.sr.ht/~marcc/guix-redux/tree/main/item/lisp-machine/kakoune.scm
https://git.sr.ht/~marcc/guix-redux/tree/main/item/dots/.config/kak/autoload/guile.kak
https://git.sr.ht/~marcc/guix-redux/tree/main/item/dots/.kak-init.scm

Now guile.kak runs .kak-init.scm on start-up which in turn starts a fifo-listening server in kakoune.scm, all messages from kakoune are sent to this fifo server instead. It makes the syntax for sending messages to guile rather clean:

echo -to-file %opt{guile_fifo} "(my-command-for-example-exit)"

The server connects directly to the kakoune session via sockets to avoid invoking kak -p session.

The next step might be implementing synchronous write/read, similar to pykak, since it’s quite a clean approach to accessing variables. ATM all information (env vars) need to be sent up front and then the response is sent async.

2 Likes

anemofilia on codeberg has a Guix setup that aims to be impermanent as per Nix definition, and also has a kakoune setup therein.

1 Like