``` 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) so you can opt out if you already have your own setup for those. `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` | | [`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
Desktop wallpaper
Set automatically via `feh --bg-scale` in [`config/sxwmrc`](config/sxwmrc). ---
Built for a fast, minimal, keyboard-driven workday.