~/ terminal notes

Building a minimal dev environment
with dwm and st

This is a living document. Everything below is copy-paste ready and tested on a fresh Arch install. No fluff — only what I actually run.

1. Package installation

One pacman invocation for everything needed. No AUR manager required initially — add paru for nerd fonts afterward.

pacman -S --needed \
  base-devel git xorg-server xorg-xinit xorg-xsetroot xorg-xrandr \
  xorg-xrdb libx11 libxft libxinerama freetype2 fontconfig \
  zsh zsh-completions fzf ripgrep fd eza tmux \
  dunst picom xclip xdotool acpi brightnessctl \
  ttf-jetbrains-mono noto-fonts noto-fonts-emoji \
  neovim nodejs npm ntfs-3g ranger rust gcc gdb make cmake \
  wget curl jq htop lsof strace
# AUR — install paru first, then:
paru -S --needed nerd-fonts-jetbrains-mono

2. Build & install

Clone from suckless directly. Never use distro packages for dwm or st.

mkdir -p ~/.builds && cd ~/.builds

# dwm
git clone https://git.suckless.org/dwm
cd dwm
# apply patches → edit config.h → build
sudo make clean install && cd ..

# st
git clone https://git.suckless.org/st
cd st
sudo make clean install && cd ..

# dmenu (suckless version for consistency)
git clone https://git.suckless.org/dmenu
cd dmenu && sudo make clean install

Directory layout after cloning:

~
├── .builds/
│   ├── dwm/
│   └── st/
├── .config/
│   ├── zsh/
│   └── nvim/
└── .local/bin/     # custom scripts

3. Patches

dwm

patchwhy
autostartrun ~/.dwm/autostart.sh on launch
backlightControll screen brightness using brightnessctl
pertagindependent layout per tag
vanitygapsinner/outer gaps for visual breathing room
fullscreentrue fullscreen toggle (no bar)
attachbottomnew clients below master, not above
statusbarclickable status bar segments

st

patchwhy
alphabackground transparency
scrollbackscroll without tmux dependency
font2fallback font for emoji/symbols
boxdrawclean box-drawing character rendering
undercurlwavy underlines for LSP diagnostics

Apply patches before editing config.h:

cd ~/.builds/dwm
curl -O https://dwm.suckless.org/patches/autostart/dwm-autostart-20210120-cb3f58a.diff
patch -p1 < dwm-autostart-20210120-cb3f58a.diff
# resolve any conflicts in config.def.h manually

4. dwm config.h

Use config from github which will be the most uptodate

Gruvbox dark palette throughout. Super as modifier. ThinkPad brightness and volume keys wired to hardware controls.

/* appearance */
static const unsigned int borderpx  = 2;
static const unsigned int gappx     = 6;
static const unsigned int snap      = 32;
static const int          showbar   = 1;
static const int          topbar    = 1;

static const char *fonts[] = {
    "JetBrainsMono Nerd Font:size=10",
    "Noto Color Emoji:size=10"
};

/* gruvbox dark */
static const char col_bg[]     = "#1d2021";
static const char col_bg1[]    = "#282828";
static const char col_fg[]     = "#ebdbb2";
static const char col_accent[] = "#d79921";
static const char col_blue[]   = "#458588";

static const char *colors[][3] = {
    [SchemeNorm] = { col_fg,  col_bg,     col_bg1  },
    [SchemeSel]  = { col_bg,  col_accent, col_blue },
};

/* tags */
static const char *tags[] = {
    "1","2","3","4","5","6","7","8","9"
};

/* layout — tile default, monocle, float */
static const float mfact     = 0.55;
static const int   nmaster   = 1;
static const int   resizehints = 0; /* ignore size hints */

See the full customisation reference for all available fields.

5. st config.h

/* font */
static char *font = "JetBrainsMono Nerd Font:pixelsize=14:antialias=true";
static char *font2[] = { "Noto Color Emoji:pixelsize=14" };

static int borderpx = 12;

/* transparency (alpha patch) */
static const float alpha = 0.92;

/* gruvbox colours — indices 0-15 */
static const char *colorname[] = {
    "#1d2021", "#cc241d", "#98971a", "#d79921",
    "#458588", "#b16286", "#689d6a", "#a89984",
    "#928374", "#fb4934", "#b8bb26", "#fabd2f",
    "#83a598", "#d3869b", "#8ec07c", "#ebdbb2",
    [255] = 0,
    "#ebdbb2", /* defaultfg */
    "#1d2021", /* defaultbg */
    "#d79921", /* cursor    */
};

static char *shell = "/bin/zsh";
static int  scroll_history = 4000;

6. Shell setup

No plugin manager. Two plugins cloned manually. Configuration split across ~/.zshenv and ~/.config/zsh/.zshrc.

chsh -s /bin/zsh
mkdir -p ~/.config/zsh/plugins

~/.zshenv


export PATH="$HOME/.local/bin:$PATH"
export EDITOR=nvim
export VISUAL=nvim
export PAGER=less
export MANPAGER="nvim +Man!"
export FZF_DEFAULT_COMMAND='fd --type f --hidden --follow --exclude .git'
export FZF_DEFAULT_OPTS='--height 40% --layout=reverse --border'

~/.zshrc (essentials)

# history
HISTSIZE=50000; SAVEHIST=50000; HISTFILE="~/.zsh_history"
setopt HIST_IGNORE_DUPS HIST_IGNORE_SPACE SHARE_HISTORY

# completion
autoload -Uz compinit && compinit 
# use -d to specify .zcompdump file
zstyle ':completion:*' menu select

# vi mode
bindkey -v
export KEYTIMEOUT=1
bindkey '^R' history-incremental-search-backward


# new github conf uses starship as prmpt
# prompt (no external deps)
autoload -Uz vcs_info
precmd() { vcs_info }
zstyle ':vcs_info:git:*' formats ' %b'
setopt PROMPT_SUBST
PROMPT='%F{yellow}%~%F{blue}${vcs_info_msg_0_}%f %(?.%F{green}.%F{red})❯%f '

# plugins
source "/usr/share/zsh/plugins/fast-syntax-highlighting/fast-syntax-highlighting.plugin.zsh"
source "/usr/share/zsh/plugins/zsh-autosuggestions/zsh-autosuggestions.zsh"
ZSH_AUTOSUGGEST_STRATEGY=(history completion)

# fzf
source /usr/share/fzf/key-bindings.zsh
source /usr/share/fzf/completion.zsh

source "/.aliases.zsh"

Clone plugins

download from repo instead of cloneing
git clone https://github.com/zdharma-continuum/fast-syntax-highlighting \
  ~/.config/zsh/plugins/fast-syntax-highlighting

git clone https://github.com/zsh-users/zsh-autosuggestions \
  ~/.config/zsh/plugins/zsh-autosuggestions

Key aliases

# replacements
alias ls='eza --group-directories-first'
alias ll='eza -la --git'
alias cat='bat --style=plain'
alias grep='rg'
alias find='fd'
alias vim='nvim'

# git shortcuts
alias gs='git status -sb'
alias gl='git log --oneline --graph --decorate'
alias gd='git diff'

# config quick-edit
alias edwm='nvim ~/.builds/dwm/config.h'
alias est='nvim ~/.builds/st/config.h'
alias ec='nvim ~/.config/zsh/.zshrc'

7. Essential tools

lf (file manager)

paru -S lf
mkdir -p ~/.config/lf
# ~/.config/lf/lfrc
set icons true
set hidden true
set drawbox true

cmd open ${{
    case $(file --mime-type "$f" -bL) in
        text/*|application/json) $EDITOR "$f" ;;
        image/*)   sxiv "$f"    ;;
        application/pdf) zathura "$f" ;;
        *) xdg-open "$f" ;;
    esac
}}

map <enter> open
map D delete
map R rename
map . set hidden!

8. Keybindings

bindingaction
Super+Shift+Returnspawn terminal (st)
Super+Pdmenu launcher
Super+Shift+Bfirefox
Super+Shift+Ckill client
Super+J / Kfocus next / prev
Super+H / Lresize master
Super+Returnzoom (swap to master)
Super+Tablast tag
Super+Ttile layout
Super+Mmonocle layout
Super+Ffloat layout
Super+1–9switch tag
Super+Shift+1–9move client to tag
Super+Btoggle bar
Super+Shift+Llock screen (slock)
Super+Shift+Qquit dwm

st

bindingaction
Ctrl+Shift+C / Vcopy / paste
Ctrl+Shift+PgUp / Dnzoom in / out
Shift+PgUp / PgDnscroll history

9. Fonts & theme

Gruvbox dark hard — consistently applied across dwm, st, nvim, and dmenu. One palette, zero cognitive switching cost.

rolehex
bg#1d2021
bg1#282828
fg#ebdbb2
accent (yellow)#d79921
blue#458588
red#cc241d
green#98971a

Font: JetBrainsMono Nerd Font — 10pt UI, 14px terminal.

# ~/.Xresources
Xft.dpi:       96
Xft.antialias: true
Xft.hinting:   true
Xft.hintstyle: hintslight
Xft.rgba:      rgb
Xcursor.size:  16
Xcursor.theme: Adwaita

10. Startup (.xinitrc)

#!/bin/sh

xrdb -merge ~/.Xresources

# keyboard
xset r rate 200 40
setxkbmap -layout us -option caps:escape   # Caps → Escape

# display (adjust to your output name)
xrandr --output eDP-1 --mode 1920x1080 --dpi 96

# natural scrolling
xinput set-prop "SynPS/2 Synaptics TouchPad" \
  "libinput Natural Scrolling Enabled" 1 2>/dev/null

# compositor
picom --daemon --backend glx --vsync \
  --inactive-opacity 0.9 --active-opacity 1.0 \
  --shadow --shadow-radius 8 &

dunst &
~/.local/bin/dwmstatus &

exec dwm

Auto-start X on login — append to ~/.config/zsh/.zprofile:

if [ -z "$DISPLAY" ] && [ "$XDG_VTNR" = "1" ]; then
  exec startx
fi

11. Status bar

Pure bash. Sets the root window name every 5 seconds. dwm reads it for the bar. No external deps — battery, volume, network, clock.

#!/bin/bash
# ~/.local/bin/dwmstatus

interval=5

get_bat() {
  bat=$(cat /sys/class/power_supply/BAT0/capacity)
  status=$(cat /sys/class/power_supply/BAT0/status)
  case "$status" in
    Charging)    echo "󰂄 ${bat}%" ;;
    Discharging) echo "󰁹 ${bat}%" ;;
    *)           echo "󰁹 FULL"   ;;
  esac
}

get_vol() {
  vol=$(pactl get-sink-volume @DEFAULT_SINK@ | grep -oP '\d+%' | head -1)
  mute=$(pactl get-sink-mute @DEFAULT_SINK@ | grep -c 'yes')
  [ "$mute" -gt 0 ] && echo "󰖁 mute" || echo "󰕾 $vol"
}

get_date() { date +" %a %d %b  %H:%M"; }

while true; do
  xsetroot -name "$(get_vol)  $(get_bat)  $(get_date)"
  sleep "$interval"
done
chmod +x ~/.local/bin/dwmstatus
That's the full setup. From pacstrap to a working dwm session, this is everything — no steps omitted, nothing assumed. Rebuild time from scratch: under 30 minutes.
back to all posts →