C.W.K.
Stream
Lesson 02 of 03 · published

Project Layouts and Real Workflows

~15 min · vim, tmux, workflow, remote-development

Level 0Trapped
0 XP0/35 lessons0/12 achievements
0/100 XP to next level100 XP to go0% complete

One project, three windows, lifelong rhythm

The most common Vim+tmux project layout is so consistent it's almost a default. Three windows: editor, server, shell. Sometimes a fourth: logs. Each window has 1–3 panes depending on how chatty the task is. Once you've used this for a week, switching projects is just changing session names.

Window 1 — editor

Full-screen Vim, or Vim + a side pane for a Python REPL / terminal in the same project root. Inside Vim you have buffers + splits + LSP for the actual code work. The pane to the right is for sending lines of code to (with vim-slime or similar) or for running commands like pytest path/to/test.py that you trigger from the editor.

Window 2 — server

The dev server. npm run dev, uvicorn app.main:app --reload, cargo watch, whatever your stack runs. One pane usually. Sometimes split with a tail of logs if the server doesn't show them itself.

Window 3 — shell

General-purpose. Git operations, ad-hoc scripts, docker exec, psql, whatever. Often split with lazygit or tig on one side for interactive git work.

Window 4 — logs (optional)

For services that produce a lot of log output, dedicate a window to tail -f on log files or kubectl logs -f on a pod. Keep it open all the time; flip to it when something looks weird.

The REPL workflow

Edit code in Vim; send a region or paragraph to the adjacent tmux pane running a Python/Node/SQL REPL. Tools that automate this:

  • vim-slime — sends a Vim selection to a target tmux pane. Works for any REPL.
  • vim-tmux-runner — like slime but with a first-class "send the test file" command.
  • iron.nvim — Lua-native REPL integration with language-aware send commands.

The git workflow

Two equally good patterns:

  • Inside Vim with gitsigns.nvim: see hunks in the gutter, stage / reset / preview hunks with leader bindings. Best for fine-grained "this hunk yes, that hunk no" work.
  • Adjacent pane with lazygit: a TUI git client in its own pane. Best for branch operations, commit history browsing, and "actually I want to interactive-rebase" moments.

Most experienced users use both. The patterns are complementary, not competing.

The persistent-dev-environment endgame

SSH into a remote workstation (a powerful desktop, a cloud VM, a colocated server). Start tmux there. Set up your three-window layout once. From now on:

  • Work from your laptop's terminal at home.
  • SSH in from a tablet at a coffee shop.
  • SSH in from a borrowed machine at a meetup.
  • Same session every time — same buffers, same processes, same scrollback.
The remote-first dev environment is the moat. Once your day-to-day work happens inside a tmux session on a remote workstation, the device you sit in front of stops mattering. Laptop, iPad with a keyboard, a friend's machine — they're all thin clients into the real workspace.

Code

Three-window project script — quick variant·bash
#!/usr/bin/env bash
# pippa-up.sh — open the cwk-pippa workspace
set -e

S="pippa"
D="$HOME/projects/cwkPippa"

tmux has-session -t $S 2>/dev/null && exec tmux attach -t $S

tmux new -d -s $S -c $D -n editor
tmux send-keys -t $S:editor 'nvim .' C-m

tmux new-window -t $S -n server -c $D
tmux send-keys -t $S:server 'pippa serve' C-m

tmux new-window -t $S -n shell -c $D
tmux split-window -h -t $S:shell -c $D
tmux send-keys -t $S:shell.0 'lazygit' C-m

tmux select-window -t $S:editor
exec tmux attach -t $S
vim-slime — send Vim region to tmux pane·lua
-- in lazy.nvim spec
{
  "jpalardy/vim-slime",
  init = function()
    vim.g.slime_target = "tmux"
    vim.g.slime_default_config = {
      socket_name = "default",
      target_pane = "{last}",   -- send to last-active pane in current window
    }
    vim.g.slime_dont_ask_default = 1
  end,
}
gitsigns.nvim — git hunks in the gutter·lua
{
  "lewis6991/gitsigns.nvim",
  event = { "BufReadPost", "BufNewFile" },
  opts = {
    signs = {
      add          = { text = "┃" },
      change       = { text = "┃" },
      delete       = { text = "_" },
      topdelete    = { text = "‾" },
      changedelete = { text = "~" },
    },
    on_attach = function(bufnr)
      local gs = package.loaded.gitsigns
      local map = function(mode, lhs, rhs, desc)
        vim.keymap.set(mode, lhs, rhs, { buffer = bufnr, desc = desc })
      end
      map("n", "]c", gs.next_hunk,            "Next git hunk")
      map("n", "[c", gs.prev_hunk,            "Prev git hunk")
      map("n", "<leader>hs", gs.stage_hunk,   "Stage hunk")
      map("n", "<leader>hr", gs.reset_hunk,   "Reset hunk")
      map("n", "<leader>hp", gs.preview_hunk, "Preview hunk")
      map("n", "<leader>hb", function() gs.blame_line({ full = true }) end, "Blame line")
    end,
  },
}

External links

Exercise

Take one project you actually work on. Write a startup script (model on pippa-up.sh) with the three-window layout. Add gitsigns.nvim to your lazy.nvim spec. Verify on a modified file: ]c and [c jump between hunks; <leader>hp previews a hunk; <leader>hs stages it. Then commit the startup script with the project so future-you starts in the right state every morning.

Progress

Progress is local-only — sign in to sync across devices.
Spotted a bug or have feedback on this page?Report an Issue

Comments 0

🔔 Reply notifications (sign in)
Sign inPlease sign in to comment.

No comments yet — be the first.