Nushell in Kakoune: An attempt being made for fun

Hey friends, so I’m attempting to write a plugin that providers a nu %{} command in Kakoune so one can write Nushell code in their Kakoune scripts. POSIX sh is fine, but sometimes you need something more powerful.

So far, I have the following for the Nushell module:

# knu mod.nu - Main module file for internal Kakoune usage
# This file exports all functions available within nu %{} blocks in Kakoune

module buffer {
  # Buffer-related functions for internal Kakoune usage
  export def "buffer file" [] {
    if ($env.KAK_BUFFILE? | is-empty) { null } else { $env.KAK_BUFFILE }
  }
  
  export def "buffer name" [] {
    if ($env.KAK_BUFNAME? | is-empty) { null } else { $env.KAK_BUFNAME }
  }
  
  export def "buffer line count" [] {
    if ($env.KAK_BUF_LINE_COUNT? | is-empty) { 0 } else { $env.KAK_BUF_LINE_COUNT | into int }
  }
  
  export def "buffer timestamp" [] {
    if ($env.KAK_BUF_TIMESTAMP? | is-empty) { null } else { $env.KAK_BUF_TIMESTAMP }
  }
  
  export def "buffer modified-p" [] {
    if ($env.KAK_BUF_MODIFIED? | is-empty) { false } else { $env.KAK_BUF_MODIFIED | into bool }
  } 
}

module client {
  # Client-related functions for internal Kakoune usage
  export def "client curr" [] {
    if ($env.KAK_CLIENT? | is-empty) { null } else { $env.KAK_CLIENT }
  }
  
  export def "client pid" [] {
    if ($env.KAK_CLIENT_PID? | is-empty) { 0 } else { $env.KAK_CLIENT_PID | into int }
  }
  
  export def "client name" [] {
    if ($env.KAK_CLIENT_NAME? | is-empty) { null } else { $env.KAK_CLIENT_NAME }
  }
  
  export def "client session" [] {
    if ($env.KAK_SESSION? | is-empty) { null } else { $env.KAK_SESSION }
  }
  
  export def "client command-fifo" [] {
    if ($env.KAK_COMMAND_FIFO? | is-empty) { null } else { $env.KAK_COMMAND_FIFO }
  } 
}

module selections {
  export def "selections curr" [] {
    if ($env.KAK_SELECTIONS? | is-empty) { [] } else { $env.KAK_SELECTIONS | lines }
  }

  export def "selections quoted" [] {
    if ($env.KAK_QUOTED_SELECTIONS? | is-empty) { [] } else { $env.KAK_QUOTED_SELECTIONS | split row ' ' }
  } 
}

# Cursor and selection position functions for internal Kakoune usage
module cursor {
  export def "cursor line" [] {
    if ($env.KAK_CURSOR_LINE? | is-empty) { 0 } else { $env.KAK_CURSOR_LINE | into int }
  }
  
  export def "cursor column" [] {
    if ($env.KAK_CURSOR_COLUMN? | is-empty) { 0 } else { $env.KAK_CURSOR_COLUMN | into int }
  }
  
  export def "cursor selection anchor-line" [] {
    if ($env.KAK_SELECTION_ANCHOR_LINE? | is-empty) { 0 } else { $env.KAK_SELECTION_ANCHOR_LINE | into int }
  }
  
  export def "cursor selection anchor-column" [] {
    if ($env.KAK_SELECTION_ANCHOR_COLUMN? | is-empty) { 0 } else { $env.KAK_SELECTION_ANCHOR_COLUMN | into int }
  }
  
  export def "cursor selection cursor-line" [] {
    if ($env.KAK_SELECTION_CURSOR_LINE? | is-empty) { 0 } else { $env.KAK_SELECTION_CURSOR_LINE | into int }
  }
  
  export def "cursor selection cursor-column" [] {
    if ($env.KAK_SELECTION_CURSOR_COLUMN? | is-empty) { 0 } else { $env.KAK_SELECTION_CURSOR_COLUMN | into int }
  } 
}

module external {
  export def sessions [] {
    ^kak -l | lines | where ($it | str length) > 0
  }

  export def session-exists [session: string] {
    sessions | where $it == $session | length | $in > 0
  }

  export def current-session [] {
    $env.KAKOUNE_SESSION?
  }

  export def send [session: string, command: string] {
    $command | ^kak -p $session
  }

  export def eval [session: string, code: string] {
    let tmpfile = (mktemp)
    send $session $"echo -to-file ($tmpfile) ($code)"
    let result = (open $tmpfile)
    rm $tmpfile
    $result
  }

  # TODO, flush this out
  export def fifo [session: string, code: string, buf_name: string = "*fifo*"] {
    let fifo: string = (mktemp | str trim)
    ^mkfifo $fifo

    send $session $"edit -fifo ($fifo) ($buf_name)"

    let line = (input "fifo> ")
    open $fifo --raw | while $line {
      if $line == "exit" {
        break
      }
      $line | save --append --raw $fifo
    }

    rm $fifo
  }

  export def get-option [session: string, option: string] {
    eval $session $"echo %opt{($option)}"
  }

  export def set-option [session: string, option: string, value: string] {
    send $session $"set-option global ($option) $value"
  }

  export def get-register [session: string, reg: string] {
    eval $session $"echo %reg{($reg)}"
  }

  export def set-register [session: string, reg: string, value: string] {
    send $session $"set-register ($reg) ($value)"
  }

  export def get-value [session: string, val: string] {
    eval $session $"echo %val{($val)}"
  }

  export def quote [input: string] {
    # Simple quoting: wrap in single quotes and escape existing single quotes
    $input | str replace "'" "'\\''" | $"'($in)'"
  }

  export def unquote [input: string] {
    $input | str trim -c "'" | str replace "'\\''" "'"
  } 
}

export use external *
export use buffer *
export use client *
export use selections *
export use cursor *

And the following for the Kakoune script file:

# knu.kak - Kakoune + Nushell integration
#
# Provides a 'nu' command to execute Nushell code from within Kakoune.
# Usage: nu %{ echo "Hello from Nushell!" }

# Declare options for configuration
# TODO Make this more generic
declare-option -hidden str knu_path %sh{ echo "$HOME/.dotfiles/nushell/.config/nushell/modules/knu.nu" }
declare-option -docstring "The Nushell interpreter used to execute Nushell code" str knu_interpreter nu

# Define the nu command that executes Nushell code
define-command nu -params 1.. -docstring %{
    nu [<switches>] [args...] code: Execute provided Nushell code as a script.
                                    Switches:
            -debug Print Nushell commands to *debug* buffer instead of executing them.
} %{
    evaluate-commands %sh{
        # Check if we have `nu` in our path
        if ! command -v "$kak_opt_knu_interpreter" >/dev/null 2>&1; then
            echo "echo -debug Error: $kak_opt_knu_interpreter not found in PATH" | kak -p "$kak_session"
            exit 1
        fi
        
        # Check if the knu module file exists
        if [ ! -f "$kak_opt_knu_path" ]; then
            echo "echo -debug Error: knu module not found at $kak_opt_knu_path" | kak -p "$kak_session"
            exit 1
        fi
        
        # Export important Kakoune variables as environment variables
        export KAK_CLIENT="$kak_client"
        export KAK_SESSION="$kak_session"
        export KAK_BUFFILE="$kak_buffile"
        export KAK_BUFNAME="$kak_bufname"
        export KAK_SELECTIONS="$kak_selections"
        export KAK_QUOTED_SELECTIONS="$kak_quoted_selections"
        export KAK_COMMAND_FIFO="$kak_command_fifo"
        
        # Buffer-related variables
        export KAK_BUF_LINE_COUNT="$kak_buf_line_count"
        export KAK_BUF_TIMESTAMP="$kak_buf_timestamp"
        export KAK_BUF_MODIFIED="$kak_buf_modified"
        
        # Client-related variables
        export KAK_CLIENT_PID="$kak_client_pid"
        export KAK_CLIENT_NAME="$kak_client_name"
        
        # Cursor and selection variables
        export KAK_CURSOR_LINE="$kak_cursor_line"
        export KAK_CURSOR_COLUMN="$kak_cursor_column"
        export KAK_SELECTION_ANCHOR_LINE="$kak_selection_anchor_line"
        export KAK_SELECTION_ANCHOR_COLUMN="$kak_selection_anchor_column"
        export KAK_SELECTION_CURSOR_LINE="$kak_selection_cursor_line"
        export KAK_SELECTION_CURSOR_COLUMN="$kak_selection_cursor_column"
        
        # Common options
        export KAK_OPT_FILETYPE="$kak_opt_filetype"
        export KAK_OPT_TABSTOP="$kak_opt_tabstop"
        export KAK_OPT_EXPANDTAB="$kak_opt_expandtab"
        export KAK_OPT_INDENTWIDTH="$kak_opt_indentwidth"
        export KAK_OPT_ALIGNWIDTH="$kak_opt_alignwidth"
        export KAK_OPT_SCROLLOFF="$kak_opt_scrolloff"
        export KAK_OPT_WRAP="$kak_opt_wrap"
        export KAK_OPT_MODELINE="$kak_opt_modeline"
        export KAK_OPT_HIGHLIGHT="$kak_opt_highlight"
        export KAK_OPT_AUTOSCROLL="$kak_opt_autoscroll"
        export KAK_OPT_AUTOCOMPLETE="$kak_opt_autocomplete"
        export KAK_OPT_AUTOINFO="$kak_opt_autoinfo"
        export KAK_OPT_AUTOINDENT="$kak_opt_autoindent"
        export KAK_OPT_AUTOSELECT="$kak_opt_autoselect"
        export KAK_OPT_AUTOSELECTCOMPLETE="$kak_opt_autoselectcomplete"
        export KAK_OPT_AUTOSELECTCOMPLETEINSERT="$kak_opt_autoselectcompleteinsert"
        export KAK_OPT_AUTOSELECTCOMPLETESELECT="$kak_opt_autoselectcompleteselect"
        export KAK_OPT_AUTOSELECTCOMPLETESELECTINSERT="$kak_opt_autoselectcompleteselectinsert"
        export KAK_OPT_AUTOSELECTCOMPLETESELECTINSERTCOMPLETE="$kak_opt_autoselectcompleteselectinsertcomplete"
        
        # Get the Nushell code (last argument)
        code="${!#}"
        
        # Build the Nushell command
        nu_cmd="use '$kak_opt_knu_path' *; $code"
        
        # Check for the debug switch
        if [ "$1" = "-debug" ]; then
            shift
            tmpfile=$(mktemp)
            if [ $# -gt 0 ]; then
                printf %s "$1" | "$kak_opt_knu_interpreter" --commands "$nu_cmd" >"$tmpfile" 2>&1
            else
                "$kak_opt_knu_interpreter" --commands "$nu_cmd" >"$tmpfile" 2>&1
            fi
            # Send output to debug buffer
            while IFS= read -r line; do
                echo "echo -debug $line" | kak -p "$kak_session"
            done < "$tmpfile"
            rm -f "$tmpfile"
            exit 0
        fi
        
        # Check if we have arguments to pipe (like %reg{'})
        if [ $# -gt 0 ]; then
            printf %s "$1" | "$kak_opt_knu_interpreter" --commands "$nu_cmd" >/dev/null 2>&1
        else
            "$kak_opt_knu_interpreter" --commands "$nu_cmd" >/dev/null 2>&1
        fi
    }
}

And so far so good. Is there anything here to fix with this? I’m potentially thinking about providing something like PyKak does with a server to prevent opening a new shell process each time a nu %{} block is called.