← LOGBOOK LOG-452
COMPLETE · SOFTWARE ·
HASKELLMACOSGHOSTTYZELLIJZSHNEOVIMLAZYVIMGHCUPDEVELOPMENT-ENVIRONMENT

My Haskell Coding Environment

A terminal-first Haskell setup on macOS using Ghostty, Zellij, zsh, LazyVim, GHCup, and editor-managed language tooling.

The environment is a stack of independent layers: Ghostty renders the terminal, Zellij preserves the workspace, zsh runs each shell, LazyVim provides the editor, and GHCup manages the compiler and build tools. Haskell Language Server connects the project toolchain to Neovim.

Ghostty
  └─ Zellij session
       ├─ zsh → nvim → Haskell Language Server
       ├─ zsh → cabal / stack
       └─ zsh → ghci

              GHC toolchain

Each layer has one responsibility. Closing a terminal window does not destroy the Zellij session, changing the GHC version does not require changing the editor, and Neovim remains useful outside Haskell projects.

Base Packages

Homebrew provides the macOS applications and command-line tools around the Haskell toolchain:

brew install --cask ghostty
brew install neovim zellij zplug zoxide lsd bat

macOS already supplies zsh, so another shell installation is unnecessary. zplug manages the small set of shell plugins, zoxide replaces directory traversal with a frecency-based index, and lsd and bat provide more readable directory listings and file output.

Ghostty

Ghostty is the outer terminal emulator. Its native macOS application can be installed from the official disk image or through the Homebrew cask. The active configuration is deliberately small:

font-size = 24
theme = "Catppuccin Frappe"

Ghostty reads this from:

~/Library/Application Support/com.mitchellh.ghostty/config

The terminal handles font rendering, colour, keyboard input, and windows. It does not own the working session; that state belongs to Zellij.

Zellij

Zellij supplies persistent sessions, tabs, and panes:

brew install zellij
mkdir -p ~/.config/zellij
zellij setup --dump-config > ~/.config/zellij/config.kdl

The generated configuration was reduced to a compact workspace:

simplified_ui true
theme "dracula"
show_startup_tips false
pane_frames false
default_layout "compact"

The custom key map keeps pane and tab movement close to Vim conventions. Pane mode uses h, j, k, and l for focus; tab mode uses the same horizontal movement; and Ctrl-g locks or unlocks Zellij input. A short shell alias starts it:

alias zj='zellij'

The usual session loop is:

zj
zellij list-sessions
zellij attach <session-name>

zsh

Oh My Zsh supplies the shell framework, while its theme system is disabled so the Pure prompt can be loaded through zplug:

sh -c "$(curl -fsSL https://raw.githubusercontent.com/ohmyzsh/ohmyzsh/master/tools/install.sh)"
brew install zplug zoxide lsd bat

The relevant part of ~/.zshrc is:

export ZSH="$HOME/.oh-my-zsh"
ZSH_THEME=""
source "$ZSH/oh-my-zsh.sh"

export ZPLUG_HOME="$(brew --prefix)/opt/zplug"
source "$ZPLUG_HOME/init.zsh"
zplug "mafredri/zsh-async", from:github
zplug "sindresorhus/pure", use:pure.zsh, from:github, as:theme
zplug "zsh-users/zsh-autosuggestions"
zplug load

eval "$(zoxide init zsh)"

alias cd='z'
alias zj='zellij'
alias ls='lsd'
alias l='lsd -l'
alias la='lsd -a'
alias lla='lsd -la'
alias lt='lsd --tree'
alias cat='bat'

GHCup adds its tools through a separate environment file:

[ -f "$HOME/.ghcup/env" ] && . "$HOME/.ghcup/env"

This places ghc, ghci, cabal, stack, and ghcup on PATH without hard-coding a compiler version.

GHCup and the Haskell Toolchain

GHCup owns the language toolchain rather than Homebrew. The installer creates ~/.ghcup, installs selected tools beneath it, and writes the shell environment loaded above:

curl --proto '=https' --tlsv1.2 -sSf https://get-ghcup.haskell.org | sh

The interactive installer installs GHC, Cabal, and optionally Stack and Haskell Language Server. The same operations can be made explicit later:

ghcup install ghc
ghcup set ghc <version>
ghcup install cabal
ghcup install stack

The active setup uses GHC 9.10.3, Cabal 3.16.1.0, and Stack 3.11.1. GHCup’s versioned directories and unversioned symlinks allow another compiler to be installed without replacing the current one. ghcup set ghc changes which version the plain ghc command resolves to.

Compiler and language-server versions must agree. A project built with GHC 9.10.3 needs an HLS binary built for GHC 9.10.3; the HLS wrapper selects that binary from the project compiler version. A missing matching binary produces an editor failure even when ghc and haskell-language-server-wrapper both exist.

Neovim and LazyVim

Neovim is installed through Homebrew, then LazyVim is bootstrapped from its starter configuration:

brew install neovim
mv ~/.config/nvim ~/.config/nvim.bak
git clone https://github.com/LazyVim/starter ~/.config/nvim
nvim

The first launch bootstraps lazy.nvim and installs LazyVim’s plugin set. Haskell support is enabled with :LazyExtras, selecting lang.haskell. The selection is recorded in ~/.config/nvim/lazyvim.json:

{
  "extras": [
    "lazyvim.plugins.extras.lang.haskell"
  ]
}

The Haskell extra composes several editor features rather than treating language support as one plugin:

  • Tree-sitter parses Haskell syntax.
  • haskell-tools.nvim owns the HLS client and provides REPL, evaluation, and Hoogle actions.
  • Mason installs a matching Haskell Language Server for the editor.
  • haskell-snippets.nvim adds language-specific snippets.
  • Fourmolu, cabal-fmt, and HLint provide formatting and linting when their integrations are enabled.

Because haskell-tools.nvim owns HLS, the ordinary nvim-lspconfig path must not start a second client. The local plugin override keeps that boundary explicit:

return {
  {
    "neovim/nvim-lspconfig",
    opts = {
      servers = {
        hls = { enabled = false },
      },
    },
  },
}

LazyVim and Mason keep editor tooling under Neovim’s data directory. GHCup keeps project compilers and build tools under ~/.ghcup. This avoids mixing editor-managed HLS binaries with the compiler itself while still allowing the HLS wrapper to select the correct build.

Verifying the Stack

The shell layer is correct when all project commands resolve:

command -v ghc ghci cabal stack
ghc --version
cabal --version
stack --version

The Haskell layer can be checked without the editor:

mkdir haskell-check
cd haskell-check
cabal init --non-interactive
cabal build
cabal repl

Opening the generated project with nvim . exercises the remaining path. :LazyHealth checks the LazyVim installation, :Mason shows the installed Haskell tools, and :LspInfo confirms that one HLS client is attached to the buffer. Successful diagnostics and hover information establish the complete route from Ghostty input to the project’s GHC version.

Sources