Constantin Gahr

rss icon
A blog about things that I find interesting and the Via Alpina

A python REPL for helix using tmux and ipython

The helix editor does not have an inbuilt shell or python REPL like VS Code or other IDEs. Thanks to being a well-design editor, however, it doesn’t need one as we can build our own using tmux and a simple python script.

The full code is available in my git repository tmux_hx_repl

Requirements

You only need tmux and a python interpreter. The latter is only necessary to run a small wrapper script around tmux and provides better ergonomics then a pure shell solution.

Setup without tmux

In case you don’t have access to tmux (for example on your windows work laptop where you can only use git bash), you can find a pure python solution in my git repository.

Core idea

The core idea is to run a python interpreter within tmux. Then we can use tmux to programatically send code from helix to the python interpreter.

There are two ingredients:

A simply python script

The python script tmux_hx_repl.py is a CLI tool with two main functions:

  1. starting a properly configured tmux window
  2. sending code to this window

The start subcommand

The command

python tmux_hx_repl.py start -L <socket> -t <buffer> <command>

opens a preconfigured tmux window. Using -L and -t you can configure socket and buffer name, respectively. A unique socket name like -L hx_repl ensures that your tmux session does not interfere with other tmux sessions. Similarly, the -t flag can be used to get different REPLs for different files open in helix.

The python code itself boils down to

tmux -L <socket> new -n <buffer> <command>

That is, open a new tmux buffer under the socket <socket> with name <buffer> and run the command <command>. In practice, I use the command

ipython || python

to execute ipython if available and pure python otherwise.

Additionally, the python script normalizes <buffer> to ensure that no . are present – names with . are not valid buffernames.

The send subcommand

The second subcommand is the send command

python tmux_hx_repl.py send

It sends code to the tmux windows opened with python tmux_hx_repl.py start, similarly using -L and -t to set socket and buffer name.

The code itself essentially boils down to 3 consecutive tmux invocations:

  1. tmux -L <socket> load-buffer - reads stdin into the tmux buffer;
  2. tmux -L <socket> paste-buffer -dpr -t <buffer> pastes the buffer as plain text into the tmux pane while replacing existing text and deleting the buffer afterwards;
  3. and tmux -L <socket> send-keys -t <buffer> Enter executes the code.

And that’s it.

helix keybindings

In helix, I use six custom keybindings to navigate between and execute code in cells.

For navigation, I use [+n and ]+n to select the previous and next delimiter # %% using

[keys.normal."["]
n = "@\"_?^#<space>%%<ret>"

[keys.normal."]"]
n = "@\"_/^#<space>%%<ret>"

Different delimiters like # command ---------- used in databricks notebooks work just the same with

[keys.normal."["]
n = "@\"_?^#<space>command<space>----------<ret>"

[keys.normal."]"]
n = "@\"_/^#<space>command<space>----------<ret>"

Next I use <backspace>+<space> to open a new terminal (foot), run python tmux_hx_repl.py to open a tmux window with properly set socket and buffer, and run ipython || python.

[keys.normal.backspace]
space = '''\
:sh foot python tmux_hx_repl.py start \
-L hx_repl -t repl-%{buffer_name} \
'ipython || python'
'''

<backspace>+s then pipes the current selection into the tmux_hx_repl.py script which sends the selection to the pane with name repl-%{buffer_name} and server hx_repl. Using the new helix command expansion %{buffer_name}, each buffer gets its own, named REPL shell.

[keys.normal.backspace]
s = ":pipe-to python tmux_hx_repl.py send -L hx_repl -t repl-%{buffer_name}"

The most important keybinding is the <backspace>+n helper. It executes code between 2 cell markers by selecting the code between both markers and sending the selection using <backspace>+s to the respective tmux pane. That’s your typical <shift>+<enter> in a jupyter notebook.

[keys.normal.backspace]
n = '''@\
"_?^#<space>%%<ret>\
"_/^#<space>%%<ret>\
i#<esc>\
"_/^#<space>%%<ret>\
F<ret>;\
vg.v_\
<backspace>su\
"_/^#<space>%%<ret>\
'''

It works as follows:

  1. go to the next cell marker
  2. go back to the previous cell marker
  3. insert a #
  4. go to the next cell marker
  5. go up one line so that the cursor is before the cell marker
  6. select back to the last change (i.e. the #)
  7. press <backspace>+s to pipe the selection to tmux and undo the insertion
  8. go to the next cell marker

This logic ensures that all possible cursor positions within a cell select the correct code.

Finally, I use <backspace>+o to insert a new cell marker surrounded by newlines.

[keys.normal.backspace]
o = "@]<space>]<space>gjgji# %%<esc>]<space>]<space>gjgji"

Full configuration file

The full helix configuration looks like this:

# config.toml
[keys.normal."["]
n = "@\"_?^#<space>%%<ret>"

[keys.normal."]"]
n = "@\"_/^#<space>%%<ret>"

[keys.normal.backspace]
space = '''\
:sh foot python tmux_hx_repl.py start \
-L hx_repl -t repl-%{buffer_name} \
'ipython || python
'''
n = '''@\
"_?^#<space>%%<ret>\
"_/^#<space>%%<ret>\
i#<esc>\
"_/^#<space>%%<ret>\
F<ret>;\
vg.v_\
<backspace>su\
"_/^#<space>%%<ret>\
'''
s = ":pipe-to python tmux_hx_repl.py send -L hx_repl -t repl-%{buffer_name}"
o = "@]<space>]<space>gjgji# %%<esc>]<space>]<space>gjgji"