I have been exploring some new coding tools lately, specifically Helix and Zellij. Before I dive into those, let me share some context. I've been an Emacs user for about seven or eight years, primarily using it as a GUI application rather than in the terminal. I love it – it's incredibly powerful, and the key bindings are burned into my fingertips at this point. Emacs served as my one-stop solution for pretty much any programming language, and the addition of native compilation and LSP-related tooling made it even more capable. I particularly benefited from its rich ecosystem, saving countless hours with tools like Magit and Org Mode.
When I found myself with a month of free time between clients at the end of last year, I decided to revisit Helix. I paired it with Ghostty, a terminal that's been generating some buzz lately. This combination, along with Zellij, has been my daily driver for about two months now. I'm pleasantly delighted with it, and I think it'll stick around for a while yet.
Zellij
Zellij is a Rust-based terminal multiplexer, comparable to Tmux. It provides powerful key bindings for managing splits, panes, sessions, and tabs in terminal windows. Where I previously used project tabs in (Doom) Emacs to jump between different tasks, I now use Zellij tabs. Each tab typically represents either a repository or a specific task. While it's not quite as feature-rich as Emacs, it's remarkably capable – and importantly, this functionality is core to Zellij's purpose, rather than being a third-party plugin.
Zellij has a plugin ecosystem, and while fairly small, the zjstatus plugin allows some nice UI customization feats.
Helix
Helix is a feature-complete editor with robust language support, powered by tree-sitter for highlighting and navigation. While it might initially remind you of Vim, there's a fundamental difference: Helix prioritizes text selection over actions, unlike Vim. For instance, pressing W
in Vim moves you to the start of the next word, but in Helix, it both moves and selects that word. It's an unusual approach, but I've grown to appreciate this distinction.
What really stands out about Helix is its minimal configuration needs. As my time with Emacs (specifically Doom Emacs) grew longer, I became increasingly conscious of its complexity. Relying on a distribution like Doom Emacs meant being tied to that project's trajectory, with all its moving parts and potential for components to be updated, modified, or abandoned.
I've been pleasantly surprised to find that the combination of a terminal multiplexer and Helix allows me to work effectively without Emacs. While I miss certain features and occasionally return to Emacs for tasks like Magit, file management, project-wide searches, and general navigation, coding in Helix + Zellij has been seamless. In fact, it's noticeably faster and more responsive – which was part of what drew me to try this setup in the first place.
I plan to stick with this system for at least a few more months. It's liberating to not be completely dependent on a single tool anymore, though Emacs will always have a place in my toolbox. Here's hoping Helix eventually gets a Lisp-based plugin system.
Here's what my helix config is looking like at the time of writing this:
theme = "adwaita-dark"
[editor]
line-number = "absolute"
# Minimum severity to show a diagnostic after the end of a line:
end-of-line-diagnostics = "hint"
soft-wrap.enable = true
soft-wrap.wrap-indicator = ""
[editor.inline-diagnostics]
cursor-line = "error"
[editor.statusline]
right = [ "position", "position-percentage", "total-line-numbers", "selections"]
left = [ "mode", "spinner", "diagnostics", "file-name", "separator", "version-control" ]
mode.insert = "INS"
mode.normal = "NOR"
mode.select = "SEL"
separator = " │ "
[keys.normal]
ret = "goto_word"
S = "@ /%p <C-r>% %n " # Open string search on current buffer
W = "@miW" # Select Whole word under cursor
# w = "@miw" # Select word under cursor
"*" = ["move_prev_word_start", "move_next_word_end", "search_selection"]
[keys.normal.space]
"space" = "file_picker"
[keys.normal.space.f]
f = "file_picker"
g = "changed_file_picker"
s = ":write"
k = "file_picker_in_current_buffer_directory"
d = "file_picker_in_current_directory"
r = ":reload"
a = ":toggle file-picker.git-ignore"
y = "@\"%p<space>Yd"
Y = "@o<esc>|pwd<ret>;i/<esc>\"%PmiW\"+yddk" # Copy buffer full-path in clipboard
p = ":sh zellij run -c -f -x 10% -y 10% --width 80% --height 80% -- bash ~/.config/helix/yazi-picker.sh open" # Open the file(s) in the current window
# code things
[keys.normal.space.c]
o = ":open ~/dotfiles/helix/.config/helix/config.toml"
r = ":config-reload"
l = ":lsp-restart"
# buffer things
[keys.normal.space.b]
b = "buffer_picker"
n = ":buffer-next"
p = ":buffer-previous"
k = ":buffer-close"
x = ":write-buffer-close"
W = "@miW" # Select Whole word under cursor
w = "@miw" # Select word under cursor
# tasks / terminal / zellij
[keys.normal.space.t]
j = ":sh just"
f = ":sh zellij action new-pane -f"
u = "expand_selection"
d = "shrink_selection"
t = ":sh zellij action new-pane -f -x 10% -y 10% --width 80% --height 80% " # Open the file(s) in the current window
# git
[keys.normal.space.g]
g = ":run-shell-command zellij run -c -- gitu"
l = ":sh zellij run -f -n lazygit --width 90% --height 90% -x 5% -y 5% -c -- lazygit"
r = ":reset-diff-change"
Things I miss
Magit. Of course. So I keep emacs running in a terminal for any complex git operations. But everything else I now use Lazygit, spawned into a floating terminal. I also have to flip over to emacs from time to time to use the git-timemachine plugin, as well as the "open in github" key command in doom-emacs