This post documents my minimal desktop setup on Ubuntu, built around the i3 window manager. It’s designed to be fast, distraction-free, and fully keyboard-driven — a system that adapts to different kinds of work and gives me full control over my workspace environment.

I use Dropbox to sync files across machines and symlink all key config files — Emacs, Tmux, i3, and more — from a central dotfiles repository. This setup makes it easy to manage multiple machines with different configurations, like separate work and personal environments, while maintaining a shared foundation.

This post also serves as a reference for myself when setting things up on new machines.


Base System Setup

I primarily use apt-get for installing packages, but occasionally fall back to snap when it’s the simpler or more reliable choice. The goal is practicality — whatever gets things working cleanly with minimal fuss.

sudo apt-get update
sudo apt-get install build-essential
sudo apt-get install git curl make cmake autoconf vim tldr htop xsel arandr tree openssh-server net-tools

Disabling the GRUB splash makes boot cleaner and faster. I also unbind Control + semicolon from IBus to free it up for other use.

# disable splash screen
sudo apt-get purge plymouth-theme-ubuntu-text

# edit /etc/default/grub
GRUB_CMDLINE_LINUX_DEFAULT=""

sudo update-grub2

# remove C-; keybinding from ibus
ibus-setup  # delete keybinding

To make additional drives available on boot, I manually find their UUIDs and add them to /etc/fstab.

# find UUIDs and Filesystem Types
lsblk -o NAME,FSTYPE,UUID

# create mountpoints with correct user permissions
sudo mkdir /mnt/drive1
sudo chown -R $USER:$USER /mnt/drive1

# edit /etc/fstab
UUID=your-uuid /mnt/drive1 ext4 defaults 0 0

I like using Source Code Pro as my terminal and editor font.

wget https://github.com/adobe-fonts/source-code-pro/archive/2.030R-ro/1.050R-it.zip
unzip 1.050R-it.zip
mkdir ~/.local/share/fonts
cp source-code-pro-*-it/OTF/*.otf ~/.local/share/fonts
fc-cache -f -v

Shell and Dev Environment

ZSH

Install and configure ZSH with oh-my-zsh. This sets up a more readable, functional shell with fuzzy search and syntax highlighting.

sudo apt-get install zsh
sh -c "$(curl -fsSL https://raw.github.com/ohmyzsh/ohmyzsh/master/tools/install.sh)"

# zsh syntax highlighting
git clone https://github.com/zsh-users/zsh-syntax-highlighting.git ${ZSH_CUSTOM:-~/.oh-my-zsh/custom}/plugins/zsh-syntax-highlighting

sudo apt-get install fzf
plugins=(git fzf colored-man-pages colorize zsh-syntax-highlighting)

Tmux and Tmuxinator

Tmux setup with TPM for managing plugins, and tmuxinator for complex session layouts.

sudo apt-get install tmux tmuxinator
git clone https://github.com/tmux-plugins/tpm ~/.tmux/plugins/tpm

Minimal .tmux.conf:

# ~/.tmux.conf
set -g mouse on
set -g history-limit 10000
set-option -g allow-rename off

bind | split-window -h
bind - split-window -v
unbind '"'
unbind %

set -g @plugin 'tmux-plugins/tpm'
set -g @plugin 'tmux-plugins/tmux-sensible'

run '~/.tmux/plugins/tpm/tpm'

Sample tmuxinator config for a default session.

# ~/.tmuxinator/default.yml
name: default
root: ~/

windows:
  - system:
      layout: tiles
      panes:
        - htop
        - cd ~/grind && jupyter lab
        - nvidia-smi -l 2
        - cd
  - editor:
      layout: tiles
      panes:
        - cd

SSH Key Setup for Multiple Accounts

To keep work, personal, and other Git accounts separate, I generate multiple SSH keys and configure them in ~/.ssh/config. This lets me switch between GitHub, Bitbucket, and other services without conflicts.

# create multiple SSH keys, one for each account
ssh-keygen -t ed25519 -C "comment"
# create other keys

# start ssh-agent
eval "$(ssh-agent -s)"

# ssh-add multiple keys
ssh-add ~/.ssh/id_ed25519_1
# add other keys

Edit ~/.ssh/config to specify which key to use for each host.

# ~/.ssh/config
Host github.com
  HostName github.com
  User git
  IdentityFile ~/.ssh/id_ed25519_1

Host bitbucket.org
  HostName bitbucket.org
  User git
  IdentityFile ~/.ssh/id_ed25519_2

# add other hosts as needed

Managing Python with pyenv

To manage multiple Python versions independently of the system Python, I use pyenv. This makes it easy to install, switch and isolate Python runtimes.

curl -L https://github.com/pyenv/pyenv-installer/raw/master/bin/pyenv-installer | bash

# add pyenv to PATH
echo 'export PYENV_ROOT="$HOME/.pyenv"' >> ~/.zshrc
echo '[[ -d $PYENV_ROOT/bin ]] && export PATH="$PYENV_ROOT/bin:$PATH"' >> ~/.zshrc
echo 'eval "$(pyenv init - zsh)"' >> ~/.zshrc

# install python dependencies
sudo apt-get update
sudo apt-get install make build-essential libssl-dev zlib1g-dev libbz2-dev libreadline-dev libsqlite3-dev wget curl llvm libncursesw5-dev xz-utils tk-dev libxml2-dev libxmlsec1-dev libffi-dev liblzma-dev

pyenv install x.yy.z
pyenv global x.yy.z

Emacs

Set up Emacs as the default editor – a full operating system in the guise of a text editor – and configure related environment variables. For a deep dive into my full Emacs configuration, see: Emacs Configuration.

sudo apt-get install ripgrep git-delta
sudo snap install node
sudo snap install emacs --classic
# ~/.zshrc
export EDITOR="emacsclient -c -a emacs"
export VISUAL="$EDITOR"
alias sudo-emacsclient="SUDO_EDITOR=\"emacsclient\" sudo -e"

# variables used by Emacs
export DROPBOX_HOME="$HOME/Dropbox"
export ORG_HOME="$DROPBOX_HOME/org_files"

Window Manager: i3 Setup

i3 is the core of my environment — a tiling window manager that enables a fully keyboard-driven, minimal workflow. I extend it with Rofi for application launching, and Greenclip for clipboard management.

# install i3
sudo apt-get install i3 xautolock

# install rofi
sudo apt-get install rofi rofi-dev

# to change rofi theme, run
rofi -show run
# choose rofi-theme-selector

# install greenclip
wget https://github.com/erebe/greenclip/releases/download/v4.2/greenclip
chmod +x greenclip
sudo mv greenclip /usr/local/bin

# rofi-calc
# follow instructions from https://github.com/svenstaro/rofi-calc

i3 Config

Basic i3 configuration to set up the environment, mostly using the default settings.

# ~/.config/i3/config

# set modifier key
set $mod Mod4

# Font for window titles. Will also be used by the bar unless a different font
# is used in the bar {} block below.
font pango:SourceCodePro-Semibold 8

# Use Mouse+$mod to drag floating windows to their wanted position
floating_modifier $mod

# terminal
bindsym $mod+Return exec i3-sensible-terminal
# bindsym $mod+Return exec /usr/bin/urxvt

# reload the configuration file
bindsym $mod+Shift+c reload
# restart i3 inplace (preserves your layout/session, can be used to upgrade i3)
bindsym $mod+Shift+r restart
# exit i3 (logs you out of your X session)
bindsym $mod+Shift+e exec "i3-nagbar -t warning -m 'You pressed the exit shortcut. Do you really want to exit i3? This will end your X session.' -b 'Yes, exit i3' 'i3-msg exit'"
# lock and autolock
bindsym Control+$mod+l exec i3lock -c 000000
exec_always --no-startup-id xautolock -time 10 -locker "i3lock -c 000000"

Remap Keys: Caps Lock to Control for better keyboard shortcuts.

# remap Capslock to control, double shift for Capslock
exec_always --no-startup-id setxkbmap -option 'ctrl:nocaps,shift:both_capslock_cancel'

Setup Rofi for application launching, window switching, and other tasks.

# rofi
bindsym $mod+d exec --no-startup-id "rofi -combi-modi window,drun -show combi -modi combi -show-icons"

# rofi-calc
bindsym $mod+c exec --no-startup-id "rofi -show calc -modi calc -no-show-match -no-sort"

# ssh
bindsym $mod+z exec --no-startup-id "rofi -show ssh"

# clipboard with greenclip
exec --no-startup-id greenclip daemon
bindsym $mod+x exec --no-startup-id rofi -modi "clipboard:greenclip print" -show clipboard -run-command '{cmd}'

Display Layout with xrandr: I use arandr to manually configure multi-monitor layouts for work, personal, and other environments. I save them as shell scripts and use xrandr to apply them when switching between setups. I use keybings in i3 to quickly switch between them.

# ~/.screenlayout/setup1.sh
# sample xrandr command
xrandr --output HDMI-0 --primary --mode 2560x1440 --pos 1920x0 --rotate normal --output DP-0 --off --output DP-1 --mode 1920x1200 --pos 0x0 --rotate normal --output HDMI-1 --off --output DP-2 --off --output DP-3 --mode 1920x1200 --pos 4480x0 --rotate normal --output DP-4 --off --output DP-5 --off --output HDMI-1-0 --off --output DP-1-0 --off --output DP-1-1 --off --output DP-1-2 --off --output DP-1-3 --off --output USB-C-1-0 --off

in i3:

# ~/.config/i3/config
bindsym $mod+Shift+[ exec ~/.screenlayout/setup1.sh

Start Applications: I map specific applications to dedicated workspaces

exec --no-startup-id i3-msg 'workspace 1; exec /snap/bin/emacs'
exec --no-startup-id i3-msg 'workspace 2; exec /snap/bin/firefox'
exec --no-startup-id i3-msg 'workspace 3; exec i3-sensible-terminal'
exec --no-startup-id i3-msg 'workspace 5; exec /usr/bin/nautilus'
exec --no-startup-id i3-msg 'workspace 9; exec /usr/local/bin/zotero'

Assign workspaces to displays: I map specific applications to dedicated workspaces and assign those workspaces to monitors based on what I’m currently working on. For example, when programming, I prefer Emacs on the primary display and Firefox on a secondary one. For writing, I might reverse that, or shift focus to a different set of apps — the layout adapts to the task. To achieve that, I write scripts using i3-msg and bind them to keys in i3.

# work_setup__prog.sh
i3-msg "workspace 1, move workspace to output DP-3"
i3-msg "workspace 2, move workspace to output HDMI-0"
i3-msg "workspace 3, move workspace to output DP-1"
i3-msg "workspace 5, move workspace to output DP-1"

and in i3:

# ~/.config/i3/config
bindsym $mod+Shift+o exec /path/to/work_setup__prog.sh
bindsym $mod+Shift+p exec /path/to/work_setup__writing.sh

Start applets, other services:

# nm applet
exec --no-startup-id nm-applet

# bluetooth
exec --no-startup-id blueman-applet

# dropbox
exec --no-startup-id dropbox start

i3bar: I use i3bar with i3status for a clean, minimal status bar

# Base16 Black Metal theme colors
set $base00 #000000
set $base01 #121212
set $base02 #222222
set $base03 #333333
set $base04 #999999
set $base05 #c1c1c1
set $base06 #999999
set $base07 #c1c1c1
set $base08 #5f8787
set $base09 #aaaaaa
set $base0A #a06666
set $base0B #dd9999
set $base0C #aaaaaa
set $base0D #888888
set $base0E #999999
set $base0F #444444

bar {
    status_command i3status
    font pango:Ubuntu Mono 9
    position top

    # Disable scrolling in the bar
    bindsym button4 nop
    bindsym button5 nop

    tray_output primary
    separator_symbol "|"

    colors {
        background         $base00
        separator          $base01
        statusline         $base04

        # State              Border    BG        Text
        focused_workspace   $base05   $base0D   $base00
        active_workspace    $base03   $base01   $base05
        inactive_workspace  $base03   $base01   $base05
        urgent_workspace    $base08   $base08   $base00
        binding_mode        $base00   $base0A   $base00
    }
}

Media keys:

# Pulse Audio controls
bindsym XF86AudioRaiseVolume exec pactl set-sink-volume @DEFAULT_SINK@ +5% #increase sound volume
bindsym XF86AudioLowerVolume exec pactl set-sink-volume @DEFAULT_SINK@ -5% #decrease sound volume
bindsym XF86AudioMute exec pactl set-sink-mute @DEFAULT_SINK@ toggle # mute sound

# Media player controls
bindsym XF86AudioPlay exec playerctl play
bindsym XF86AudioPause exec playerctl pause
bindsym XF86AudioNext exec playerctl next
bindsym XF86AudioPrev exec playerctl previous

Changing focus, moving workspaces:

# change focus
bindsym $mod+j focus left
bindsym $mod+k focus down
bindsym $mod+l focus up
bindsym $mod+semicolon focus right

# alternatively, you can use the cursor keys:
bindsym $mod+Left focus left
bindsym $mod+Down focus down
bindsym $mod+Up focus up
bindsym $mod+Right focus right

# move focused window
bindsym $mod+Shift+j move left
bindsym $mod+Shift+k move down
bindsym $mod+Shift+l move up
bindsym $mod+Shift+semicolon move right

# alternatively, you can use the cursor keys:
bindsym $mod+Shift+Left move left
bindsym $mod+Shift+Down move down
bindsym $mod+Shift+Up move up
bindsym $mod+Shift+Right move right

# split in horizontal orientation
bindsym $mod+h split h

# split in vertical orientation
bindsym $mod+v split v

# enter fullscreen mode for the focused container
bindsym $mod+f fullscreen toggle

# change container layout (stacked, tabbed, toggle split)
bindsym $mod+s layout stacking
bindsym $mod+w layout tabbed
bindsym $mod+e layout toggle split

# toggle tiling / floating
bindsym $mod+Shift+space floating toggle

# change focus between tiling / floating windows
bindsym $mod+space focus mode_toggle

# focus the parent container
bindsym $mod+a focus parent

# focus the child container
#bindsym $mod+d focus child

# switch to workspace
bindsym $mod+1 workspace 1
bindsym $mod+2 workspace 2
bindsym $mod+3 workspace 3
bindsym $mod+4 workspace 4
# ...

# move focused container to workspace
bindsym $mod+Shift+1 move container to workspace 1
bindsym $mod+Shift+2 move container to workspace 2
bindsym $mod+Shift+3 move container to workspace 3
bindsym $mod+Shift+4 move container to workspace 4
# ...

Resizing windows:

# resize window (you can also use the mouse for that)

mode "resize" {
        # These bindings trigger as soon as you enter the resize mode
        bindsym j resize shrink width 10 px or 10 ppt
        bindsym k resize grow height 10 px or 10 ppt
        bindsym l resize shrink height 10 px or 10 ppt
        bindsym semicolon resize grow width 10 px or 10 ppt

        # same bindings, but for the arrow keys
        bindsym Left resize shrink width 10 px or 10 ppt
        bindsym Down resize grow height 10 px or 10 ppt
        bindsym Up resize shrink height 10 px or 10 ppt
        bindsym Right resize grow width 10 px or 10 ppt

        # back to normal: Enter or Escape
        bindsym Return mode "default"
        bindsym Escape mode "default"
}

bindsym $mod+r mode "resize"