``` 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 ![Platform](https://img.shields.io/badge/platform-Linux%20%2F%20Debian-informational?style=flat-square) ![Shell](https://img.shields.io/badge/shell-Bash-4EAA25?style=flat-square&logo=gnubash&logoColor=white) ![WM](https://img.shields.io/badge/window%20manager-sxwm-blueviolet?style=flat-square) ![Bar](https://img.shields.io/badge/status%20bar-sxbar-orange?style=flat-square) ![License](https://img.shields.io/badge/license-personal%20use-lightgrey?style=flat-square)
--- ## 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. Both are skipped if already usable — it checks for the actual `iwctl` binary and a `dhcpcd@.service` unit, not just the package name, since Debian's `dhcpcd-base` installs the `dhcpcd` binary without the systemd units the script needs. If either is missing and there's no network yet to install them with, it says so and skips WiFi setup instead of trying and failing. 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`. Right after that (WiFi configured or not — e.g. you're on Ethernet), the installer checks it actually has a working connection before starting the unattended run, since apt and the `sxwm`/`sxbar`/`st` clones need one. With no connection it asks whether to continue anyway (default no); declining exits with a hint to plug in a cable or rerun and answer yes to the WiFi question. 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 │ ├── crypt # Mounts the gocryptfs vault on a USB stick to ~/crypt │ ├── 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`. - **`crypt [mount|umount|status|toggle]`** — the no-config version of `gcfs` for one USB stick: finds the gocryptfs cipherdir (the directory holding `gocryptfs.conf`) under `/media/$USER` or `/run/media/$USER`, mounts it on `~/crypt` and lets `gocryptfs` ask for the password. If the stick isn't mounted yet it tries `udisksctl`; with several vaults it shows a menu. `CRYPT_MOUNT` / `CRYPT_CIPHERDIR` override the mountpoint / cipherdir. ## 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
Desktop wallpaper
Set automatically via `feh --bg-scale` in [`config/sxwmrc`](config/sxwmrc). ---
Built for a fast, minimal, keyboard-driven workday.