My Terminal Setup, Explained
The Zsh, tmux, and Git configs I use daily. Annotated so you can understand what each block does and take what's useful.

People ask about my terminal setup more than almost anything else I write about. Usually during screen shares โ someone notices the auto-suggestions, or the fuzzy finder popping up on Ctrl+R, or the way I jump between tmux panes, and the question comes up. "What is that? How do I get that?"
Most of what I have now stabilized about a year ago, I think. The result of trying a lot of tools, dropping the ones that felt gimmicky or created more friction than they removed, and keeping what I actually reach for daily. Nothing here is exotic. Steal whatever looks useful. Skip whatever doesn't.
Zsh Plugins
Running Zsh with Oh My Zsh. Most basic setup possible. Some people give you a look for that โ like you should be hand-curating a framework-less config with manually compiled plugins. They might be right. OMZ works fine though, the plugin system is simple, and there are better uses for my time than optimizing shell bootstrap sequences.
Four plugins that would go on a new machine first:
plugins=(
git
z
zsh-autosuggestions
zsh-syntax-highlighting
)
git โ about a hundred aliases for git commands. gst for git status, gco for git checkout, gp for git push. Don't use all of them โ maybe 10-15 regularly. The ones I use save enough keystrokes to justify the plugin. glog in particular (formatted git log with branch graph) gets used all the time.
z โ directory jumper. After navigating to a directory a few times, z learns it. Instead of cd ~/projects/technoknowledge24/src/components, type z compon and it takes you there. Matching based on "frecency" โ combination of frequency and recency. Surprisingly accurate, from what I've seen. Saves maybe 20-30 cd commands a day.
zsh-autosuggestions โ ghost-text suggestion from history as you type. Start typing docker compose and the rest of the most recent matching command fills in as gray text. Right arrow to accept. Distracting for the first week. Can't live without it after that.
zsh-syntax-highlighting โ colors commands green if valid, red if not, as you type. Catches typos before you press Enter. Simple. Effective.
Aliases
alias ..="cd .."
alias ...="cd ../.."
alias wipe="clear && printf '\e[3J'"
alias ports="lsof -i -P -n | grep LISTEN"
alias dev="npm run dev"
alias dcu="docker compose up -d"
alias dcd="docker compose down"
ports gets the most use after dev. "Address already in use" โ run ports to see what's hogging the port. Answer appears immediately.
wipe is clear but it flushes the scrollback buffer too. Regular clear just moves the prompt to the top โ scroll up and all previous output is still there. Sometimes you want a truly blank terminal. The printf '\e[3J' escape sequence handles that.
FZF (Fuzzy Finder)
Replaced the default Ctrl+R history search and Ctrl+T file picker. Built-in Zsh history search matches from the start of the command only. FZF does fuzzy matching โ type "docker" and it shows every command containing "docker" anywhere, ranked by relevance.
export FZF_DEFAULT_OPTS='--height 40% --layout=reverse --border --color=dark'
export FZF_CTRL_T_OPTS="--preview 'bat --color=always --line-range=:100 {}'"
--preview on Ctrl+T shows a syntax-highlighted file preview (using bat) as you scroll through matches. This is the feature that prompts the "wait, what was that?" questions during screen shares. Looks flashy. Also genuinely practical, I think โ when you're looking for a file and can't remember the exact name, seeing content preview as you browse beats opening each candidate.
Rust CLI Replacements
Not a "rewrite everything in Rust" evangelist. But a few Rust tools have replaced their GNU counterparts because they're noticeably better at the job.
ripgrep (rg) replaces grep. Mentioned briefly in my Linux command line post โ worth expanding on. Faster on large codebases. Not marginally โ noticeably. But the real win is defaults. Respects .gitignore by default, so no flood of node_modules matches. Readable output with file names and line numbers. More intuitive flags.
rg "useState" --type ts
Search for "useState" in TypeScript files. grep equivalent: grep -rn "useState" --include="*.ts" --include="*.tsx" . โ more typing, harder to recall.
fd replaces find. Immediately more readable:
fd -e json "config"
Find .json files with "config" in the name. The find equivalent involves flag combinations I have to look up every time. fd I can type from memory.
bat replaces cat. Syntax highlighting, line numbers, git diff markers in the gutter. alias cat="bat" means syntax highlighting every time I view a file in the terminal. Lowest-effort upgrade with the most visible improvement.
eza replaces ls. Tree view, git status per file, icons. eza --tree --level=2 --git for a project structure overview with git status. Much nicer than ls -la.
tmux
Two uses: keeping sessions alive on remote servers (especially useful when monitoring Docker Compose services), and splitting local terminals into panes.
Session persistence is the main reason I started with tmux. SSH into a server, start a long-running process, connection drops โ the process dies. With tmux, it runs inside a session on the server. Connection drops, session keeps running. Reconnect and reattach. Process didn't notice.
Locally: editor on the left, dev server on the right, small pane at the bottom for one-off commands. Some people use VS Code's integrated terminal for this. tmux layout is more flexible and shortcuts are faster once memorized.
Config changes from defaults:
# Change prefix from Ctrl+b to Ctrl+a
unbind C-b
set-option -g prefix C-a
bind-key C-a send-prefix
# Split with | and - instead of % and "
bind | split-window -h -c "#{pane_current_path}"
bind - split-window -v -c "#{pane_current_path}"
# Enable mouse
set -g mouse on
# Start window numbering at 1, not 0
set -g base-index 1
setw -g pane-base-index 1
The prefix change from Ctrl+B to Ctrl+A is the most important one. Ctrl+B requires an awkward hand stretch. Ctrl+A keeps your left hand in a natural position. tmux is all keyboard shortcuts, so ergonomics matter.
Mouse support gets side-eye from terminal purists. Scrolling through log output with the mouse wheel is faster than entering copy mode and navigating with keys. I'll take the practical tool over the ideologically pure one.
#{pane_current_path} on split commands โ new panes open in the same directory as the current pane. Without it, every new pane starts in your home directory and you cd to where you were working. Fixed once, never thought about again.
Git Aliases
[alias]
s = status -sb
lg = log --color --graph --pretty=format:'%Cred%h%Creset -%C(yellow)%d%Creset %s %Cgreen(%cr) %C(bold blue)<%an>%Creset' --abbrev-commit
wip = !git add -A && git commit -m "WIP"
undo = reset --soft HEAD~1
branches = branch --sort=-committerdate --format='%(committerdate:relative)%09%(refname:short)'
lg gets the most use. Default git log is borderline unreadable on a busy repo. This alias shows a compact, colorized graph with short hashes, branch decorations, relative dates, and author names. Use it so often I sometimes forget what the standard git log output looks like.
wip โ quick save before switching branches or heading out. Stages everything, commits with "WIP." Not clean, but squashed before opening a PR โ interactive rebase cleanup is something I covered in my advanced Git workflows post.
undo โ resets the last commit but keeps changes staged. Committed too early, forgot something, want to change the message. Safer than --hard because nothing gets deleted.
branches โ sorts local branches by most recent commit. Fifteen branches and can't remember what the one from Tuesday was called? Recent ones appear at the top with dates. More useful than alphabetical git branch.
Starship Prompt
Almost forgot this one. Starship is a cross-shell prompt written in Rust. Shows the current directory, git branch and status, Node/Python/Rust version when you're in a project that uses those languages, command duration for slow commands, and error indicators for failed commands. All without noticeable latency.
Before Starship, I had a custom Zsh prompt that showed git branch and status using shell script. It worked but was slow on large repos โ you could feel the lag when entering a directory with a big .git folder. Starship does the same thing faster because it's a compiled binary rather than shell script parsing git output.
Configuration is in a starship.toml file. I've kept mine minimal:
[git_status]
conflicted = "โ "
ahead = "โฌ"
behind = "โฌ"
diverged = "โฌ"
[cmd_duration]
min_time = 2_000
format = "took [$duration]($style) "
[nodejs]
detect_files = ["package.json"]
The cmd_duration module is more useful than I expected. Any command taking longer than 2 seconds shows the elapsed time in the prompt. Good for noticing when a build is getting slower over time, or when a test suite that used to take 8 seconds starts taking 14. You see the duration every time without needing to look at a watch.
Things I Tried and Dropped
Not everything stuck. Tried tmuxinator for managing named tmux sessions with predefined window layouts. The idea is good โ save your "work on project X" layout and restore it with one command. In practice, I didn't switch contexts often enough to justify the config files. When I do need to set up a specific layout, it takes about 30 seconds to do manually, which doesn't merit automation.
Tried navi โ an interactive cheatsheet tool where you can search for commands by description. Nice concept but I found FZF's history search faster for commands I'd run before, and for commands I hadn't run before, I'd usually just look them up on a browser. Navi occupied a middle ground that didn't match my workflow.
Tried kitty terminal emulator for its GPU rendering and image support. Faster than my previous terminal. But the configuration format was unfamiliar and I didn't have a strong reason to leave Windows Terminal, which has gotten good enough. The GPU rendering was noticeably smoother for scrolling through large log files, but "noticeably smoother" doesn't translate to "worth the switching cost" for something I'm staring at all day.
The biggest takeaway from years of terminal tinkering: the gap between "I know this tool exists" and "I reach for it without thinking" is where the productivity actually lives. Reading about ripgrep doesn't make you faster. Using it fifty times does. The muscle memory is the feature. Installing the tool is just the prerequisite.
Same goes for keybindings. I can list all the tmux shortcuts. But the ones that actually save me time are the three or four I use reflexively โ split pane, switch pane, scroll up. The rest I look up when needed, and that's fine.
If I had to start over with a bare terminal and could only install five things, they'd be: zsh-autosuggestions (history-based suggestions while typing), fzf (fuzzy search for everything), ripgrep (fast code search), z (directory jumping), and tmux (session persistence). Everything else is nice to have. Those five are the foundation.
The lesson with terminal tooling is that the tools that stick are the ones that solve an actual friction point. FZF solved "I can't find that command from last week." ripgrep solved "grep is too slow and the output is ugly." tmux solved "my SSH session dropped and my process died." The tools that didn't stick were the ones solving problems I didn't have frequently enough to build the muscle memory.
Shell Functions Over Complex Aliases
One thing I picked up later that's worth mentioning: when an alias gets complicated enough to need arguments or conditional logic, turn it into a shell function instead. Aliases are string substitutions. They work great for simple command shortcuts. But the moment you need to pass arguments in a specific position or add an if/else, you're fighting the syntax.
Example. I wanted a shortcut to create a new branch and push it to the remote in one step. An alias can't handle that cleanly because the branch name needs to go in two different places. A function handles it naturally:
gnew() {
git checkout -b "$1" && git push -u origin "$1"
}
Type gnew feature/login-page and it creates the branch locally and sets up remote tracking in one shot. Three seconds instead of two separate commands and remembering the -u flag.
Another one I use daily โ a quick function to kill whatever process is using a specific port:
killport() {
lsof -ti :"$1" | xargs kill -9
}
killport 3000 when the dev server didn't shut down cleanly. Faster than checking ports, finding the PID, then running kill separately. Small things, but they add up across a full workday. I keep these functions in a separate ~/.zsh_functions file that gets sourced from .zshrc, which keeps the main config file from turning into a wall of code.
The Full Config, For Reference
Dotfiles are in a git repo. Not a fancy setup โ just a bare repository with a custom alias that tracks my home directory. Clone it on a new machine, install the Rust tools with cargo install ripgrep fd-find bat eza starship, copy a few config files, source the shell profile. Maybe 30 minutes end to end. Should probably automate the installation with a script. One of those perpetual "one of these days" items that has been on the list long enough to qualify as a permanent resident.
Things People Ask About That I Don't Use
Since this post consistently generates "but what about X?" responses, here are the things I've tried or been asked about and why they're not in my setup.
Neovim. Tried it twice. Both times got three weeks in, had a productive config, and then needed to do something quickly in a pair programming session and couldn't because my muscle memory was split between Neovim bindings and VS Code bindings. Went back to VS Code. The terminal integration in VS Code is good enough for my needs, and having one editor for everything means one set of shortcuts in my head.
Fish shell. Better autocompletion than Zsh out of the box, nicer syntax for scripting. But not POSIX-compatible, which means shell scripts written for bash don't work without modification. Stack Overflow answers assume bash. CI scripts assume bash. Half the convenience of a shared shell ecosystem disappears when you're on a different shell. Tried it for a month. The incompatibilities added more friction than the features removed.
Alacritty. GPU-accelerated terminal. Fast. Minimalist. Configuration through a YAML file. I respect it. But it doesn't support tabs or panes natively โ you're expected to use tmux for that. Which I do. But I also sometimes want a quick tab for a one-off command without the overhead of a tmux session. Windows Terminal gives me both tabs and tmux when I want it. Good enough.
Warp terminal. AI-integrated, block-based terminal. Interesting concept. The blocks (grouping commands with their output) are a nice UX idea. But the AI suggestions felt distracting when I was in flow, and some of the features required a Warp account, which I wasn't comfortable with for a terminal emulator. Personal preference โ others on my team use it and like it.
The meta-pattern: I've stopped chasing the "best" terminal setup. The one that's configured, memorized, and used daily beats the theoretically superior one that takes a week to learn and another week to configure. Invest in tools that solve pain points. Leave the rest alone.
Written by
Anurag Sinha
Full-stack developer specializing in React, Next.js, cloud infrastructure, and AI. Writing about web development, DevOps, and the tools I actually use in production.
Stay Updated
New articles and tutorials sent to your inbox. No spam, no fluff, unsubscribe whenever.
I send one email per week, max. Usually less.
Comments
Loading comments...
Related Articles

Linux CLI: The Commands I Use Every Day
A collection of bash commands and shortcuts I reach for constantly, organized by what I'm trying to do rather than by category.

SSH Tunneling โ The Networking Swiss Army Knife Nobody Taught Me
Local forwarding, remote forwarding, dynamic SOCKS proxies, jump hosts, and the SSH config shortcuts that replaced half my VPN usage.

Learning Docker โ What I Wish Someone Had Told Me Earlier
Why most Docker tutorials fail beginners, the critical difference between images and containers, and what actually happens when you run a container.