```
mmmm mm mm
*@@@***@@m @@ m@* ** @@ *@@@
@@ *@@m @@ @@* @@
@@ *@@ m@@*@@m @@@@@@ @@@@@ *@@@ @@ mm@*@@ m@@*@@@
@@ @@ @@* *@@ @@ @@ @@ !@ m@* @@ @@ **
@! m@@ @@ @@ @@ !@ !@ !@ !@****** *@@@@@m
@! m@!* @@ !@ @! !@ !@ !@ !@m m @@
!! !!! !@ !! !! !: !! !! !!****** *! @!
!: !!:* !!! !!! !! !: !! :! :!! !! !!
: : : : : : : : : ::: :: ::: : : : : : : : : :: : :!:
```
### My personal dotfiles for a minimal, tiling X11 desktop





---
## Contents
- [About](#about)
- [Features](#features)
- [Installation](#installation)
- [Structure](#structure)
- [sxwm patches](#sxwm-patches)
- [st patches](#st-patches)
- [st quirks](#st-quirks)
- [Keyboard shortcuts (sxwm)](#keyboard-shortcuts-sxwm)
- [Status bar modules (sxbar)](#status-bar-modules-sxbar)
- [Utility scripts (bin/)](#utility-scripts-bin)
- [Screensaver](#screensaver)
- [Wallpaper](#wallpaper)
---
## About
This is my personal dotfiles setup for a lightweight, keyboard-driven X11 desktop
environment built around [`sxwm`](https://github.com/uint23/sxwm) (tiling window
manager, patched with a handful of local fixes — see [below](#sxwm-patches)),
[`sxbar`](https://github.com/MrJensK/sxbar) (status bar),
[`dmenu`](https://github.com/MrJensK/dmenu) (application launcher) and
[`st`](https://st.suckless.org/) (terminal, patched with a curated set of
upstream suckless patches — see [below](#st-patches)). Everything is set up
automatically via [`install.sh`](install.sh) — clone the repo, run the script,
and you have a working environment.
## Features
| | |
|---|---|
| 🪟 | **sxwm** — tiling window manager, patched with 5 local fixes (DPI, taskbar focus, multi-monitor struts, hotplug remap, fullscreen targeting) |
| 📊 | **sxbar** — status bar with clock, date, battery, volume, CPU, brightness, Bluetooth, network, media player, taskbar and start menu popups |
| 🚀 | **dmenu** — fast application launcher (`mod + p`) |
| 📶 | **bluetui** — Bluetooth management right in the terminal |
| 🖥️ | **st** — suckless terminal, patched with 11 upstream fixes (scrollback, clipboard, box-drawing, alpha, blinking cursor, inline images, and more) |
| 🔤 | **JetBrainsMono Nerd Font** — icon support in the bar and terminal |
| 🔊 | **PipeWire** — audio (pipewire + pipewire-pulse + wireplumber) enabled automatically |
| 🔇 | Quiet install — PC speaker (`pcspkr`) and the X bell are disabled |
| 🌙 | Screen blanking / DPMS after 5 minutes of inactivity, plus a TTE-powered terminal screensaver |
| 💻 | Laptop-lid handling — auto-toggle the internal display on lid open/close, with an inhibitor to temporarily disable it |
| 🖱️ | Touchpad tweaks applied automatically (natural scrolling, tap-to-click, etc. via `xinput`) |
| ⌨️ | Swedish keyboard layout (`setxkbmap se`) |
| 🟢 | X autostarts on TTY1 at login |
## Installation
```bash
git clone
cd dotfiles
./install.sh
```
The script installs system packages, builds and installs `sxwm` (with all local
patches applied), `sxbar`, `dmenu`, `st` (with all local patches applied) and
`bluetui` from source, installs the JetBrainsMono Nerd Font, copies config files
(backing up anything that already exists), installs the utility scripts under
`~/.local/bin`, sets up Geany with a Gruvbox theme (see [Geany](#geany) below),
and offers to write `~/.xinitrc` for you. A couple of steps are interactive
(writing `~/.xinitrc`, enabling the lid-watch daemon, setting
`HandleLidSwitch=ignore`, installing the Bluetooth no-autosuspend udev rule,
Rust/`bluetui`, autostarting X on TTY1) so you can opt out if you already have
your own setup for those.
The first question is an optional interactive WiFi setup (`iwd` + `dhcpcd`),
done *before* the install so `apt` has network: it installs the two packages if
missing, starts `iwd`, lets you pick the adapter (only asked when there's more
than one), scans and lets you pick a network (or enter a hidden one), connects
with `iwctl`, and enables `dhcpcd@.service` so DHCP only runs on the
adapter you chose. If `dhcpcd` had to be installed, the generic
`dhcpcd.service` (all interfaces) that Debian enables is turned off again so it
doesn't grab the wired interface. 802.1x (enterprise) networks aren't handled —
configure those manually with `iwctl`.
The installer has an Omarchy-style UI: a centered logo, all the yes/no questions
asked up front (one keypress each), then an unattended run with a progress bar,
the current step and the tail of the log. Full output (apt, make, git, ...) goes
to `/tmp/dotfiles-install.log`; if a step fails the log tail is shown and the
script stops. `./install.sh --demo` plays the UI with fake steps and touches
nothing, useful for previewing or tweaking the look. Without a terminal
(piped/CI) it falls back to plain `==> step` lines and answers "no" to every
question.
`HandleLidSwitch=ignore` only takes effect on the next reboot — the script
deliberately doesn't restart `systemd-logind` live, since on this machine's
manually-started (no display manager) X session that has hung the whole
desktop and forced a hard reboot.
## Structure
```
dotfiles/
├── install.sh # Main install script
├── BG/
│ └── j9huwdxo1zzg1.jpeg # Desktop wallpaper
├── bin/ # Utility scripts, installed to ~/.local/bin
│ ├── gcfs # Minimal gocryptfs vault manager (config-driven)
│ ├── geany # Wrapper: launches Geany with GTK_THEME=Gruvbox-Dark
│ ├── internal-display # on/off/toggle/status for the internal (eDP) screen
│ ├── lid-display-watch # Background daemon: auto internal-display on lid open/close
│ ├── lidsuspend # Temporarily inhibit lid-close suspend
│ ├── mouse-settings # Applies touchpad tweaks via xinput
│ ├── screensaver # Runs random TTE animations in a loop
│ ├── screensaverd # Idle watcher, launches the screensaver after inactivity
│ ├── screensaver-launch # Launches the screensaver fullscreen on every monitor
│ ├── screensaver.txt # ASCII art shown by the screensaver
│ └── status # Overview: WM stack, services, network, bluetooth, audio, battery, lock state
├── *.patch # Local sxwm patches, see "sxwm patches" below
├── st-patches/ # Upstream st patches, see "st patches" below
│ ├── 01-delkey.patch
│ ├── 02-scrollback.patch
│ ├── 03-clipboard.patch
│ ├── 04-boxdraw.patch
│ ├── 05-vertcenter.patch
│ ├── 06-alpha.patch
│ ├── 07-blinking_cursor.patch
│ ├── 08-bold-is-not-bright.patch
│ ├── 09-anysize.patch
│ ├── 10-histsize-5000.patch
│ └── 11-kitty-graphics-protocol.patch
├── applications/
│ └── geany.desktop # Geany launcher override (forces Gruvbox-Dark GTK theme)
├── udev/
│ └── 71-bluetooth-no-autosuspend.rules # Opt-in: disables USB autosuspend on the BT adapter
├── themes/
│ └── Gruvbox-Dark/ # GTK3 theme, installed to ~/.local/share/themes
│ ├── index.theme
│ └── gtk-3.0/gtk.css
└── config/
├── sxwmrc # sxwm config (keybinds, layout, colours)
├── sxbarc # sxbar config (modules, colours, icons)
├── gcfs/
│ └── vaults.conf # gcfs vault list (ships with the commented example only)
├── geany/
│ ├── geany.conf # Geany prefs (Nerd Font, Gruvbox editor colour scheme)
│ └── colorschemes/
│ └── gruvbox.conf # Gruvbox editor colour scheme
└── st/
└── config.def.h # st config (font, colours, alpha, cursor, keybinds)
```
## sxwm patches
`install.sh` clones upstream `sxwm` and applies these on top before building, in
order:
| Patch | Fixes |
|---|---|
| [`cursor-dpi-fix.patch`](cursor-dpi-fix.patch) | Incorrect cursor scaling on high-DPI displays |
| [`net-active-window.patch`](net-active-window.patch) | Adds `_NET_ACTIVE_WINDOW` support — required for sxbar's taskbar module; without it, clicking a taskbar entry does nothing |
| [`multi-monitor-struts.patch`](multi-monitor-struts.patch) | `_NET_WM_STRUT_PARTIAL` reservations weren't clamped to the panel's own monitor, so a monitor's reserved area could exceed its actual size and push windows off-screen |
| [`monitor-hotplug-remap.patch`](monitor-hotplug-remap.patch) | Windows lost their correct monitor association on hotplug (e.g. `lid-display-watch` toggling eDP); monitor ownership is now recomputed from each window's actual position |
| [`fullscreen-monitor-fix.patch`](fullscreen-monitor-fix.patch) | Fullscreening a window that had been moved to another monitor by an external tool (`xdotool`, `screensaver-launch`) fullscreened it on the wrong screen; the target monitor is now recomputed from the window's real geometry |
| [`strut-fresh-screen-size.patch`](strut-fresh-screen-size.patch) | A bottom/right-edge panel (e.g. sxbar's `secondary_bar`) could end up hidden behind normal windows — `reserve_bottom`/`reserve_right` are measured from the *opposite* screen edge and need the full screen size, but read it from `scr_width`/`scr_height`, cached globals only refreshed by `update_mons()`; `reserve_top`/`reserve_left` never depend on screen size so stayed correct. Reads the screen size fresh from the X server instead |
## st patches
`install.sh` clones upstream `st` (v0.9.3) and applies these on top before
building, in order — all sourced from [st.suckless.org/patches](https://st.suckless.org/patches/):
| Patch | Adds |
|---|---|
| [`01-delkey.patch`](st-patches/01-delkey.patch) | Fixes the Delete key to send `\033[3~` instead of the legacy `\033[P` |
| [`02-scrollback.patch`](st-patches/02-scrollback.patch) | Keyboard (`Shift+PageUp/Down`) and mouse-wheel scrollback, disabled in the alt screen (vim, less, etc.) |
| [`03-clipboard.patch`](st-patches/03-clipboard.patch) | Selecting text also copies it to the `CLIPBOARD` selection, not just `PRIMARY`. Local fix on top of the upstream patch: empty/whitespace-only selections (a jittery click, dragging over blank cells) no longer overwrite `CLIPBOARD`, which used to wipe what you'd just copied in e.g. Firefox before you could paste it |
| [`04-boxdraw.patch`](st-patches/04-boxdraw.patch) | Renders box-drawing/braille characters natively instead of relying on the font |
| [`05-vertcenter.patch`](st-patches/05-vertcenter.patch) | Vertically centers glyphs that don't fill the cell (pairs with boxdraw) |
| [`06-alpha.patch`](st-patches/06-alpha.patch) | Background transparency (`-A`/`alpha`), needs a compositor |
| [`07-blinking_cursor.patch`](st-patches/07-blinking_cursor.patch) | Blinking cursor support with a configurable rate |
| [`08-bold-is-not-bright.patch`](st-patches/08-bold-is-not-bright.patch) | Bold text no longer automatically renders in the bright color variant |
| [`09-anysize.patch`](st-patches/09-anysize.patch) | Window can be resized to any pixel size instead of snapping to whole cells |
| [`10-histsize-5000.patch`](st-patches/10-histsize-5000.patch) | Bumps the scrollback buffer from 2000 to 5000 lines (local tweak, not from upstream — matches the previous kitty `scrollback_lines`) |
| [`11-kitty-graphics-protocol.patch`](st-patches/11-kitty-graphics-protocol.patch) | Inline image display via a subset of the [kitty graphics protocol](https://sw.kovidgoyal.net/kitty/graphics-protocol/) (works with `icat`, image.nvim, yazi, etc.). Needs `imlib2`/`zlib` (installed by `install.sh`), plus `imagemagick` and `bc` at runtime for the bundled `icat` script. Hand-merged against the other patches — several hunks conflicted with boxdraw/vertcenter/anysize/blinking_cursor and were reconciled manually; confirmed working |
[`config/st/config.def.h`](config/st/config.def.h) is copied over the patched
source before building, so local tweaks survive a rebuild — edit it and
re-run `install.sh` to apply changes. It's tuned to match the previous kitty
setup: Gruvbox Dark Hard colours, opaque background, steady (non-blinking)
block cursor, JetBrainsMono Nerd Font, and 3 lines scrolled per mouse-wheel
notch. Live font zoom is built into vanilla st — `Ctrl+Shift+PageUp`/`PageDown`
to zoom, `Ctrl+Shift+Home` to reset — no patch needed.
`install.sh` also installs `icat` (the patch's bundled `icat-mini.sh`) to
`~/.local/bin/icat` — run `icat path/to/image.png` in st to display it inline.
Image protocol shortcuts (from patch 11): `Ctrl+Shift+click` on an image
previews it in `feh`, `Ctrl+Shift+middle-click` shows placement info in
`less`, `Ctrl+Shift+F1` toggles graphics debug output, `Ctrl+Shift+F6` dumps
internal state, `Ctrl+Shift+F7` unloads cached images to free RAM, and
`Ctrl+Shift+F8` toggles image display on/off entirely.
## st quirks
Coming from kitty, a few things about st behave differently enough to be
worth writing down:
- **Config is compiled in, not read at runtime.** kitty re-reads
`kitty.conf` on the fly; st bakes `config.def.h` into the binary at build
time as `config.h`. Changing font/colours/keybinds means editing
[`config/st/config.def.h`](config/st/config.def.h) and rebuilding (`./install.sh`,
or the manual steps below) — there's no config file to just edit and
reopen the terminal.
- **`config.h` doesn't auto-refresh from `config.def.h`.** The Makefile's
rule for `config.h` only runs if the file doesn't exist yet — `make clean`
does *not* delete it. If you rebuild in a directory that already has a
stale `config.h` from a previous build, your `config.def.h` edits are
silently ignored. Either `rm -f config.h` before `make`, or always build
in a fresh `git clone` (which is what `install.sh` does).
- **Manual rebuild, step by step** (what `install.sh` does for st, if you
want to do it yourself):
```bash
cd ~ # don't sit inside the directory you're about to rm -rf!
rm -rf /tmp/st-build
git clone --depth=1 https://git.suckless.org/st /tmp/st-build
cd /tmp/st-build
for p in ~/dotfiles/st-patches/*.patch; do patch -p1 < "$p"; done
cp ~/dotfiles/config/st/config.def.h config.def.h
make
sudo make install
```
- **Live font zoom exists but isn't advertised.** `Ctrl+Shift+PageUp`/`PageDown`
zooms the font in/out, `Ctrl+Shift+Home` resets it — built into vanilla st,
no patch needed. Easy to miss since kitty's equivalent
(`change_font_size`) is the one actually documented in `kitty.conf`.
- **No runtime equivalent of kitty's per-launch `--override`.** Things like
`window_padding_width` or `mouse_hide_wait` are compile-time-only in st;
`screensaver-launch` can only override font (`-f`) and alpha (`-A`) when
spawning the screensaver's st instance, not padding or cursor-hiding.
- **Window size is in character cells, not pixels.** `cols`/`rows` in
`config.def.h` set the *initial* size; kitty's `initial_window_width/height`
were literal pixels. With the `anysize` patch the window can still be
resized to any pixel size afterwards, it just doesn't start at one under
sxwm anyway since sxwm tiles it immediately.
- **`icat` needs `imagemagick` and `bc` at runtime**, not just build-time
`imlib2`/`zlib`. `make` succeeds without them; the script only fails the
first time you actually try to display an image. Both are in
`install.sh`'s package list now.
- **Scrollback is intentionally disabled inside full-screen apps** (vim,
less, tmux, etc. — anything using the alt screen). That's the
`02-scrollback.patch` behaviour, not a bug — those apps have their own
scrolling.
- **Selecting text copies to both `PRIMARY` and `CLIPBOARD`** (from
`03-clipboard.patch`), unlike vanilla st which only ever touches
`PRIMARY` on select and needs an explicit keybind to push to `CLIPBOARD`.
## Keyboard shortcuts (sxwm)
`mod` = Super
| Shortcut | Action |
|---|---|
| `mod + Return` | Open st |
| `mod + b` | Open Firefox |
| `mod + p` | dmenu (application launcher) |
| `mod + q` | Close window |
| `mod + shift + e` | Quit sxwm |
| `mod + c` | Centre window |
| `mod + m` | Toggle monocle mode |
| `mod + j` / `mod + k` | Focus next / previous window |
| `mod + comma` / `mod + period` | Focus previous / next monitor |
| `mod + shift + comma` / `mod + shift + period` | Move window to previous / next monitor |
| `mod + shift + j` / `mod + shift + k` | Move window in master/stack |
| `mod + l` / `mod + h` | Increase / decrease master area |
| `mod + ctrl + l` / `mod + ctrl + h` | Increase / decrease stack window size |
| `mod + Arrow keys` | Move window |
| `mod + shift + Arrow keys` | Resize window |
| `mod + o` / `mod + i` | Increase / decrease gaps |
| `mod + space` | Toggle floating |
| `mod + shift + space` | Toggle global floating |
| `mod + shift + f` | Fullscreen |
| `mod + r` | Reload config |
| `mod + alt + [1-4]` | Create scratchpad |
| `mod + ctrl + [1-4]` | Toggle scratchpad |
| `mod + alt + shift + [1-4]` | Remove scratchpad |
| `mod + [1-9]` | Switch workspace |
| `mod + shift + [1-9]` | Move window to workspace |
Media/function keys (volume, brightness, playback) are bound too — see
[`config/sxwmrc`](config/sxwmrc).
## Status bar modules (sxbar)
| Module | Refresh interval | Colour | Notes |
|---|---|---|---|
| 🕐 Clock | 1 s | `#50fa7b` | Centred in the bar |
| 📅 Date | 60 s | `#8be9fd` | |
| 🔋 Battery | 30 s | `#ffb86c` | |
| 🔊 Volume | 5 s | `#ff79c6` | |
| ⚙️ CPU | 3 s | `#bd93f9` | |
| ☀️ Brightness | 5 s | `#f1fa8c` | Hover to reveal a slider |
| 🔵 Bluetooth | 10 s | `#4a9eff` | Click for an on/off/scan/pair menu |
| 👤 User menu | 300 s | `#ff5555` | Click for sleep/log out/shut down |
| 🌐 Network | 10 s | `#2ee6d6` | Click for a WiFi/Ethernet + IP menu |
| 🎵 Media | 2 s | `#cba6f7` | Hover for album art + prev/play-pause/next |
| 🗔 Taskbar | 1 s | — | Secondary bar; click an entry to focus that window |
| ⭐ Start menu | 3600 s | — | Hover for favourite apps as popup buttons |
## Utility scripts (bin/)
Installed to `~/.local/bin` by `install.sh` and wired into `sxwmrc`/`~/.xinitrc`:
- **`mouse-settings`** — applies libinput touchpad tweaks via `xinput` (natural
scrolling on the touchpad only, tap-to-click, etc.). Safe to re-run any time.
- **`internal-display [on|off|toggle|status]`** — (de)activates the internal
(eDP) screen via `xrandr`. Refuses to turn it off if it's the only active
display.
- **`lid-display-watch`** — background daemon (started via sxwm's `exec`) that
watches the laptop lid state through `systemd-logind` and automatically turns
the internal display off on lid-close / on on lid-open, provided an external
monitor is connected. Opt-in during install.
- **`lidsuspend [on|off|status]`** — temporarily disables/re-enables what happens
on lid-close (suspend), without touching `/etc/systemd/logind.conf`, by
holding a systemd inhibitor lock.
- **`screensaverd`** — idle watcher; launches the screensaver after a period of
inactivity (default 150 s) and kills it the moment real activity resumes.
- **`screensaver-launch`** — launches the screensaver fullscreen on every
connected monitor.
- **`screensaver`** — runs random [TTE](https://github.com/ChrisBuilds/terminaltexteffects)
(terminal text effects) animations in a loop over the ASCII art in
`screensaver.txt`.
- **`status`** — read-only overview: which WM-stack processes and systemd
services (bluetooth, pipewire, pipewire-pulse, wireplumber) are running,
network/wifi state, connected Bluetooth devices, default audio sink,
battery/AC state, DPMS/lid/lidsuspend state, and currently playing media.
- **`gcfs [status|mount|umount|mount-all|umount-all|add|init|edit]`** — minimal
[gocryptfs](https://github.com/rfjakob/gocryptfs) vault manager. Reads a
`name|cipherdir|mountpoint` list from `~/.config/gcfs/vaults.conf`, shows each
vault's mount state, and mounts/unmounts by name or number (run with no args
for an interactive menu). Lets `gocryptfs` do its own password prompt, so no
secret passes through the script. Needs `gocryptfs` and `fusermount` (`fuse3`),
both pulled in by `install.sh`.
## Screensaver
The screensaver renders `bin/screensaver.txt` through random
[terminaltexteffects](https://github.com/ChrisBuilds/terminaltexteffects)
animations (installed via `pipx` during setup) inside a fullscreen st
window, and exits the instant you touch the mouse or keyboard.
## Geany
Geany is set up with a Gruvbox dark theme and JetBrainsMono Nerd Font, covering
both the editor and the surrounding GTK chrome (menus, toolbar, sidebar):
- **[`config/geany/geany.conf`](config/geany/geany.conf)** — editor/tagbar/msgwin
fonts set to JetBrainsMono Nerd Font, `color_scheme=gruvbox.conf`.
- **[`config/geany/colorschemes/gruvbox.conf`](config/geany/colorschemes/gruvbox.conf)**
— Gruvbox dark syntax colours for the editor itself.
- **[`themes/Gruvbox-Dark/`](themes/Gruvbox-Dark/)** — a GTK3 theme (Adwaita-dark's
structure with Gruvbox colours layered on via `@define-color`) for the rest of
the window, since Geany's own `color_scheme` only styles the editor pane, not
the menus/toolbar/sidebar.
- **[`bin/geany`](bin/geany)** — wrapper installed to `~/.local/bin/geany` that
launches the real `/usr/bin/geany` with `GTK_THEME=Gruvbox-Dark`. A shell
alias isn't enough here: dmenu/sxwm never source `~/.bashrc`, but `~/.local/bin`
is first in `PATH` (see `~/.xinitrc`), so the wrapper shadows the system binary
for every launch path (dmenu, `.desktop`, terminal).
- **[`applications/geany.desktop`](applications/geany.desktop)** — copy of the
system `.desktop` entry with `Exec=env GTK_THEME=Gruvbox-Dark geany %F`, so
app-launcher entries that read `.desktop` files (not just `$PATH`) pick up the
theme too.
The theme is scoped to Geany only — the system GTK theme (Adwaita) is untouched,
so other GTK apps aren't affected.
## Wallpaper
Set automatically via `feh --bg-scale` in [`config/sxwmrc`](config/sxwmrc).
---
Built for a fast, minimal, keyboard-driven workday.