├── .clang-format ├── .clangd ├── .gitignore ├── CHANGELOG.md ├── LICENSE ├── Makefile ├── README.md ├── default_sxwmrc ├── images ├── 1.png ├── 3.png ├── 4.png ├── sxwm_logo.png ├── sxwm_logo.psd └── x.png ├── src ├── config.h ├── defs.h ├── parser.c ├── parser.h └── sxwm.c ├── sxwm.1 └── sxwm.desktop /.clang-format: -------------------------------------------------------------------------------- 1 | BasedOnStyle: LLVM 2 | UseTab: ForIndentation 3 | TabWidth: 4 4 | IndentWidth: 4 5 | BreakBeforeBraces: Custom 6 | BraceWrapping: 7 | AfterFunction: true 8 | AfterControlStatement: false 9 | AfterEnum: false 10 | AfterStruct: false 11 | AfterUnion: false 12 | BeforeElse: true 13 | IndentBraces: false 14 | AllowShortIfStatementsOnASingleLine: false 15 | AllowShortLoopsOnASingleLine: false 16 | AllowShortFunctionsOnASingleLine: None 17 | ColumnLimit: 120 18 | SortIncludes: false 19 | SpaceBeforeParens: ControlStatements 20 | IndentCaseLabels: true 21 | -------------------------------------------------------------------------------- /.clangd: -------------------------------------------------------------------------------- 1 | CompileFlags: 2 | Add: [ 3 | "-x", "c", 4 | "-std=c99", 5 | "-Wall", 6 | "-Wextra", 7 | "-O3", 8 | "-Isrc" 9 | ] 10 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | build/ 2 | sxwm 3 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ### Changelog 2 | 3 | All notable changes to this project will be documented in this file. 4 | 5 | #### v1.6 (current) 6 | - **NEW**: True multi-monitor support 7 | - **FIXED**: Invisible windows of minimized programs 8 | - **FIXED**: Zombie processes spawned from apps 9 | - **FIXED**: Invalid sample config 10 | - **FIXED**: Undefined behaviour in `parse_col` 11 | 12 | #### v1.5 13 | - **NEW**: Using XCursor instead of cursor font && new logo. 14 | - **FIXED**: Proper bind resetting on refresh config. && Multi-arg binds now work due to new and improved spawn function 15 | - **CHANGE**: No longer using INIT_WORKSPACE macro, proper workspace handling. New sxwmrc 16 | 17 | #### v1.4 18 | - **CHANGE**: Added motion throttle && master width general options 19 | 20 | #### v1.3 21 | - **CHANGE**: ulong, u_char uint are gone 22 | 23 | #### v1.2 24 | - **NEW**: Parser support 25 | - **FIXED**: Quit syntax && Freeing cursor on exit 26 | 27 | #### v1.1 28 | - **NEW**: Xinerama support, swap windows with Mod + Shift + Drag 29 | - **FIXED**: New windows in `global_floating` mode spawn centered 30 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) Abhinav Prasai 2025 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | CC ?= gcc 2 | CFLAGS ?= -std=c99 -Wall -Wextra -O3 -Isrc 3 | LDFLAGS ?= -lX11 -lXinerama -lXcursor 4 | 5 | PREFIX ?= /usr/local 6 | BIN := sxwm 7 | SRC_DIR := src 8 | OBJ_DIR := build 9 | SRC := $(wildcard $(SRC_DIR)/*.c) 10 | OBJ := $(patsubst $(SRC_DIR)/%.c,$(OBJ_DIR)/%.o,$(SRC)) 11 | DEP := $(OBJ:.o=.d) 12 | 13 | MAN := sxwm.1 14 | MAN_DIR := $(PREFIX)/share/man/man1 15 | 16 | XSESSIONS := $(DESTDIR)$(PREFIX)/share/xsessions 17 | 18 | all: $(BIN) 19 | 20 | $(BIN): $(OBJ) 21 | $(CC) $(CFLAGS) -o $@ $^ $(LDFLAGS) 22 | 23 | $(OBJ_DIR)/%.o: $(SRC_DIR)/%.c | $(OBJ_DIR) 24 | @mkdir -p $(dir $@) 25 | $(CC) $(CFLAGS) -MMD -MP -c -o $@ $< 26 | 27 | -include $(DEP) 28 | 29 | $(OBJ_DIR): 30 | @mkdir -p $@ 31 | 32 | clean: 33 | @rm -rf $(OBJ_DIR) $(BIN) 34 | 35 | install: all 36 | @echo "Installing $(BIN) to $(DESTDIR)$(PREFIX)/bin..." 37 | @mkdir -p "$(DESTDIR)$(PREFIX)/bin" 38 | @install -m 755 $(BIN) "$(DESTDIR)$(PREFIX)/bin/$(BIN)" 39 | @echo "Installing sxwm.desktop to $(XSESSIONS)..." 40 | @mkdir -p "$(XSESSIONS)" 41 | @install -m 644 sxwm.desktop "$(XSESSIONS)/sxwm.desktop" 42 | @echo "Installing man page to $(DESTDIR)$(MAN_DIR)..." 43 | @mkdir -p $(DESTDIR)$(MAN_DIR) 44 | @install -m 644 $(MAN) $(DESTDIR)$(MAN_DIR)/ 45 | @echo "Copying default configuration to $(DESTDIR)$(PREFIX)/share/sxwmrc..." 46 | @mkdir -p "$(DESTDIR)$(PREFIX)/share" 47 | @install -m 644 default_sxwmrc "$(DESTDIR)$(PREFIX)/share/sxwmrc" 48 | @echo "Installation complete." 49 | 50 | uninstall: 51 | @echo "Uninstalling $(BIN) from $(DESTDIR)$(PREFIX)/bin..." 52 | @rm -f "$(DESTDIR)$(PREFIX)/bin/$(BIN)" 53 | @echo "Uninstalling sxwm.desktop from $(XSESSIONS)..." 54 | @rm -f "$(XSESSIONS)/sxwm.desktop" 55 | @echo "Uninstalling man page from $(DESTDIR)$(MAN_DIR)..." 56 | @rm -f $(DESTDIR)$(MAN_DIR)/$(MAN) 57 | @echo "Uninstallation complete." 58 | 59 | .PHONY: all clean install uninstall 60 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | > ⚠️ **Note:** I won’t be updating this project for a month or so due to exams. 2 | > Issues & PRs are welcome, just don't expect a quick response 🥀🥀 3 | > **24/05/25:** I have very _little_ time but I am able to develop some features 4 | > Thank you to the wonderful people who have sumbitted fixes and other PR's 5 | 6 |
7 | 8 |
9 | Minimal. Fast. Configurable. Tiling Window Manager for X11 10 |
11 | Abhinav Prasai (2025) 12 |

13 | 14 | 15 |
16 | 17 | --- 18 | 19 | ## Table of Contents 20 | - [Features](#features) 21 | - [Screenshots](#screenshots) 22 | - [Configuration](#configuration) 23 | - [Keybindings](#keybindings) 24 | - [Example Bindings](#example-bindings) 25 | - [Default Keybindings](#default-keybindings) 26 | - [Dependencies](#dependencies) 27 | - [Build & Install](#build--install) 28 | - [Makefile Targets](#makefile-targets) 29 | - [Thanks & Inspiration](#thanks--inspiration) 30 | 31 | --- 32 | 33 | ## Features 34 | 35 | - **Tiling & Floating**: Switch seamlessly between layouts. 36 | - **Workspaces**: 9 workspaces, fully integrated with your bar. 37 | - **Live Config Reload**: Change your config and reload instantly with a keybind. 38 | - **Easy Configuration**: Human-friendly `sxwmrc` file, no C required. 39 | - **Master-Stack Layout**: DWM-inspired productive workflow. 40 | - **Mouse Support**: Move, swap, resize, and focus windows with the mouse. 41 | - **Zero Dependencies**: Only `libX11` and `Xinerama` required. 42 | - **Lightweight**: Single C file, minimal headers, compiles in seconds. 43 | - **Bar Friendly**: Works great with [sxbar](https://github.com/uint23/sxbar). 44 | - **Xinerama Support**: Multi-monitor ready. 45 | - **Fast**: Designed for speed and low resource usage. 46 | 47 | --- 48 | 49 | ## Screenshots 50 | See on the [website](https://uint23.xyz/sxwm.html) 51 | 52 | --- 53 | 54 | ## Configuration 55 | 56 | `sxwm` is configured via a simple text file located at `~/.config/sxwmrc`. Changes can be applied instantly by reloading the configuration (`MOD + r`). 57 | 58 | The file uses a `key : value` format. Lines starting with `#` are ignored. 59 | 60 | ### General Options 61 | 62 | | Option | Type | Default | Description | 63 | |--------------------------|---------|-----------|-----------------------------------------------------------------------------| 64 | | `mod_key` | String | `super` | Sets the primary modifier key (`alt`, `super`, `ctrl`). | 65 | | `gaps` | Integer | `10` | Pixels between windows and screen edges. | 66 | | `border_width` | Integer | `1` | Thickness of window borders in pixels. | 67 | | `focused_border_colour` | Hex | `#c0cbff` | Border color for the currently focused window. | 68 | | `unfocused_border_colour`| Hex | `#555555` | Border color for unfocused windows. | 69 | | `swap_border_colour` | Hex | `#fff4c0` | Border color when selecting a window to swap (`MOD+Shift+Drag`). | 70 | | `master_width` | Integer | `60` | Percentage of the screen width for the master window. | 71 | | `resize_master_amount` | Integer | `1` | Percent to increase/decrease master width. | 72 | | `snap_distance` | Integer | `5` | Distance (px) before a floating window snaps to edge. | 73 | | `motion_throttle` | Integer | `60` | Target FPS for mouse drag actions. | 74 | | `should_float` | String | `"st"` | Always-float rule. Multiple entries should be comma-seperated. Optionally, entries can be enclosed in quotes.| 75 | | `new_win_focus` | Bool | `true` | Whether openening new windows should also set focus to them or keep on current window.| 76 | 77 | --- 78 | 79 | ## Keybindings 80 | 81 | ### Syntax 82 | 83 | ```sh 84 | bind : modifier + modifier + ... + key : action 85 | ``` 86 | 87 | - **Modifiers**: `mod`, `shift`, `ctrl`, `alt`, `super` 88 | - **Key**: Case-insensitive keysym (e.g., `Return`, `q`, `1`) 89 | - **Action**: Either an external command (in quotes) or internal function. 90 | 91 | ```sh 92 | workspace : modifier + modifier + ... + key : move n 93 | workspace : modifier + modifier + ... + key : swap n 94 | ``` 95 | - **Modifiers**: `mod`, `shift`, `ctrl`, `alt`, `super` 96 | - **Key**: Case-insensitive keysym (e.g., `Return`, `q`, `1`) 97 | - **move**: Move to that worspace 98 | - **swap**: Swap window to that workspace 99 | - **n**: Workspace number 100 | 101 | ### Available Functions 102 | 103 | | Function Name | Description | 104 | |----------------------|--------------------------------------------------------------| 105 | | `close_window` | Closes the focused window. | 106 | | `decrease_gaps` | Shrinks gaps. | 107 | | `focus_next` | Moves focus forward in the stack. | 108 | | `focus_previous` | Moves focus backward in the stack. | 109 | | `increase_gaps` | Expands gaps. | 110 | | `master_next` | Moves focused window down in master/stack order. | 111 | | `master_prev` | Moves focused window up in master/stack order. | 112 | | `quit` | Exits `sxwm`. | 113 | | `reload_config` | Reloads config. | 114 | | `master_increase` | Expands master width. | 115 | | `master_decrease` | Shrinks master width. | 116 | | `toggle_floating` | Toggles floating state of current window. | 117 | | `global_floating` | Toggles floating state for all windows. | 118 | | `fullscreen` | Fullscreen toggle. | 119 | 120 | ### Example Bindings 121 | 122 | ```yaml 123 | # Launch terminal 124 | bind : mod + Return : "st" 125 | # Close window 126 | bind : mod + shift + q : close_window 127 | 128 | # Switch workspace 129 | workspace : mod + 3 : move 3 130 | # Move window to workspace 131 | workspace : mod + shift + 5 : swap 5 132 | ``` 133 | 134 | --- 135 | 136 | ## Default Keybindings 137 | 138 | ### Window Management 139 | 140 | | Combo | Action | 141 | | ---------------------------- | ------------------------- | 142 | | Mouse | Focus under cursor | 143 | | `MOD` + Left Mouse | Move window by mouse | 144 | | `MOD` + Right Mouse | Resize window by mouse | 145 | | `MOD` + `j` / `k` | Focus next / previous | 146 | | `MOD` + `Shift` + `j` / `k` | Move in master stack | 147 | | `MOD` + `Space` | Toggle floating | 148 | | `MOD` + `Shift` + `Space` | Toggle all floating | 149 | | `MOD` + `=` / `-` | Increase/Decrease gaps | 150 | | `MOD` + `f` | Fullscreen toggle | 151 | | `MOD` + `q` | Close focused window | 152 | | `MOD` + `1-9` | Switch workspace 1–9 | 153 | | `MOD` + `Shift` + `1-9` | Move window to WS 1–9 | 154 | 155 | ### Programs 156 | 157 | | Combo | Action | Program | 158 | | -------------------- | ---------- | ---------- | 159 | | `MOD` + `Return` | Terminal | `st` | 160 | | `MOD` + `b` | Browser | `firefox` | 161 | | `MOD` + `p` | Launcher | `dmenu_run`| 162 | 163 | --- 164 | 165 | ## Dependencies 166 | 167 | - `libX11` (Xorg client library) 168 | - `Xinerama` 169 | - `XCursor` 170 | - GCC or Clang & Make 171 | 172 |
173 | Debian / Ubuntu / Linux Mint 174 |
sudo apt update
175 | sudo apt install libx11-dev libxcursor-dev libxinerama-dev build-essential
176 |
177 | 178 |
179 | Arch Linux / Manjaro 180 |
sudo pacman -Syy
181 | sudo pacman -S libx11 libxinerama gcc make
182 |
183 | 184 |
185 | Gentoo 186 |
sudo emerge --ask x11-libs/libX11 x11-libs/libXinerama sys-devel/gcc sys-devel/make
187 | sudo emaint -a sync
188 | 
189 |
190 | 191 |
192 | Void Linux 193 |
sudo xbps-install -S
194 | sudo xbps-install libX11-devel libXinerama-devel gcc make
195 |
196 | 197 |
198 | Fedora / RHEL / AlmaLinux / Rocky 199 |
sudo dnf update
200 | sudo dnf install libX11-devel libXinerama-devel gcc make
201 |
202 | 203 |
204 | OpenSUSE (Leap / Tumbleweed) 205 |
sudo zypper refresh
206 | sudo zypper install libX11-devel libXinerama-devel gcc make
207 |
208 | 209 |
210 | Alpine Linux 211 |
doas apk update
212 | doas apk add libx11-dev libxinerama-dev gcc make musl-dev
213 |
214 | 215 |
216 | NixOS 217 |
buildInputs = [
218 |   pkgs.xorg.libX11
219 |   pkgs.xorg.libXinerama
220 |   pkgs.libgcc
221 |   pkgs.gnumake
222 | ];
223 | sudo nixos-rebuild switch
224 | 
225 |
226 | 227 |
228 | Slackware 229 |
slackpkg update
230 | slackpkg install gcc make libX11 libXinerama
231 |
232 | 233 |
234 | OpenBSD 235 |
doas pkg_add gmake
236 | You will also need the X sets (xbase, xfonts, xserv and xshare) installed. 237 | When you make the code, use gmake instead of make (which will be BSD make). Use the following command to build: gmake CFLAGS="-I/usr/X11R6/include -Wall -Wextra -O3 -Isrc" LDFLAGS="-L/usr/X11R6/lib -lX11 -lXinerama -lXcursor" 238 |
239 | 240 |
241 | FreeBSD 242 |
# If you use doas or su instead of sudo, modify the following commands accordingly.
243 | sudo pkg update
244 | sudo pkg install gcc gmake libX11 libXinerama
245 |
246 | 247 | --- 248 | 249 | ## Build & Install 250 | 251 | ### Arch Linux (AUR) 252 | 253 | ```sh 254 | yay -S sxwm 255 | # OR for latest features: 256 | yay -S sxwm-git 257 | ``` 258 | 259 | ### Build from Source 260 | 261 | ```sh 262 | git clone --depth=1 https://github.com/uint23/sxwm.git 263 | cd sxwm/ 264 | # Replace make with gmake on FreeBSD 265 | make 266 | sudo make clean install 267 | ``` 268 | 269 | ### Run 270 | 271 | Add to your `~/.xinitrc`: 272 | ```sh 273 | exec sxwm 274 | ``` 275 | 276 | --- 277 | ## Makefile Targets 278 | 279 | | Target | Description | 280 | |-----------------------|----------------------------------------------------------| 281 | | `make` / `make all` | Build the `sxwm` binary | 282 | | `make clean` | Remove build artifacts | 283 | | `make install` | Install `sxwm` to `$(PREFIX)/bin` (default `/usr/local`) | 284 | | `make uninstall` | Remove installed binary | 285 | | `make clean install` | Clean then install | 286 | 287 | > Override install directory with `PREFIX`: 288 | > ```sh 289 | > make install PREFIX=$HOME/.local 290 | > ``` 291 | 292 | --- 293 | 294 | ## Thanks & Inspiration 295 | 296 | - [dwm](https://dwm.suckless.org) — Tiling & source code 297 | - [i3](https://i3wm.org) — Easy configuration 298 | - [sowm](https://github.com/dylanaraps/sowm) — README inspiration 299 | - [tinywm](http://incise.org/tinywm.html) — Minimal X11 WM 300 | 301 | --- 302 | 303 |

304 | Contributions welcome! Open issues or submit PRs. 305 |

306 | -------------------------------------------------------------------------------- /default_sxwmrc: -------------------------------------------------------------------------------- 1 | # Colour Themes: 2 | focused_border_colour : #c0cbff 3 | unfocused_border_colour : #555555 4 | swap_border_colour : #fff4c0 5 | 6 | # General Options: 7 | gaps : 10 8 | border_width : 1 9 | master_width : 60 # Percentage of screen width 10 | resize_master_amount : 1 11 | snap_distance : 5 12 | motion_throttle : 60 # Set to screen refresh rate for smoothest motions 13 | should_float : st 14 | new_win_focus : true 15 | 16 | # Keybinds: 17 | # Commands must be surrounded with "" 18 | # Function calls don't need this 19 | 20 | mod_key : super 21 | 22 | # Application Launchers: 23 | bind : mod + Return : "st" 24 | bind : mod + b : "firefox" 25 | bind : mod + p : "dmenu_run" 26 | 27 | # Window Management: 28 | call : mod + shift + q : close_window 29 | call : mod + shift + e : quit 30 | 31 | # Focus Movement: 32 | call : mod + j : focus_next 33 | call : mod + k : focus_prev 34 | 35 | # Master/Stack Movement 36 | call : mod + shift + j : master_next 37 | call : mod + shift + k : master_previous 38 | 39 | # Master Area Resize 40 | call : mod + l : master_increase 41 | call : mod + h : master_decrease 42 | 43 | # Gaps 44 | call : mod + equal : increase_gaps 45 | call : mod + minus : decrease_gaps 46 | 47 | # Floating/Fullscreen 48 | call : mod + space : toggle_floating 49 | call : mod + shift + space : global_floating 50 | call : mod + shift + f : fullscreen 51 | 52 | # Reload Config 53 | call : mod + r : reload_config 54 | 55 | # Workspaces (1-9) 56 | workspace : mod + 1 : move 1 57 | workspace : mod + shift + 1 : swap 1 58 | workspace : mod + 2 : move 2 59 | workspace : mod + shift + 2 : swap 2 60 | workspace : mod + 3 : move 3 61 | workspace : mod + shift + 3 : swap 3 62 | workspace : mod + 4 : move 4 63 | workspace : mod + shift + 4 : swap 4 64 | workspace : mod + 5 : move 5 65 | workspace : mod + shift + 5 : swap 5 66 | workspace : mod + 6 : move 6 67 | workspace : mod + shift + 6 : swap 6 68 | workspace : mod + 7 : move 7 69 | workspace : mod + shift + 7 : swap 7 70 | workspace : mod + 8 : move 8 71 | workspace : mod + shift + 8 : swap 8 72 | workspace : mod + 9 : move 9 73 | workspace : mod + shift + 9 : swap 9 74 | -------------------------------------------------------------------------------- /images/1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/uint23/sxwm/26d38b5b0831d9afb9a91fc840cdbd3268d5c359/images/1.png -------------------------------------------------------------------------------- /images/3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/uint23/sxwm/26d38b5b0831d9afb9a91fc840cdbd3268d5c359/images/3.png -------------------------------------------------------------------------------- /images/4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/uint23/sxwm/26d38b5b0831d9afb9a91fc840cdbd3268d5c359/images/4.png -------------------------------------------------------------------------------- /images/sxwm_logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/uint23/sxwm/26d38b5b0831d9afb9a91fc840cdbd3268d5c359/images/sxwm_logo.png -------------------------------------------------------------------------------- /images/sxwm_logo.psd: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/uint23/sxwm/26d38b5b0831d9afb9a91fc840cdbd3268d5c359/images/sxwm_logo.psd -------------------------------------------------------------------------------- /images/x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/uint23/sxwm/26d38b5b0831d9afb9a91fc840cdbd3268d5c359/images/x.png -------------------------------------------------------------------------------- /src/config.h: -------------------------------------------------------------------------------- 1 | /* See LICENSE for more information on use */ 2 | #include 3 | #include 4 | #include "defs.h" 5 | 6 | CMD(terminal, "st"); 7 | CMD(browser, "firefox"); 8 | 9 | const Binding binds[] = { 10 | {Mod4Mask | ShiftMask, XK_e, {.fn = quit}, TYPE_FUNC}, 11 | {Mod4Mask | ShiftMask, XK_q, {.fn = close_focused}, TYPE_FUNC}, 12 | 13 | {Mod4Mask, XK_j, {.fn = focus_next}, TYPE_FUNC}, 14 | {Mod4Mask, XK_k, {.fn = focus_prev}, TYPE_FUNC}, 15 | 16 | {Mod4Mask | ShiftMask, XK_j, {.fn = move_master_next}, TYPE_FUNC}, 17 | {Mod4Mask | ShiftMask, XK_k, {.fn = move_master_prev}, TYPE_FUNC}, 18 | 19 | {Mod4Mask, XK_l, {.fn = resize_master_add}, TYPE_FUNC}, 20 | {Mod4Mask, XK_h, {.fn = resize_master_sub}, TYPE_FUNC}, 21 | 22 | {Mod4Mask, XK_equal, {.fn = inc_gaps}, TYPE_FUNC}, 23 | {Mod4Mask, XK_minus, {.fn = dec_gaps}, TYPE_FUNC}, 24 | 25 | {Mod4Mask, XK_space, {.fn = toggle_floating}, TYPE_FUNC}, 26 | {Mod4Mask | ShiftMask, XK_space, {.fn = toggle_floating_global}, TYPE_FUNC}, 27 | {Mod4Mask | ShiftMask, XK_f, {.fn = toggle_fullscreen}, TYPE_FUNC}, 28 | 29 | {Mod4Mask, XK_Return, {.cmd = terminal}, TYPE_CMD}, 30 | {Mod4Mask, XK_b, {.cmd = browser}, TYPE_CMD}, 31 | {Mod4Mask, XK_p, {.cmd = (const char *[]){"dmenu_run", NULL}}, TYPE_CMD}, 32 | 33 | {Mod4Mask, XK_r, {.fn = reload_config}, TYPE_FUNC}, 34 | 35 | {Mod4Mask, XK_1, {.ws = 0}, TYPE_CWKSP}, 36 | {Mod4Mask | ShiftMask, XK_1, {.ws = 0}, TYPE_MWKSP}, 37 | {Mod4Mask, XK_2, {.ws = 1}, TYPE_CWKSP}, 38 | {Mod4Mask | ShiftMask, XK_2, {.ws = 1}, TYPE_MWKSP}, 39 | {Mod4Mask, XK_3, {.ws = 2}, TYPE_CWKSP}, 40 | {Mod4Mask | ShiftMask, XK_3, {.ws = 2}, TYPE_MWKSP}, 41 | {Mod4Mask, XK_4, {.ws = 3}, TYPE_CWKSP}, 42 | {Mod4Mask | ShiftMask, XK_4, {.ws = 3}, TYPE_MWKSP}, 43 | {Mod4Mask, XK_5, {.ws = 4}, TYPE_CWKSP}, 44 | {Mod4Mask | ShiftMask, XK_5, {.ws = 4}, TYPE_MWKSP}, 45 | {Mod4Mask, XK_6, {.ws = 5}, TYPE_CWKSP}, 46 | {Mod4Mask | ShiftMask, XK_6, {.ws = 5}, TYPE_MWKSP}, 47 | {Mod4Mask, XK_7, {.ws = 6}, TYPE_CWKSP}, 48 | {Mod4Mask | ShiftMask, XK_7, {.ws = 6}, TYPE_MWKSP}, 49 | {Mod4Mask, XK_8, {.ws = 7}, TYPE_CWKSP}, 50 | {Mod4Mask | ShiftMask, XK_8, {.ws = 7}, TYPE_MWKSP}, 51 | {Mod4Mask, XK_9, {.ws = 8}, TYPE_CWKSP}, 52 | {Mod4Mask | ShiftMask, XK_9, {.ws = 8}, TYPE_MWKSP}, 53 | }; -------------------------------------------------------------------------------- /src/defs.h: -------------------------------------------------------------------------------- 1 | /* See LICENSE for more information on use */ 2 | #pragma once 3 | #include 4 | #define SXWM_VERSION "sxwm ver. 1.5" 5 | #define SXWM_AUTHOR "(C) Abhinav Prasai 2025" 6 | #define SXWM_LICINFO "See LICENSE for more info" 7 | 8 | #define ALT Mod1Mask 9 | #define SUPER Mod4Mask 10 | #define SHIFT ShiftMask 11 | 12 | #define MARGIN (gaps + BORDER_WIDTH) 13 | #define OUT_IN (2 * BORDER_WIDTH) 14 | #define MF_MIN 0.05f 15 | #define MF_MAX 0.95f 16 | #define MAX_MONITORS 32 17 | #define MAX(a, b) ((a) > (b) ? (a) : (b)) 18 | #define MIN(a, b) ((a) < (b) ? (a) : (b)) 19 | #define LENGTH(X) (sizeof X / sizeof X[0]) 20 | #define UDIST(a,b) abs((int)(a) - (int)(b)) 21 | # define CLAMP(x, lo, hi) (( (x) < (lo) ) ? (lo) : ( (x) > (hi) ) ? (hi) : (x)) 22 | #define MAXCLIENTS 99 23 | #define BIND(mod, key, cmdstr) { (mod), XK_##key, { cmdstr }, False } 24 | #define CALL(mod, key, fnptr) { (mod), XK_##key, { .fn = fnptr }, True } 25 | #define CMD(name, ...) \ 26 | const char *name[] = { __VA_ARGS__, NULL } 27 | 28 | #define TYPE_CWKSP 0 29 | #define TYPE_MWKSP 1 30 | #define TYPE_FUNC 2 31 | #define TYPE_CMD 3 32 | 33 | #define NUM_WORKSPACES 9 34 | #define WORKSPACE_NAMES \ 35 | "1" "\0"\ 36 | "2" "\0"\ 37 | "3" "\0"\ 38 | "4" "\0"\ 39 | "5" "\0"\ 40 | "6" "\0"\ 41 | "7" "\0"\ 42 | "8" "\0"\ 43 | "9" "\0"\ 44 | 45 | typedef enum { 46 | DRAG_NONE, 47 | DRAG_MOVE, 48 | DRAG_RESIZE, 49 | DRAG_SWAP 50 | } DragMode; 51 | 52 | typedef void (*EventHandler)(XEvent *); 53 | 54 | typedef union { 55 | const char **cmd; 56 | void (*fn)(void); 57 | int ws; 58 | } Action; 59 | 60 | typedef struct { 61 | int mods; 62 | KeySym keysym; 63 | Action action; 64 | int type; 65 | } Binding; 66 | 67 | typedef struct Client{ 68 | Window win; 69 | int x, y, h, w; 70 | int orig_x, orig_y, orig_w, orig_h; 71 | int mon; 72 | int ws; 73 | Bool fixed; 74 | Bool floating; 75 | Bool fullscreen; 76 | Bool mapped; 77 | struct Client *next; 78 | } Client; 79 | 80 | typedef struct { 81 | int modkey; 82 | int gaps; 83 | int border_width; 84 | long border_foc_col; 85 | long border_ufoc_col; 86 | long border_swap_col; 87 | float master_width[MAX_MONITORS]; 88 | int motion_throttle; 89 | int resize_master_amt; 90 | int snap_distance; 91 | int bindsn; 92 | Bool new_win_focus; 93 | Binding binds[256]; 94 | char **should_float[256]; 95 | } Config; 96 | 97 | typedef struct { 98 | int x, y; 99 | int w, h; 100 | } Monitor; 101 | 102 | extern void close_focused(void); 103 | extern void dec_gaps(void); 104 | extern void focus_next(void); 105 | extern void focus_prev(void); 106 | extern void inc_gaps(void); 107 | extern void move_master_next(void); 108 | extern void move_master_prev(void); 109 | extern long parse_col(const char *hex); 110 | extern void quit(void); 111 | extern void reload_config(void); 112 | extern void resize_master_add(void); 113 | extern void resize_master_sub(void); 114 | extern void toggle_floating(void); 115 | extern void toggle_floating_global(void); 116 | extern void toggle_fullscreen(void); 117 | -------------------------------------------------------------------------------- /src/parser.c: -------------------------------------------------------------------------------- 1 | #define _POSIX_C_SOURCE 200809L 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | 11 | #include "parser.h" 12 | #include "defs.h" 13 | 14 | static const struct { 15 | const char *name; 16 | void (*fn)(void); 17 | } call_table[] = {{"close_window", close_focused}, 18 | {"decrease_gaps", dec_gaps}, 19 | {"focus_next", focus_next}, 20 | {"focus_prev", focus_prev}, 21 | {"increase_gaps", inc_gaps}, 22 | {"master_next", move_master_next}, 23 | {"master_previous", move_master_prev}, 24 | {"quit", quit}, 25 | {"reload_config", reload_config}, 26 | {"master_increase", resize_master_add}, 27 | {"master_decrease", resize_master_sub}, 28 | {"toggle_floating", toggle_floating}, 29 | {"global_floating", toggle_floating_global}, 30 | {"fullscreen", toggle_fullscreen}, 31 | {NULL, NULL}}; 32 | 33 | static void remap_and_dedupe_binds(Config *cfg) 34 | { 35 | for (int i = 0; i < cfg->bindsn; i++) { 36 | for (int j = i + 1; j < cfg->bindsn; j++) { 37 | if (cfg->binds[i].mods == cfg->binds[j].mods && cfg->binds[i].keysym == cfg->binds[j].keysym) { 38 | memmove(&cfg->binds[j], &cfg->binds[j + 1], sizeof(Binding) * (cfg->bindsn - j - 1)); 39 | cfg->bindsn--; 40 | j--; 41 | } 42 | } 43 | } 44 | } 45 | 46 | static char *strip(char *s) 47 | { 48 | while (*s && isspace((unsigned char)*s)) { 49 | s++; 50 | } 51 | char *e = s + strlen(s) - 1; 52 | while (e > s && isspace((unsigned char)*e)) { 53 | *e-- = '\0'; 54 | } 55 | return s; 56 | } 57 | 58 | static char *strip_quotes(char *s) 59 | { 60 | size_t L = strlen(s); 61 | if (L > 0 && s[0] == '"') { 62 | s++; 63 | L--; 64 | } 65 | if (L > 0 && s[L - 1] == '"') { 66 | s[L - 1] = '\0'; 67 | } 68 | return s; 69 | } 70 | 71 | static Binding *alloc_bind(Config *cfg, unsigned mods, KeySym ks) 72 | { 73 | for (int i = 0; i < cfg->bindsn; i++) { 74 | if (cfg->binds[i].mods == (int)mods && cfg->binds[i].keysym == ks) { 75 | return &cfg->binds[i]; 76 | } 77 | } 78 | if (cfg->bindsn >= 256) { 79 | return NULL; 80 | } 81 | Binding *b = &cfg->binds[cfg->bindsn++]; 82 | b->mods = mods; 83 | b->keysym = ks; 84 | return b; 85 | } 86 | 87 | static unsigned parse_combo(const char *combo, Config *cfg, KeySym *out_ks) 88 | { 89 | unsigned m = 0; 90 | KeySym ks = NoSymbol; 91 | char buf[256]; 92 | strncpy(buf, combo, sizeof buf - 1); 93 | for (char *p = buf; *p; p++) { 94 | if (*p == '+' || isspace((unsigned char)*p)) { 95 | *p = '+'; 96 | } 97 | } 98 | buf[sizeof buf - 1] = '\0'; 99 | for (char *tok = strtok(buf, "+"); tok; tok = strtok(NULL, "+")) { 100 | for (char *q = tok; *q; q++) { 101 | *q = tolower((unsigned char)*q); 102 | } 103 | if (!strcmp(tok, "mod")) { 104 | m |= cfg->modkey; 105 | } 106 | else if (!strcmp(tok, "shift")) { 107 | m |= ShiftMask; 108 | } 109 | else if (!strcmp(tok, "ctrl")) { 110 | m |= ControlMask; 111 | } 112 | else if (!strcmp(tok, "alt")) { 113 | m |= Mod1Mask; 114 | } 115 | else if (!strcmp(tok, "super")) { 116 | m |= Mod4Mask; 117 | } 118 | else { 119 | ks = parse_keysym(tok); 120 | } 121 | } 122 | *out_ks = ks; 123 | return m; 124 | } 125 | 126 | int parser(Config *cfg) 127 | { 128 | char path[PATH_MAX]; 129 | const char *home = getenv("HOME"); 130 | if (!home) { 131 | fputs("sxwmrc: HOME not set\n", stderr); 132 | return -1; 133 | } 134 | 135 | // Determine config file path 136 | const char *xdg_config_home = getenv("XDG_CONFIG_HOME"); 137 | if (xdg_config_home) { 138 | snprintf(path, sizeof path, "%s/sxwmrc", xdg_config_home); 139 | if (access(path, R_OK) == 0) { 140 | goto found; 141 | } 142 | 143 | snprintf(path, sizeof path, "%s/sxwm/sxwmrc", xdg_config_home); 144 | if (access(path, R_OK) == 0) { 145 | goto found; 146 | } 147 | } 148 | 149 | snprintf(path, sizeof path, "%s/.config/sxwmrc", home); 150 | if (access(path, R_OK) == 0) { 151 | goto found; 152 | } 153 | 154 | snprintf(path, sizeof path, "/usr/local/share/sxwmrc"); 155 | if (access(path, R_OK) == 0) { 156 | goto found; 157 | } 158 | 159 | // Nothing found 160 | fprintf(stderr, "sxwmrc: no configuration file found\n"); 161 | return -1; 162 | 163 | found:; 164 | FILE *f = fopen(path, "r"); 165 | if (!f) { 166 | fprintf(stderr, "sxwmrc: cannot open %s\n", path); 167 | return -1; 168 | } 169 | 170 | char line[512]; 171 | int lineno = 0; 172 | int should_floatn = 0; 173 | 174 | // Initialize should_float matrix 175 | for (int j = 0; j < 256; j++) { 176 | cfg->should_float[j] = calloc(256, sizeof(char *)); 177 | if (!cfg->should_float[j]) { 178 | fprintf(stderr, "calloc failed\n"); 179 | fclose(f); 180 | return -1; 181 | } 182 | } 183 | 184 | while (fgets(line, sizeof line, f)) { 185 | lineno++; 186 | char *s = strip(line); 187 | if (!*s || *s == '#') { 188 | continue; 189 | } 190 | 191 | char *sep = strchr(s, ':'); 192 | if (!sep) { 193 | fprintf(stderr, "sxwmrc:%d: missing ':'\n", lineno); 194 | continue; 195 | } 196 | 197 | *sep = '\0'; 198 | char *key = strip(s); 199 | char *rest = strip(sep + 1); 200 | 201 | if (!strcmp(key, "mod_key")) { 202 | unsigned m = parse_mods(rest, cfg); 203 | if (m & (Mod1Mask | Mod4Mask)) { 204 | cfg->modkey = m; 205 | } 206 | else { 207 | fprintf(stderr, "sxwmrc:%d: unknown mod_key '%s'\n", lineno, rest); 208 | } 209 | } 210 | else if (!strcmp(key, "gaps")) { 211 | cfg->gaps = atoi(rest); 212 | } 213 | else if (!strcmp(key, "border_width")) { 214 | cfg->border_width = atoi(rest); 215 | } 216 | else if (!strcmp(key, "focused_border_colour")) { 217 | cfg->border_foc_col = parse_col(rest); 218 | } 219 | else if (!strcmp(key, "unfocused_border_colour")) { 220 | cfg->border_ufoc_col = parse_col(rest); 221 | } 222 | else if (!strcmp(key, "swap_border_colour")) { 223 | cfg->border_swap_col = parse_col(rest); 224 | } 225 | else if (!strcmp(key, "new_win_focus")) { 226 | if (!strcmp(rest, "true")) { 227 | cfg->new_win_focus = True; 228 | } 229 | else { 230 | cfg->new_win_focus = False; 231 | } 232 | } 233 | else if (!strcmp(key, "master_width")) { 234 | float mf = (float)atoi(rest) / 100.0f; 235 | for (int i = 0; i < MAX_MONITORS; i++) { 236 | cfg->master_width[i] = mf; 237 | } 238 | } 239 | else if (!strcmp(key, "motion_throttle")) { 240 | cfg->motion_throttle = atoi(rest); 241 | } 242 | else if (!strcmp(key, "resize_master_amount")) { 243 | cfg->resize_master_amt = atoi(rest); 244 | } 245 | else if (!strcmp(key, "snap_distance")) { 246 | cfg->snap_distance = atoi(rest); 247 | } 248 | else if (!strcmp(key, "should_float")) { 249 | if (should_floatn >= 256) { 250 | fprintf(stderr, "sxwmrc:%d: too many should_float entries\n", lineno); 251 | continue; 252 | } 253 | 254 | char *comment = strchr(rest, '#'); 255 | size_t len = comment ? (size_t)(comment - rest) : strlen(rest); 256 | char win[len + 1]; 257 | strncpy(win, rest, len); 258 | win[len] = '\0'; 259 | 260 | char *final = strip(win); 261 | char *comma_ptr; 262 | char *comma = strtok_r(final, ",", &comma_ptr); 263 | 264 | /* store each comma separated value in a seperate row */ 265 | while (comma && should_floatn < 256) { 266 | comma = strip(comma); 267 | if (*comma == '"') 268 | comma++; 269 | char *end = comma + strlen(comma) - 1; 270 | if (*end == '"') 271 | *end = '\0'; 272 | 273 | /* store each programs name in its own row at index 0 */ 274 | cfg->should_float[should_floatn][0] = strdup(comma); 275 | printf("DEBUG: should_float[%d][0] = '%s'\n", should_floatn, cfg->should_float[should_floatn][0]); 276 | should_floatn++; 277 | comma = strtok_r(NULL, ",", &comma_ptr); 278 | } 279 | } 280 | else if (!strcmp(key, "call") || !strcmp(key, "bind")) { 281 | char *mid = strchr(rest, ':'); 282 | if (!mid) { 283 | fprintf(stderr, "sxwmrc:%d: '%s' missing action\n", lineno, key); 284 | continue; 285 | } 286 | *mid = '\0'; 287 | char *combo = strip(rest); 288 | char *act = strip(mid + 1); 289 | 290 | KeySym ks; 291 | unsigned mods = parse_combo(combo, cfg, &ks); 292 | if (ks == NoSymbol) { 293 | fprintf(stderr, "sxwmrc:%d: bad key in '%s'\n", lineno, combo); 294 | continue; 295 | } 296 | 297 | Binding *b = alloc_bind(cfg, mods, ks); 298 | if (!b) { 299 | fputs("sxwm: too many binds\n", stderr); 300 | break; 301 | } 302 | 303 | if (*act == '"' && !strcmp(key, "bind")) { 304 | b->type = TYPE_CMD; 305 | b->action.cmd = build_argv(strip_quotes(act)); 306 | } 307 | else { 308 | b->type = TYPE_FUNC; 309 | Bool found = False; 310 | for (int i = 0; call_table[i].name; i++) { 311 | if (!strcmp(act, call_table[i].name)) { 312 | b->action.fn = call_table[i].fn; 313 | found = True; 314 | break; 315 | } 316 | } 317 | if (!found) { 318 | fprintf(stderr, "sxwmrc:%d: unknown function '%s'\n", lineno, act); 319 | } 320 | } 321 | } 322 | else if (!strcmp(key, "workspace")) { 323 | char *mid = strchr(rest, ':'); 324 | if (!mid) { 325 | fprintf(stderr, "sxwmrc:%d: workspace missing action\n", lineno); 326 | continue; 327 | } 328 | *mid = '\0'; 329 | char *combo = strip(rest); 330 | char *act = strip(mid + 1); 331 | 332 | KeySym ks; 333 | unsigned mods = parse_combo(combo, cfg, &ks); 334 | if (ks == NoSymbol) { 335 | fprintf(stderr, "sxwmrc:%d: bad key in '%s'\n", lineno, combo); 336 | continue; 337 | } 338 | 339 | Binding *b = alloc_bind(cfg, mods, ks); 340 | if (!b) { 341 | fputs("sxwm: too many binds\n", stderr); 342 | break; 343 | } 344 | 345 | int n; 346 | if (sscanf(act, "move %d", &n) == 1 && n >= 1 && n <= NUM_WORKSPACES) { 347 | b->type = TYPE_CWKSP; 348 | b->action.ws = n - 1; 349 | } 350 | else if (sscanf(act, "swap %d", &n) == 1 && n >= 1 && n <= NUM_WORKSPACES) { 351 | b->type = TYPE_MWKSP; 352 | b->action.ws = n - 1; 353 | } 354 | else { 355 | fprintf(stderr, "sxwmrc:%d: invalid workspace action '%s'\n", lineno, act); 356 | } 357 | } 358 | else { 359 | fprintf(stderr, "sxwmrc:%d: unknown option '%s'\n", lineno, key); 360 | } 361 | } 362 | 363 | fclose(f); 364 | remap_and_dedupe_binds(cfg); 365 | return 0; 366 | } 367 | 368 | int parse_mods(const char *mods, Config *cfg) 369 | { 370 | KeySym dummy; 371 | return parse_combo(mods, cfg, &dummy); 372 | } 373 | 374 | KeySym parse_keysym(const char *key) 375 | { 376 | KeySym ks = XStringToKeysym(key); 377 | if (ks != NoSymbol) { 378 | return ks; 379 | } 380 | 381 | char buf[64]; 382 | size_t n = strlen(key); 383 | if (n >= sizeof buf) { 384 | n = sizeof buf - 1; 385 | } 386 | 387 | buf[0] = toupper((unsigned char)key[0]); 388 | for (size_t i = 1; i < n; i++) { 389 | buf[i] = tolower((unsigned char)key[i]); 390 | } 391 | buf[n] = '\0'; 392 | ks = XStringToKeysym(buf); 393 | if (ks != NoSymbol) { 394 | return ks; 395 | } 396 | 397 | for (size_t i = 0; i < n; i++) { 398 | buf[i] = toupper((unsigned char)key[i]); 399 | } 400 | buf[n] = '\0'; 401 | ks = XStringToKeysym(buf); 402 | if (ks != NoSymbol) { 403 | return ks; 404 | } 405 | 406 | fprintf(stderr, "sxwmrc: unknown keysym '%s'\n", key); 407 | return NoSymbol; 408 | } 409 | 410 | const char **build_argv(const char *cmd) 411 | { 412 | char *dup = strdup(cmd); 413 | char *saveptr = NULL; 414 | const char **argv = malloc(MAX_ARGS * sizeof(*argv)); 415 | int i = 0; 416 | 417 | char *tok = strtok_r(dup, " \t", &saveptr); 418 | while (tok && i < MAX_ARGS - 1) { 419 | if (*tok == '"') { 420 | char *end = tok + strlen(tok) - 1; 421 | if (*end == '"') { 422 | *end = '\0'; 423 | argv[i++] = strdup(tok + 1); 424 | } 425 | else { 426 | char *quoted = strdup(tok + 1); 427 | while ((tok = strtok_r(NULL, " \t", &saveptr)) && *tok != '"') { 428 | quoted = realloc(quoted, strlen(quoted) + strlen(tok) + 2); 429 | strcat(quoted, " "); 430 | strcat(quoted, tok); 431 | } 432 | if (tok && *tok == '"') { 433 | quoted = realloc(quoted, strlen(quoted) + strlen(tok)); 434 | strcat(quoted, tok); 435 | } 436 | argv[i++] = quoted; 437 | } 438 | } 439 | else { 440 | argv[i++] = strdup(tok); 441 | } 442 | tok = strtok_r(NULL, " \t", &saveptr); 443 | } 444 | argv[i] = NULL; 445 | free(dup); 446 | return argv; 447 | } 448 | -------------------------------------------------------------------------------- /src/parser.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include "defs.h" 3 | #define MAX_ARGS 64 4 | 5 | const char **build_argv(const char *cmd); 6 | int parser(Config *user_config); 7 | int parse_mods(const char *mods, Config *user_config); 8 | KeySym parse_keysym(const char *key); -------------------------------------------------------------------------------- /src/sxwm.c: -------------------------------------------------------------------------------- 1 | /* 2 | * See LICENSE for more info 3 | * 4 | * simple xorg window manager 5 | * sxwm is a user-friendly, easily configurable yet powerful 6 | * tiling window manager inspired by window managers such as 7 | * DWM and i3. 8 | * 9 | * The userconfig is designed to be as user-friendly as 10 | * possible, and I hope it is easy to configure even without 11 | * knowledge of C or programming, although most people who 12 | * will use this will probably be programmers :) 13 | * 14 | * (C) Abhinav Prasai 2025 15 | */ 16 | 17 | #include 18 | #include 19 | #include 20 | #include 21 | #include 22 | #include 23 | #include 24 | #include 25 | #include 26 | 27 | #include 28 | #include 29 | #include 30 | #include 31 | #include 32 | 33 | #include 34 | #include 35 | 36 | #include "defs.h" 37 | #include "parser.h" 38 | 39 | Client *add_client(Window w, int ws); 40 | void change_workspace(int ws); 41 | int clean_mask(int mask); 42 | /* void close_focused(void); */ 43 | /* void dec_gaps(void); */ 44 | /* void focus_next(void); */ 45 | /* void focus_prev(void); */ 46 | int get_monitor_for(Client *c); 47 | void grab_keys(void); 48 | void hdl_button(XEvent *xev); 49 | void hdl_button_release(XEvent *xev); 50 | void hdl_client_msg(XEvent *xev); 51 | void hdl_config_ntf(XEvent *xev); 52 | void hdl_config_req(XEvent *xev); 53 | void hdl_dummy(XEvent *xev); 54 | void hdl_destroy_ntf(XEvent *xev); 55 | void hdl_enter(XEvent *xev); 56 | void hdl_keypress(XEvent *xev); 57 | void hdl_map_req(XEvent *xev); 58 | void hdl_motion(XEvent *xev); 59 | void hdl_root_property(XEvent *xev); 60 | void hdl_unmap_ntf(XEvent *xev); 61 | /* void inc_gaps(void); */ 62 | void init_defaults(void); 63 | /* void move_master_next(void); */ 64 | /* void move_master_prev(void); */ 65 | void move_to_workspace(int ws); 66 | void other_wm(void); 67 | int other_wm_err(Display *dpy, XErrorEvent *ee); 68 | /* long parse_col(const char *hex); */ 69 | /* void quit(void); */ 70 | /* void reload_config(void); */ 71 | /* void resize_master_add(void); */ 72 | /* void resize_master_sub(void); */ 73 | void run(void); 74 | void scan_existing_windows(void); 75 | void send_wm_take_focus(Window w); 76 | void setup(void); 77 | void setup_atoms(void); 78 | Bool window_should_float(Window w); 79 | void spawn(const char **argv); 80 | void swap_clients(Client *a, Client *b); 81 | void tile(void); 82 | /* void toggle_floating(void); */ 83 | /* void toggle_floating_global(void); */ 84 | /* void toggle_fullscreen(void); */ 85 | void update_borders(void); 86 | void update_monitors(void); 87 | void update_net_client_list(void); 88 | void update_struts(void); 89 | int xerr(Display *dpy, XErrorEvent *ee); 90 | void xev_case(XEvent *xev); 91 | #include "config.h" 92 | 93 | Atom atom_net_active_window; 94 | Atom atom_net_current_desktop; 95 | Atom atom_net_supported; 96 | Atom atom_net_wm_state; 97 | Atom atom_net_wm_state_fullscreen; 98 | Atom atom_wm_window_type; 99 | Atom atom_net_wm_window_type_dock; 100 | Atom atom_net_workarea; 101 | Atom atom_wm_delete; 102 | Atom atom_wm_strut; 103 | Atom atom_wm_strut_partial; 104 | Atom atom_net_supporting_wm_check; 105 | Atom atom_net_wm_name; 106 | Atom atom_utf8_string; 107 | 108 | Cursor c_normal, c_move, c_resize; 109 | Client *workspaces[NUM_WORKSPACES] = {NULL}; 110 | Config default_config; 111 | Config user_config; 112 | int current_ws = 0; 113 | DragMode drag_mode = DRAG_NONE; 114 | Client *drag_client = NULL; 115 | Client *swap_target = NULL; 116 | Client *focused = NULL; 117 | EventHandler evtable[LASTEvent]; 118 | Display *dpy; 119 | Window root; 120 | Window wm_check_win; 121 | Monitor *mons = NULL; 122 | int monsn = 0; 123 | Bool global_floating = False; 124 | Bool in_ws_switch = False; 125 | 126 | long last_motion_time = 0; 127 | int scr_width; 128 | int scr_height; 129 | int open_windows = 0; 130 | int drag_start_x, drag_start_y; 131 | int drag_orig_x, drag_orig_y, drag_orig_w, drag_orig_h; 132 | 133 | int reserve_left = 0; 134 | int reserve_right = 0; 135 | int reserve_top = 0; 136 | int reserve_bottom = 0; 137 | 138 | Bool next_should_float = False; 139 | 140 | Client *add_client(Window w, int ws) 141 | { 142 | Client *c = malloc(sizeof(Client)); 143 | if (!c) { 144 | fprintf(stderr, "sxwm: could not alloc memory for client\n"); 145 | return NULL; 146 | } 147 | 148 | c->win = w; 149 | c->next = NULL; 150 | c->ws = ws; 151 | 152 | if (!workspaces[ws]) { 153 | workspaces[ws] = c; 154 | } 155 | else { 156 | Client *tail = workspaces[ws]; 157 | while (tail->next) 158 | tail = tail->next; 159 | tail->next = c; 160 | } 161 | 162 | open_windows++; 163 | XSelectInput(dpy, w, 164 | EnterWindowMask | LeaveWindowMask | FocusChangeMask | PropertyChangeMask | StructureNotifyMask); 165 | 166 | Atom protos[] = {atom_wm_delete}; 167 | XSetWMProtocols(dpy, w, protos, 1); 168 | 169 | XWindowAttributes wa; 170 | XGetWindowAttributes(dpy, w, &wa); 171 | c->x = wa.x; 172 | c->y = wa.y; 173 | c->w = wa.width; 174 | c->h = wa.height; 175 | 176 | /* set monitor based on pointer location */ 177 | Window root_ret, child_ret; 178 | int root_x, root_y, win_x, win_y; 179 | unsigned int mask; 180 | int pointer_mon = 0; 181 | 182 | if (XQueryPointer(dpy, root, &root_ret, &child_ret, &root_x, &root_y, &win_x, &win_y, &mask)) { 183 | for (int i = 0; i < monsn; i++) { 184 | if (root_x >= mons[i].x && root_x < mons[i].x + mons[i].w && root_y >= mons[i].y && 185 | root_y < mons[i].y + mons[i].h) { 186 | pointer_mon = i; 187 | break; 188 | } 189 | } 190 | } 191 | 192 | c->mon = pointer_mon; 193 | c->fixed = False; 194 | c->floating = False; 195 | c->fullscreen = False; 196 | c->mapped = True; 197 | 198 | if (global_floating) { 199 | c->floating = True; 200 | } 201 | 202 | if (ws == current_ws && !focused) { 203 | focused = c; 204 | } 205 | 206 | XRaiseWindow(dpy, w); 207 | return c; 208 | } 209 | 210 | void change_workspace(int ws) 211 | { 212 | if (ws >= NUM_WORKSPACES || ws == current_ws) 213 | return; 214 | 215 | in_ws_switch = True; 216 | XGrabServer(dpy); 217 | 218 | /* unmap those still marked mapped */ 219 | for (Client *c = workspaces[current_ws]; c; c = c->next) { 220 | if (c->mapped) 221 | XUnmapWindow(dpy, c->win); 222 | } 223 | 224 | current_ws = ws; 225 | 226 | /* map those still marked mapped */ 227 | for (Client *c = workspaces[current_ws]; c; c = c->next) { 228 | if (c->mapped) 229 | XMapWindow(dpy, c->win); 230 | } 231 | 232 | tile(); 233 | if (workspaces[current_ws]) { 234 | focused = workspaces[current_ws]; 235 | XSetInputFocus(dpy, focused->win, RevertToPointerRoot, CurrentTime); 236 | } 237 | 238 | long cd = current_ws; 239 | XChangeProperty(dpy, root, XInternAtom(dpy, "_NET_CURRENT_DESKTOP", False), XA_CARDINAL, 32, PropModeReplace, 240 | (unsigned char *)&cd, 1); 241 | 242 | XUngrabServer(dpy); 243 | XSync(dpy, False); 244 | in_ws_switch = False; 245 | } 246 | 247 | int clean_mask(int mask) 248 | { 249 | return mask & ~(LockMask | Mod2Mask | Mod3Mask); 250 | } 251 | 252 | void close_focused(void) 253 | { 254 | if (!focused) { 255 | return; 256 | } 257 | 258 | Atom *protos; 259 | int n; 260 | if (XGetWMProtocols(dpy, focused->win, &protos, &n) && protos) { 261 | for (int i = 0; i < n; i++) 262 | if (protos[i] == atom_wm_delete) { 263 | XEvent ev = {.xclient = {.type = ClientMessage, 264 | .window = focused->win, 265 | .message_type = XInternAtom(dpy, "WM_PROTOCOLS", False), 266 | .format = 32}}; 267 | ev.xclient.data.l[0] = atom_wm_delete; 268 | ev.xclient.data.l[1] = CurrentTime; 269 | XSendEvent(dpy, focused->win, False, NoEventMask, &ev); 270 | XFree(protos); 271 | return; 272 | } 273 | XUnmapWindow(dpy, focused->win); 274 | XFree(protos); 275 | } 276 | XUnmapWindow(dpy, focused->win); 277 | XKillClient(dpy, focused->win); 278 | } 279 | 280 | void dec_gaps(void) 281 | { 282 | if (user_config.gaps > 0) { 283 | user_config.gaps--; 284 | tile(); 285 | update_borders(); 286 | } 287 | } 288 | 289 | void focus_next(void) 290 | { 291 | if (!focused || !workspaces[current_ws]) { 292 | return; 293 | } 294 | 295 | focused = (focused->next ? focused->next : workspaces[current_ws]); 296 | XSetInputFocus(dpy, focused->win, RevertToPointerRoot, CurrentTime); 297 | XRaiseWindow(dpy, focused->win); 298 | update_borders(); 299 | } 300 | 301 | void focus_prev(void) 302 | { 303 | if (!focused || !workspaces[current_ws]) { 304 | return; 305 | } 306 | 307 | Client *p = workspaces[current_ws], *prev = NULL; 308 | while (p && p != focused) { 309 | prev = p; 310 | p = p->next; 311 | } 312 | 313 | if (!prev) { 314 | while (p->next) 315 | p = p->next; 316 | focused = p; 317 | } 318 | else { 319 | focused = prev; 320 | } 321 | 322 | XSetInputFocus(dpy, focused->win, RevertToPointerRoot, CurrentTime); 323 | XRaiseWindow(dpy, focused->win); 324 | update_borders(); 325 | } 326 | 327 | int get_monitor_for(Client *c) 328 | { 329 | int cx = c->x + c->w / 2, cy = c->y + c->h / 2; 330 | for (int i = 0; i < monsn; i++) { 331 | if (cx >= (int)mons[i].x && cx < mons[i].x + mons[i].w && cy >= (int)mons[i].y && cy < mons[i].y + mons[i].h) 332 | return i; 333 | } 334 | return 0; 335 | } 336 | 337 | void grab_keys(void) 338 | { 339 | const int guards[] = {0, 340 | LockMask, 341 | Mod2Mask, 342 | LockMask | Mod2Mask, 343 | Mod5Mask, 344 | LockMask | Mod5Mask, 345 | Mod2Mask | Mod5Mask, 346 | LockMask | Mod2Mask | Mod5Mask}; 347 | XUngrabKey(dpy, AnyKey, AnyModifier, root); 348 | 349 | for (int i = 0; i < user_config.bindsn; i++) { 350 | Binding *b = &user_config.binds[i]; 351 | 352 | if ((b->type == TYPE_CWKSP && b->mods != user_config.modkey) || 353 | (b->type == TYPE_MWKSP && b->mods != (user_config.modkey | ShiftMask))) 354 | continue; 355 | 356 | KeyCode kc = XKeysymToKeycode(dpy, b->keysym); 357 | if (!kc) 358 | continue; 359 | 360 | for (size_t g = 0; g < sizeof guards / sizeof *guards; g++) 361 | XGrabKey(dpy, kc, b->mods | guards[g], root, True, GrabModeAsync, GrabModeAsync); 362 | } 363 | } 364 | 365 | void hdl_button(XEvent *xev) 366 | { 367 | XButtonEvent *e = &xev->xbutton; 368 | Window w = e->subwindow; 369 | if (!w) { 370 | return; 371 | } 372 | 373 | Client *head = workspaces[current_ws]; 374 | for (Client *c = head; c; c = c->next) { 375 | if (c->win != w) { 376 | continue; 377 | } 378 | 379 | /* begin swap drag mode */ 380 | if ((e->state & user_config.modkey) && (e->state & ShiftMask) && e->button == Button1 && !c->floating) { 381 | drag_client = c; 382 | drag_start_x = e->x_root; 383 | drag_start_y = e->y_root; 384 | drag_orig_x = c->x; 385 | drag_orig_y = c->y; 386 | drag_orig_w = c->w; 387 | drag_orig_h = c->h; 388 | drag_mode = DRAG_SWAP; 389 | XGrabPointer(dpy, root, True, ButtonReleaseMask | PointerMotionMask, GrabModeAsync, GrabModeAsync, None, 390 | c_move, CurrentTime); 391 | focused = c; 392 | XSetInputFocus(dpy, c->win, RevertToPointerRoot, CurrentTime); 393 | XSetWindowBorder(dpy, c->win, user_config.border_swap_col); 394 | XRaiseWindow(dpy, c->win); 395 | return; 396 | } 397 | 398 | if ((e->state & user_config.modkey) && (e->button == Button1 || e->button == Button3) && !c->floating) { 399 | focused = c; 400 | toggle_floating(); 401 | } 402 | 403 | if (!c->floating) { 404 | return; 405 | } 406 | 407 | if (c->fixed && e->button == Button3) { 408 | return; 409 | } 410 | 411 | Cursor cur = (e->button == Button1) ? c_move : c_resize; 412 | XGrabPointer(dpy, root, True, ButtonReleaseMask | PointerMotionMask, GrabModeAsync, GrabModeAsync, None, cur, 413 | CurrentTime); 414 | 415 | drag_client = c; 416 | drag_start_x = e->x_root; 417 | drag_start_y = e->y_root; 418 | drag_orig_x = c->x; 419 | drag_orig_y = c->y; 420 | drag_orig_w = c->w; 421 | drag_orig_h = c->h; 422 | drag_mode = (e->button == Button1) ? DRAG_MOVE : DRAG_RESIZE; 423 | focused = c; 424 | 425 | XSetInputFocus(dpy, c->win, RevertToPointerRoot, CurrentTime); 426 | update_borders(); 427 | XRaiseWindow(dpy, c->win); 428 | return; 429 | } 430 | } 431 | 432 | void hdl_button_release(XEvent *xev) 433 | { 434 | (void)xev; 435 | 436 | if (drag_mode == DRAG_SWAP) { 437 | if (swap_target) { 438 | XSetWindowBorder(dpy, swap_target->win, 439 | (swap_target == focused ? user_config.border_foc_col : user_config.border_ufoc_col)); 440 | swap_clients(drag_client, swap_target); 441 | } 442 | tile(); 443 | update_borders(); 444 | } 445 | 446 | XUngrabPointer(dpy, CurrentTime); 447 | 448 | drag_mode = DRAG_NONE; 449 | drag_client = NULL; 450 | swap_target = NULL; 451 | } 452 | 453 | void hdl_client_msg(XEvent *xev) 454 | { 455 | /* clickable bar workspace switching */ 456 | if (xev->xclient.message_type == atom_net_current_desktop) { 457 | int ws = (int)xev->xclient.data.l[0]; 458 | change_workspace(ws); 459 | return; 460 | } 461 | if (xev->xclient.message_type == atom_net_wm_state) { 462 | long action = xev->xclient.data.l[0]; 463 | Atom target = xev->xclient.data.l[1]; 464 | if (target == atom_net_wm_state_fullscreen) { 465 | if (action == 1 || action == 2) { 466 | toggle_fullscreen(); 467 | } 468 | else if (action == 0 && focused && focused->fullscreen) { 469 | toggle_fullscreen(); 470 | } 471 | } 472 | return; 473 | } 474 | 475 | if (xev->xclient.message_type == XInternAtom(dpy, "_NET_ACTIVE_WINDOW", False)) { 476 | Window target = xev->xclient.data.l[0]; 477 | for (int ws = 0; ws < NUM_WORKSPACES; ws++) { 478 | for (Client *c = workspaces[ws]; c; c = c->next) { 479 | if (c->win == target) { 480 | /* do NOT move to current ws */ 481 | if (ws == current_ws) { 482 | focused = c; 483 | send_wm_take_focus(c->win); 484 | XSetInputFocus(dpy, c->win, RevertToPointerRoot, CurrentTime); 485 | XRaiseWindow(dpy, c->win); 486 | update_borders(); 487 | } 488 | return; 489 | } 490 | } 491 | } 492 | } 493 | } 494 | 495 | void hdl_config_ntf(XEvent *xev) 496 | { 497 | if (xev->xconfigure.window == root) { 498 | update_monitors(); 499 | tile(); 500 | update_borders(); 501 | } 502 | } 503 | 504 | void hdl_config_req(XEvent *xev) 505 | { 506 | XConfigureRequestEvent *e = &xev->xconfigurerequest; 507 | Client *c = NULL; 508 | 509 | for (int ws = 0; ws < NUM_WORKSPACES && !c; ws++) 510 | for (c = workspaces[ws]; c; c = c->next) 511 | if (c->win == e->window) { 512 | break; 513 | } 514 | 515 | if (!c || c->floating || c->fullscreen) { 516 | /* allow client to configure itself */ 517 | XWindowChanges wc = {.x = e->x, 518 | .y = e->y, 519 | .width = e->width, 520 | .height = e->height, 521 | .border_width = e->border_width, 522 | .sibling = e->above, 523 | .stack_mode = e->detail}; 524 | XConfigureWindow(dpy, e->window, e->value_mask, &wc); 525 | return; 526 | } 527 | 528 | if (c->fixed) { 529 | return; 530 | } 531 | } 532 | 533 | void hdl_dummy(XEvent *xev) 534 | { 535 | (void)xev; 536 | } 537 | 538 | void hdl_destroy_ntf(XEvent *xev) 539 | { 540 | Window w = xev->xdestroywindow.window; 541 | 542 | for (int ws = 0; ws < NUM_WORKSPACES; ws++) { 543 | Client *prev = NULL, *c = workspaces[ws]; 544 | while (c && c->win != w) { 545 | prev = c; 546 | c = c->next; 547 | } 548 | if (c) { 549 | if (focused == c) { 550 | if (c->next) { 551 | focused = c->next; 552 | } 553 | else if (prev) { 554 | focused = prev; 555 | } 556 | else { 557 | if (ws == current_ws) { 558 | focused = NULL; 559 | } 560 | } 561 | } 562 | 563 | if (!prev) { 564 | workspaces[ws] = c->next; 565 | } 566 | else { 567 | prev->next = c->next; 568 | } 569 | 570 | free(c); 571 | update_net_client_list(); 572 | open_windows--; 573 | 574 | if (ws == current_ws) { 575 | tile(); 576 | update_borders(); 577 | 578 | if (focused) { 579 | XSetInputFocus(dpy, focused->win, RevertToPointerRoot, CurrentTime); 580 | XRaiseWindow(dpy, focused->win); 581 | } 582 | } 583 | return; 584 | } 585 | } 586 | } 587 | 588 | void hdl_enter(XEvent *xev) 589 | { 590 | Window w = xev->xcrossing.window; 591 | 592 | Client *head = workspaces[current_ws]; 593 | for (Client *c = head; c; c = c->next) { 594 | if (c->win == w) { 595 | focused = c; 596 | XSetInputFocus(dpy, w, RevertToPointerRoot, CurrentTime); 597 | update_borders(); 598 | break; 599 | } 600 | } 601 | } 602 | 603 | void hdl_keypress(XEvent *xev) 604 | { 605 | KeySym ks = XkbKeycodeToKeysym(dpy, xev->xkey.keycode, 0, 0); 606 | int mods = clean_mask(xev->xkey.state); 607 | 608 | for (int i = 0; i < user_config.bindsn; i++) { 609 | Binding *b = &user_config.binds[i]; 610 | if (b->keysym == ks && clean_mask(b->mods) == mods) { 611 | switch (b->type) { 612 | case TYPE_CMD: 613 | spawn(b->action.cmd); 614 | break; 615 | 616 | case TYPE_FUNC: 617 | if (b->action.fn) 618 | b->action.fn(); 619 | break; 620 | case TYPE_CWKSP: 621 | change_workspace(b->action.ws); 622 | update_net_client_list(); 623 | break; 624 | case TYPE_MWKSP: 625 | move_to_workspace(b->action.ws); 626 | update_net_client_list(); 627 | break; 628 | } 629 | return; 630 | } 631 | } 632 | } 633 | 634 | void swap_clients(Client *a, Client *b) 635 | { 636 | if (!a || !b || a == b) { 637 | return; 638 | } 639 | 640 | Client **head = &workspaces[current_ws]; 641 | Client **pa = head, **pb = head; 642 | 643 | while (*pa && *pa != a) 644 | pa = &(*pa)->next; 645 | while (*pb && *pb != b) 646 | pb = &(*pb)->next; 647 | 648 | if (!*pa || !*pb) { 649 | return; 650 | } 651 | 652 | /* if next to it swap */ 653 | if (*pa == b && *pb == a) { 654 | Client *tmp = b->next; 655 | b->next = a; 656 | a->next = tmp; 657 | *pa = b; 658 | return; 659 | } 660 | 661 | /* full swap */ 662 | Client *ta = *pa; 663 | Client *tb = *pb; 664 | Client *ta_next = ta->next; 665 | Client *tb_next = tb->next; 666 | 667 | *pa = tb; 668 | tb->next = ta_next == tb ? ta : ta_next; 669 | 670 | *pb = ta; 671 | ta->next = tb_next == ta ? tb : tb_next; 672 | } 673 | 674 | void hdl_map_req(XEvent *xev) 675 | { 676 | Window w = xev->xmaprequest.window; 677 | XWindowAttributes wa; 678 | 679 | if (!XGetWindowAttributes(dpy, w, &wa)) { 680 | return; 681 | } 682 | 683 | if (wa.override_redirect || wa.width <= 0 || wa.height <= 0) { 684 | XMapWindow(dpy, w); 685 | return; 686 | } 687 | 688 | Atom type; 689 | int format; 690 | unsigned long nitems, after; 691 | Atom *types = NULL; 692 | Bool should_float = False; 693 | 694 | if (XGetWindowProperty(dpy, w, atom_wm_window_type, 0, 8, False, XA_ATOM, &type, &format, &nitems, &after, 695 | (unsigned char **)&types) == Success && 696 | types) { 697 | Atom dock = XInternAtom(dpy, "_NET_WM_WINDOW_TYPE_DOCK", False); 698 | Atom util = XInternAtom(dpy, "_NET_WM_WINDOW_TYPE_UTILITY", False); 699 | Atom dialog = XInternAtom(dpy, "_NET_WM_WINDOW_TYPE_DIALOG", False); 700 | Atom toolbar = XInternAtom(dpy, "_NET_WM_WINDOW_TYPE_TOOLBAR", False); 701 | Atom splash = XInternAtom(dpy, "_NET_WM_WINDOW_TYPE_SPLASH", False); 702 | Atom popup = XInternAtom(dpy, "_NET_WM_WINDOW_TYPE_POPUP_MENU", False); 703 | 704 | for (unsigned long i = 0; i < nitems; i++) { 705 | if (types[i] == dock) { 706 | XFree(types); 707 | XMapWindow(dpy, w); 708 | return; 709 | } 710 | if (types[i] == util || types[i] == dialog || types[i] == toolbar || types[i] == splash || 711 | types[i] == popup) { 712 | should_float = True; 713 | break; 714 | } 715 | } 716 | XFree(types); 717 | } 718 | 719 | if (!should_float) { 720 | should_float = window_should_float(w); 721 | } 722 | 723 | if (open_windows == MAXCLIENTS) { 724 | fprintf(stderr, "sxwm: max clients reached, ignoring map request\n"); 725 | return; 726 | } 727 | 728 | Client *c = add_client(w, current_ws); 729 | if (!c) 730 | return; 731 | 732 | Window tr; 733 | if (!should_float && XGetTransientForHint(dpy, w, &tr)) 734 | should_float = True; 735 | XSizeHints sh; 736 | long sup; 737 | if (!should_float && XGetWMNormalHints(dpy, w, &sh, &sup) && (sh.flags & PMinSize) && (sh.flags & PMaxSize) && 738 | sh.min_width == sh.max_width && sh.min_height == sh.max_height) { 739 | should_float = True; 740 | c->fixed = True; 741 | } 742 | 743 | if (should_float || global_floating) { 744 | c->floating = True; 745 | } 746 | 747 | /* center floating windows & set border */ 748 | if (c->floating && !c->fullscreen) { 749 | int w_ = MAX(c->w, 64), h_ = MAX(c->h, 64); 750 | int mx = mons[c->mon].x, my = mons[c->mon].y; 751 | int mw = mons[c->mon].w, mh = mons[c->mon].h; 752 | int x = mx + (mw - w_) / 2, y = my + (mh - h_) / 2; 753 | c->x = x; 754 | c->y = y; 755 | c->w = w_; 756 | c->h = h_; 757 | XMoveResizeWindow(dpy, w, x, y, w_, h_); 758 | XSetWindowBorderWidth(dpy, w, user_config.border_width); 759 | } 760 | 761 | /* map & borders */ 762 | update_net_client_list(); 763 | if (!global_floating && !c->floating) 764 | tile(); 765 | else if (c->floating) 766 | XRaiseWindow(dpy, w); 767 | 768 | if (user_config.new_win_focus) { 769 | focused = c; 770 | XSetInputFocus(dpy, c->win, RevertToPointerRoot, CurrentTime); 771 | send_wm_take_focus(c->win); 772 | } 773 | 774 | XMapWindow(dpy, w); 775 | for (Client *c = workspaces[current_ws]; c; c = c->next) 776 | if (c->win == w) 777 | c->mapped = True; 778 | 779 | update_borders(); 780 | } 781 | 782 | void hdl_motion(XEvent *xev) 783 | { 784 | XMotionEvent *e = &xev->xmotion; 785 | 786 | if ((drag_mode == DRAG_NONE || !drag_client) || 787 | (e->time - last_motion_time <= (1000 / (long unsigned int)user_config.motion_throttle))) { 788 | return; 789 | } 790 | last_motion_time = e->time; 791 | 792 | if (drag_mode == DRAG_SWAP) { 793 | Window root_ret, child; 794 | int rx, ry, wx, wy; 795 | unsigned int mask; 796 | XQueryPointer(dpy, root, &root_ret, &child, &rx, &ry, &wx, &wy, &mask); 797 | 798 | Client *last_swap_target = NULL; 799 | Client *new_target = NULL; 800 | 801 | for (Client *c = workspaces[current_ws]; c; c = c->next) { 802 | if (c == drag_client || c->floating) { 803 | continue; 804 | } 805 | if (c->win == child) { 806 | new_target = c; 807 | break; 808 | } 809 | } 810 | 811 | if (new_target != last_swap_target) { 812 | if (last_swap_target) { 813 | XSetWindowBorder( 814 | dpy, last_swap_target->win, 815 | (last_swap_target == focused ? user_config.border_foc_col : user_config.border_ufoc_col)); 816 | } 817 | if (new_target) { 818 | XSetWindowBorder(dpy, new_target->win, user_config.border_swap_col); 819 | } 820 | last_swap_target = new_target; 821 | } 822 | 823 | swap_target = new_target; 824 | return; 825 | } 826 | 827 | else if (drag_mode == DRAG_MOVE) { 828 | int dx = e->x_root - drag_start_x; 829 | int dy = e->y_root - drag_start_y; 830 | int nx = drag_orig_x + dx; 831 | int ny = drag_orig_y + dy; 832 | 833 | int outer_w = drag_client->w + 2 * user_config.border_width; 834 | int outer_h = drag_client->h + 2 * user_config.border_width; 835 | 836 | if (UDIST(nx, 0) <= user_config.snap_distance) { 837 | nx = 0; 838 | } 839 | else if (UDIST(nx + outer_w, scr_width) <= user_config.snap_distance) { 840 | nx = scr_width - outer_w; 841 | } 842 | 843 | if (UDIST(ny, 0) <= user_config.snap_distance) { 844 | ny = 0; 845 | } 846 | else if (UDIST(ny + outer_h, scr_height) <= user_config.snap_distance) { 847 | ny = scr_height - outer_h; 848 | } 849 | 850 | if (!drag_client->floating && (UDIST(nx, drag_client->x) > user_config.snap_distance || 851 | UDIST(ny, drag_client->y) > user_config.snap_distance)) { 852 | toggle_floating(); 853 | } 854 | 855 | XMoveWindow(dpy, drag_client->win, nx, ny); 856 | drag_client->x = nx; 857 | drag_client->y = ny; 858 | } 859 | 860 | else if (drag_mode == DRAG_RESIZE) { 861 | int dx = e->x_root - drag_start_x; 862 | int dy = e->y_root - drag_start_y; 863 | int nw = drag_orig_w + dx; 864 | int nh = drag_orig_h + dy; 865 | drag_client->w = nw < 20 ? 20 : nw; 866 | drag_client->h = nh < 20 ? 20 : nh; 867 | XResizeWindow(dpy, drag_client->win, drag_client->w, drag_client->h); 868 | } 869 | } 870 | 871 | void hdl_root_property(XEvent *xev) 872 | { 873 | XPropertyEvent *e = &xev->xproperty; 874 | if (e->atom == atom_net_current_desktop) { 875 | long *val = NULL; 876 | Atom actual; 877 | int fmt; 878 | unsigned long n, after; 879 | if (XGetWindowProperty(dpy, root, atom_net_current_desktop, 0, 1, False, XA_CARDINAL, &actual, &fmt, &n, &after, 880 | (unsigned char **)&val) == Success && 881 | val) { 882 | change_workspace((int)val[0]); 883 | XFree(val); 884 | } 885 | } 886 | else if (e->atom == atom_wm_strut_partial) { 887 | update_struts(); 888 | tile(); 889 | update_borders(); 890 | } 891 | } 892 | 893 | void hdl_unmap_ntf(XEvent *xev) 894 | { 895 | if (!in_ws_switch) { 896 | Window w = xev->xunmap.window; 897 | for (Client *c = workspaces[current_ws]; c; c = c->next) { 898 | if (c->win == w) { 899 | c->mapped = False; 900 | break; 901 | } 902 | } 903 | } 904 | 905 | update_net_client_list(); 906 | tile(); 907 | update_borders(); 908 | } 909 | 910 | void update_struts(void) 911 | { 912 | reserve_left = reserve_right = reserve_top = reserve_bottom = 0; 913 | 914 | Window root_ret, parent_ret, *children; 915 | unsigned int nchildren; 916 | if (!XQueryTree(dpy, root, &root_ret, &parent_ret, &children, &nchildren)) 917 | return; 918 | 919 | for (unsigned int i = 0; i < nchildren; i++) { 920 | Window w = children[i]; 921 | 922 | Atom actual_type; 923 | int actual_format; 924 | unsigned long nitems, bytes_after; 925 | Atom *types = NULL; 926 | 927 | if (XGetWindowProperty(dpy, w, atom_wm_window_type, 0, 4, False, XA_ATOM, &actual_type, &actual_format, &nitems, 928 | &bytes_after, (unsigned char **)&types) != Success || 929 | !types) 930 | continue; 931 | 932 | Bool is_dock = False; 933 | for (unsigned long j = 0; j < nitems; j++) { 934 | if (types[j] == atom_net_wm_window_type_dock) { 935 | is_dock = True; 936 | break; 937 | } 938 | } 939 | XFree(types); 940 | if (!is_dock) 941 | continue; 942 | 943 | long *str = NULL; 944 | Atom actual; 945 | int sfmt; 946 | unsigned long len, rem; 947 | if (XGetWindowProperty(dpy, w, atom_wm_strut_partial, 0, 12, False, XA_CARDINAL, &actual, &sfmt, &len, &rem, 948 | (unsigned char **)&str) == Success && 949 | str && len >= 4) { 950 | reserve_left = MAX(reserve_left, str[0]); 951 | reserve_right = MAX(reserve_right, str[1]); 952 | reserve_top = MAX(reserve_top, str[2]); 953 | reserve_bottom = MAX(reserve_bottom, str[3]); 954 | XFree(str); 955 | } 956 | else if (XGetWindowProperty(dpy, w, atom_wm_strut, 0, 4, False, XA_CARDINAL, &actual, &sfmt, &len, &rem, 957 | (unsigned char **)&str) == Success && 958 | str && len == 4) { 959 | reserve_left = MAX(reserve_left, str[0]); 960 | reserve_right = MAX(reserve_right, str[1]); 961 | reserve_top = MAX(reserve_top, str[2]); 962 | reserve_bottom = MAX(reserve_bottom, str[3]); 963 | XFree(str); 964 | } 965 | } 966 | XFree(children); 967 | } 968 | 969 | void inc_gaps(void) 970 | { 971 | user_config.gaps++; 972 | tile(); 973 | update_borders(); 974 | } 975 | 976 | void init_defaults(void) 977 | { 978 | default_config.modkey = Mod4Mask; 979 | default_config.gaps = 10; 980 | default_config.border_width = 1; 981 | default_config.border_foc_col = parse_col("#c0cbff"); 982 | default_config.border_ufoc_col = parse_col("#555555"); 983 | default_config.border_swap_col = parse_col("#fff4c0"); 984 | for (int i = 0; i < MAX_MONITORS; i++) 985 | default_config.master_width[i] = 50 / 100.0f; 986 | 987 | default_config.motion_throttle = 60; 988 | default_config.resize_master_amt = 5; 989 | default_config.snap_distance = 5; 990 | default_config.bindsn = 0; 991 | default_config.new_win_focus = True; 992 | 993 | for (unsigned long i = 0; i < LENGTH(binds); i++) { 994 | default_config.binds[i].mods = binds[i].mods; 995 | default_config.binds[i].keysym = binds[i].keysym; 996 | default_config.binds[i].action.cmd = binds[i].action.cmd; 997 | default_config.binds[i].type = binds[i].type; 998 | default_config.bindsn++; 999 | } 1000 | 1001 | user_config = default_config; 1002 | } 1003 | 1004 | void move_master_next(void) 1005 | { 1006 | if (!workspaces[current_ws] || !workspaces[current_ws]->next) { 1007 | return; 1008 | } 1009 | Client *first = workspaces[current_ws]; 1010 | workspaces[current_ws] = first->next; 1011 | first->next = NULL; 1012 | 1013 | Client *tail = workspaces[current_ws]; 1014 | while (tail->next) { 1015 | tail = tail->next; 1016 | } 1017 | tail->next = first; 1018 | 1019 | tile(); 1020 | update_borders(); 1021 | } 1022 | 1023 | void move_master_prev(void) 1024 | { 1025 | if (!workspaces[current_ws] || !workspaces[current_ws]->next) { 1026 | return; 1027 | } 1028 | Client *prev = NULL, *cur = workspaces[current_ws]; 1029 | while (cur->next) { 1030 | prev = cur; 1031 | cur = cur->next; 1032 | } 1033 | prev->next = NULL; 1034 | cur->next = workspaces[current_ws]; 1035 | workspaces[current_ws] = cur; 1036 | tile(); 1037 | update_borders(); 1038 | } 1039 | 1040 | void move_to_workspace(int ws) 1041 | { 1042 | if (!focused || ws >= NUM_WORKSPACES || ws == current_ws) { 1043 | return; 1044 | } 1045 | 1046 | if (focused->fullscreen) { 1047 | focused->fullscreen = False; 1048 | XMoveResizeWindow(dpy, focused->win, focused->orig_x, focused->orig_y, focused->orig_w, focused->orig_h); 1049 | XSetWindowBorderWidth(dpy, focused->win, user_config.border_width); 1050 | } 1051 | 1052 | XUnmapWindow(dpy, focused->win); 1053 | /* remove from current list */ 1054 | Client **pp = &workspaces[current_ws]; 1055 | while (*pp && *pp != focused) 1056 | pp = &(*pp)->next; 1057 | if (*pp) { 1058 | *pp = focused->next; 1059 | } 1060 | 1061 | /* push to target list */ 1062 | focused->next = workspaces[ws]; 1063 | workspaces[ws] = focused; 1064 | 1065 | /* tile current ws */ 1066 | tile(); 1067 | focused = workspaces[current_ws]; 1068 | if (focused) { 1069 | XSetInputFocus(dpy, focused->win, RevertToPointerRoot, CurrentTime); 1070 | } 1071 | } 1072 | 1073 | void other_wm(void) 1074 | { 1075 | XSetErrorHandler(other_wm_err); 1076 | XChangeWindowAttributes(dpy, root, CWEventMask, &(XSetWindowAttributes){.event_mask = SubstructureRedirectMask}); 1077 | XSync(dpy, False); 1078 | XSetErrorHandler(xerr); 1079 | XChangeWindowAttributes(dpy, root, CWEventMask, &(XSetWindowAttributes){.event_mask = 0}); 1080 | XSync(dpy, False); 1081 | } 1082 | 1083 | int other_wm_err(Display *dpy, XErrorEvent *ee) 1084 | { 1085 | errx(0, "can't start because another window manager is already running"); 1086 | return 0; 1087 | (void)dpy; 1088 | (void)ee; 1089 | } 1090 | 1091 | long parse_col(const char *hex) 1092 | { 1093 | XColor col; 1094 | Colormap cmap = DefaultColormap(dpy, DefaultScreen(dpy)); 1095 | 1096 | if (!XParseColor(dpy, cmap, hex, &col)) { 1097 | fprintf(stderr, "sxwm: cannot parse color %s\n", hex); 1098 | return WhitePixel(dpy, DefaultScreen(dpy)); 1099 | } 1100 | 1101 | if (!XAllocColor(dpy, cmap, &col)) { 1102 | fprintf(stderr, "sxwm: cannot allocate color %s\n", hex); 1103 | return WhitePixel(dpy, DefaultScreen(dpy)); 1104 | } 1105 | 1106 | /* return col.pixel |= 0xff << 24; */ 1107 | /* This is a fix for picom making the borders transparent. DANGEROUS */ 1108 | return col.pixel; 1109 | } 1110 | 1111 | void quit(void) 1112 | { 1113 | for (int ws = 0; ws < NUM_WORKSPACES; ws++) { 1114 | for (Client *c = workspaces[ws]; c; c = c->next) { 1115 | XUnmapWindow(dpy, c->win); 1116 | XKillClient(dpy, c->win); 1117 | } 1118 | } 1119 | XSync(dpy, False); 1120 | XCloseDisplay(dpy); 1121 | XFreeCursor(dpy, c_move); 1122 | XFreeCursor(dpy, c_normal); 1123 | XFreeCursor(dpy, c_resize); 1124 | errx(0, "quitting..."); 1125 | } 1126 | 1127 | void reload_config(void) 1128 | { 1129 | puts("sxwm: reloading config..."); 1130 | memset(&user_config, 0, sizeof(user_config)); 1131 | 1132 | for (int i = 0; i < user_config.bindsn; i++) { 1133 | free(user_config.binds[i].action.cmd); 1134 | user_config.binds[i].action.cmd = NULL; 1135 | 1136 | user_config.binds[i].action.fn = NULL; 1137 | user_config.binds[i].type = -1; 1138 | user_config.binds[i].keysym = 0; 1139 | user_config.binds[i].mods = 0; 1140 | } 1141 | 1142 | init_defaults(); 1143 | if (parser(&user_config)) { 1144 | fprintf(stderr, "sxrc: error parsing config file\n"); 1145 | init_defaults(); 1146 | } 1147 | grab_keys(); 1148 | XUngrabButton(dpy, AnyButton, AnyModifier, root); 1149 | XGrabButton(dpy, Button1, user_config.modkey, root, True, ButtonPressMask | ButtonReleaseMask | PointerMotionMask, 1150 | GrabModeAsync, GrabModeAsync, None, None); 1151 | XGrabButton(dpy, Button1, user_config.modkey | ShiftMask, root, True, 1152 | ButtonPressMask | ButtonReleaseMask | PointerMotionMask, GrabModeAsync, GrabModeAsync, None, None); 1153 | XGrabButton(dpy, Button3, user_config.modkey, root, True, ButtonPressMask | ButtonReleaseMask | PointerMotionMask, 1154 | GrabModeAsync, GrabModeAsync, None, None); 1155 | XSync(dpy, False); 1156 | 1157 | tile(); 1158 | update_borders(); 1159 | } 1160 | 1161 | void resize_master_add(void) 1162 | { 1163 | /* pick the monitor of the focused window (or 0 if none) */ 1164 | int m = focused ? focused->mon : 0; 1165 | float *mw = &user_config.master_width[m]; 1166 | 1167 | if (*mw < MF_MAX - 0.001f) { 1168 | *mw += ((float)user_config.resize_master_amt / 100); 1169 | } 1170 | tile(); 1171 | update_borders(); 1172 | } 1173 | 1174 | void resize_master_sub(void) 1175 | { 1176 | /* pick the monitor of the focused window (or 0 if none) */ 1177 | int m = focused ? focused->mon : 0; 1178 | float *mw = &user_config.master_width[m]; 1179 | 1180 | if (*mw > MF_MIN + 0.001f) { 1181 | *mw -= ((float)user_config.resize_master_amt / 100); 1182 | } 1183 | tile(); 1184 | update_borders(); 1185 | } 1186 | 1187 | void run(void) 1188 | { 1189 | XEvent xev; 1190 | for (;;) { 1191 | XNextEvent(dpy, &xev); 1192 | xev_case(&xev); 1193 | } 1194 | } 1195 | 1196 | void scan_existing_windows(void) 1197 | { 1198 | Window root_return, parent_return; 1199 | Window *children; 1200 | unsigned int nchildren; 1201 | 1202 | if (XQueryTree(dpy, root, &root_return, &parent_return, &children, &nchildren)) { 1203 | for (unsigned int i = 0; i < nchildren; i++) { 1204 | XWindowAttributes wa; 1205 | if (!XGetWindowAttributes(dpy, children[i], &wa) || wa.override_redirect || wa.map_state != IsViewable) { 1206 | continue; 1207 | } 1208 | 1209 | XEvent fake_event = {0}; 1210 | fake_event.type = MapRequest; 1211 | fake_event.xmaprequest.window = children[i]; 1212 | hdl_map_req(&fake_event); 1213 | } 1214 | if (children) { 1215 | XFree(children); 1216 | } 1217 | } 1218 | } 1219 | 1220 | void send_wm_take_focus(Window w) 1221 | { 1222 | Atom wm_protocols = XInternAtom(dpy, "WM_PROTOCOLS", False); 1223 | Atom wm_take_focus = XInternAtom(dpy, "WM_TAKE_FOCUS", False); 1224 | Atom *protos; 1225 | int n; 1226 | if (XGetWMProtocols(dpy, w, &protos, &n)) { 1227 | for (int i = 0; i < n; i++) { 1228 | if (protos[i] == wm_take_focus) { 1229 | XEvent ev = { 1230 | .xclient = {.type = ClientMessage, .window = w, .message_type = wm_protocols, .format = 32}}; 1231 | ev.xclient.data.l[0] = wm_take_focus; 1232 | ev.xclient.data.l[1] = CurrentTime; 1233 | XSendEvent(dpy, w, False, NoEventMask, &ev); 1234 | } 1235 | } 1236 | XFree(protos); 1237 | } 1238 | } 1239 | 1240 | void setup(void) 1241 | { 1242 | if ((dpy = XOpenDisplay(NULL)) == 0) { 1243 | errx(0, "can't open display. quitting..."); 1244 | } 1245 | root = XDefaultRootWindow(dpy); 1246 | 1247 | setup_atoms(); 1248 | other_wm(); 1249 | init_defaults(); 1250 | if (parser(&user_config)) { 1251 | fprintf(stderr, "sxrc: error parsing config file\n"); 1252 | init_defaults(); 1253 | } 1254 | grab_keys(); 1255 | 1256 | c_normal = XcursorLibraryLoadCursor(dpy, "left_ptr"); 1257 | c_move = XcursorLibraryLoadCursor(dpy, "fleur"); 1258 | c_resize = XcursorLibraryLoadCursor(dpy, "bottom_right_corner"); 1259 | XDefineCursor(dpy, root, c_normal); 1260 | 1261 | scr_width = XDisplayWidth(dpy, DefaultScreen(dpy)); 1262 | scr_height = XDisplayHeight(dpy, DefaultScreen(dpy)); 1263 | update_monitors(); 1264 | 1265 | XSelectInput(dpy, root, 1266 | StructureNotifyMask | SubstructureRedirectMask | SubstructureNotifyMask | KeyPressMask | 1267 | PropertyChangeMask); 1268 | 1269 | XGrabButton(dpy, Button1, user_config.modkey, root, True, ButtonPressMask | ButtonReleaseMask | PointerMotionMask, 1270 | GrabModeAsync, GrabModeAsync, None, None); 1271 | XGrabButton(dpy, Button1, user_config.modkey | ShiftMask, root, True, 1272 | ButtonPressMask | ButtonReleaseMask | PointerMotionMask, GrabModeAsync, GrabModeAsync, None, None); 1273 | XGrabButton(dpy, Button3, user_config.modkey, root, True, ButtonPressMask | ButtonReleaseMask | PointerMotionMask, 1274 | GrabModeAsync, GrabModeAsync, None, None); 1275 | XSync(dpy, False); 1276 | 1277 | for (int i = 0; i < LASTEvent; i++) { 1278 | evtable[i] = hdl_dummy; 1279 | } 1280 | 1281 | evtable[ButtonPress] = hdl_button; 1282 | evtable[ButtonRelease] = hdl_button_release; 1283 | evtable[ClientMessage] = hdl_client_msg; 1284 | evtable[ConfigureNotify] = hdl_config_ntf; 1285 | evtable[ConfigureRequest] = hdl_config_req; 1286 | evtable[DestroyNotify] = hdl_destroy_ntf; 1287 | evtable[EnterNotify] = hdl_enter; 1288 | evtable[KeyPress] = hdl_keypress; 1289 | evtable[MapRequest] = hdl_map_req; 1290 | evtable[MotionNotify] = hdl_motion; 1291 | evtable[PropertyNotify] = hdl_root_property; 1292 | evtable[UnmapNotify] = hdl_unmap_ntf; 1293 | scan_existing_windows(); 1294 | 1295 | signal(SIGCHLD, SIG_IGN); /* prevent child processes from becoming zombies */ 1296 | } 1297 | 1298 | void setup_atoms(void) 1299 | { 1300 | Atom a_num = XInternAtom(dpy, "_NET_NUMBER_OF_DESKTOPS", False); 1301 | Atom a_names = XInternAtom(dpy, "_NET_DESKTOP_NAMES", False); 1302 | atom_net_current_desktop = XInternAtom(dpy, "_NET_CURRENT_DESKTOP", False); 1303 | atom_net_active_window = XInternAtom(dpy, "_NET_ACTIVE_WINDOW", False); 1304 | atom_net_supported = XInternAtom(dpy, "_NET_SUPPORTED", False); 1305 | atom_wm_strut_partial = XInternAtom(dpy, "_NET_WM_STRUT_PARTIAL", False); 1306 | atom_wm_strut = XInternAtom(dpy, "_NET_WM_STRUT", False); /* legacy struts */ 1307 | atom_wm_window_type = XInternAtom(dpy, "_NET_WM_WINDOW_TYPE", False); 1308 | atom_net_wm_window_type_dock = XInternAtom(dpy, "_NET_WM_WINDOW_TYPE_DOCK", False); 1309 | atom_net_workarea = XInternAtom(dpy, "_NET_WORKAREA", False); 1310 | atom_net_wm_state = XInternAtom(dpy, "_NET_WM_STATE", False); 1311 | atom_net_wm_state_fullscreen = XInternAtom(dpy, "_NET_WM_STATE_FULLSCREEN", False); 1312 | atom_net_wm_state = XInternAtom(dpy, "_NET_WM_STATE", False); 1313 | atom_net_wm_state_fullscreen = XInternAtom(dpy, "_NET_WM_STATE_FULLSCREEN", False); 1314 | atom_wm_delete = XInternAtom(dpy, "WM_DELETE_WINDOW", False); 1315 | atom_net_supporting_wm_check = XInternAtom(dpy, "_NET_SUPPORTING_WM_CHECK", False); 1316 | atom_net_wm_name = XInternAtom(dpy, "_NET_WM_NAME", False); 1317 | atom_utf8_string = XInternAtom(dpy, "UTF8_STRING", False); 1318 | 1319 | Atom support_list[] = { 1320 | atom_net_current_desktop, 1321 | atom_net_active_window, 1322 | atom_net_supported, 1323 | atom_net_wm_state, 1324 | atom_net_wm_state_fullscreen, 1325 | atom_wm_window_type, 1326 | atom_net_wm_window_type_dock, 1327 | atom_net_workarea, 1328 | atom_wm_strut, 1329 | atom_wm_strut_partial, 1330 | atom_wm_delete, 1331 | atom_net_supporting_wm_check, 1332 | atom_net_wm_name, 1333 | atom_utf8_string, 1334 | }; 1335 | 1336 | long num = NUM_WORKSPACES; 1337 | XChangeProperty(dpy, root, a_num, XA_CARDINAL, 32, PropModeReplace, (unsigned char *)&num, 1); 1338 | 1339 | const char names[] = WORKSPACE_NAMES; 1340 | int names_len = sizeof(names); 1341 | 1342 | XChangeProperty(dpy, root, a_names, XInternAtom(dpy, "UTF8_STRING", False), 8, PropModeReplace, 1343 | (unsigned char *)names, names_len); 1344 | 1345 | long initial = current_ws; 1346 | XChangeProperty(dpy, root, XInternAtom(dpy, "_NET_CURRENT_DESKTOP", False), XA_CARDINAL, 32, PropModeReplace, 1347 | (unsigned char *)&initial, 1); 1348 | 1349 | XChangeProperty(dpy, root, atom_net_supported, XA_ATOM, 32, PropModeReplace, (unsigned char *)support_list, 1350 | sizeof(support_list) / sizeof(Atom)); 1351 | } 1352 | 1353 | Bool window_should_float(Window w) 1354 | { 1355 | XClassHint ch; 1356 | if (XGetClassHint(dpy, w, &ch)) { 1357 | for (int i = 0; i < 256; i++) { 1358 | if (!user_config.should_float[i] || !user_config.should_float[i][0]) { 1359 | break; 1360 | } 1361 | 1362 | printf("[DEBUG] Checking window class '%s' and instance '%s' against should_float[%d][0] = '%s'\n", 1363 | ch.res_class ? ch.res_class : "NULL", ch.res_name ? ch.res_name : "NULL", i, 1364 | user_config.should_float[i][0]); 1365 | 1366 | if ((ch.res_class && !strcmp(ch.res_class, user_config.should_float[i][0])) || 1367 | (ch.res_name && !strcmp(ch.res_name, user_config.should_float[i][0]))) { 1368 | printf("[DEBUG] Window should float based on class/instance match\n"); 1369 | XFree(ch.res_class); 1370 | XFree(ch.res_name); 1371 | return True; 1372 | } 1373 | } 1374 | XFree(ch.res_class); 1375 | XFree(ch.res_name); 1376 | } 1377 | 1378 | return False; 1379 | } 1380 | 1381 | void spawn(const char **argv) 1382 | { 1383 | int pipe_idx = -1; 1384 | for (int i = 0; argv[i]; i++) { 1385 | if (strcmp(argv[i], "|") == 0) { 1386 | pipe_idx = i; 1387 | break; 1388 | } 1389 | } 1390 | 1391 | if (pipe_idx < 0) { 1392 | if (fork() == 0) { 1393 | close(ConnectionNumber(dpy)); 1394 | setsid(); 1395 | execvp(argv[0], (char *const *)argv); 1396 | fprintf(stderr, "sxwm: execvp '%s' failed\n", argv[0]); 1397 | exit(EXIT_FAILURE); 1398 | } 1399 | } 1400 | else { 1401 | ((char **)argv)[pipe_idx] = NULL; 1402 | const char **left = argv; 1403 | const char **right = argv + pipe_idx + 1; 1404 | int fd[2]; 1405 | Bool x = pipe(fd); 1406 | (void)x; 1407 | 1408 | pid_t pid1 = fork(); 1409 | if (pid1 == 0) { 1410 | dup2(fd[1], STDOUT_FILENO); 1411 | close(fd[0]); 1412 | close(fd[1]); 1413 | execvp(left[0], (char *const *)left); 1414 | perror("spawn left"); 1415 | exit(EXIT_FAILURE); 1416 | } 1417 | 1418 | pid_t pid2 = fork(); 1419 | if (pid2 == 0) { 1420 | dup2(fd[0], STDIN_FILENO); 1421 | close(fd[0]); 1422 | close(fd[1]); 1423 | execvp(right[0], (char *const *)right); 1424 | perror("spawn right"); 1425 | exit(EXIT_FAILURE); 1426 | } 1427 | 1428 | close(fd[0]); 1429 | close(fd[1]); 1430 | waitpid(pid1, NULL, 0); 1431 | waitpid(pid2, NULL, 0); 1432 | } 1433 | } 1434 | 1435 | void tile(void) 1436 | { 1437 | update_struts(); /* fills reserve_top, reserve_bottom */ 1438 | 1439 | Client *head = workspaces[current_ws]; 1440 | 1441 | int total = 0; 1442 | for (Client *c = head; c; c = c->next) { 1443 | if (!c->mapped || c->floating || c->fullscreen) 1444 | continue; 1445 | total++; 1446 | } 1447 | 1448 | if (total == 1) { 1449 | for (Client *c = head; c; c = c->next) { 1450 | if (!c->floating && c->fullscreen) 1451 | return; 1452 | } 1453 | } 1454 | 1455 | for (int m = 0; m < monsn; m++) { 1456 | int mon_x = mons[m].x; 1457 | int mon_y = mons[m].y + reserve_top; 1458 | int mon_w = mons[m].w; 1459 | int mon_h = mons[m].h - reserve_top - reserve_bottom; 1460 | 1461 | int cnt = 0; 1462 | for (Client *c = head; c; c = c->next) { 1463 | if (!c->mapped || c->floating || c->fullscreen || c->mon != m) 1464 | continue; 1465 | cnt++; 1466 | } 1467 | if (!cnt) 1468 | continue; 1469 | 1470 | int gx = user_config.gaps; 1471 | int gy = user_config.gaps; 1472 | int tile_x = mon_x + gx; 1473 | int tile_y = mon_y + gy; 1474 | int tile_w = mon_w - 2 * gx; 1475 | int tile_h = mon_h - 2 * gy; 1476 | if (tile_w < 1) 1477 | tile_w = 1; 1478 | if (tile_h < 1) 1479 | tile_h = 1; 1480 | 1481 | /* master‐stack split */ 1482 | float mf = user_config.master_width[m]; 1483 | if (mf < MF_MIN) 1484 | mf = MF_MIN; 1485 | if (mf > MF_MAX) 1486 | mf = MF_MAX; 1487 | int master_w = (cnt > 1) ? (int)(tile_w * mf) : tile_w; 1488 | int stack_w = tile_w - master_w - ((cnt > 1) ? gx : 0); 1489 | int stack_h = (cnt > 1) ? (tile_h - (cnt - 1) * gy) / (cnt - 1) : 0; 1490 | 1491 | int i = 0, sy = tile_y; 1492 | for (Client *c = head; c; c = c->next) { 1493 | if (!c->mapped || c->floating || c->fullscreen || c->mon != m) 1494 | continue; 1495 | 1496 | XWindowChanges wc = {.border_width = user_config.border_width}; 1497 | int bw2 = 2 * user_config.border_width; 1498 | 1499 | if (i == 0) { 1500 | /* master */ 1501 | wc.x = tile_x; 1502 | wc.y = tile_y; 1503 | wc.width = MAX(1, master_w - bw2); 1504 | wc.height = MAX(1, tile_h - bw2); 1505 | } 1506 | else { 1507 | /* stack */ 1508 | wc.x = tile_x + master_w + gx; 1509 | wc.y = sy; 1510 | int h = (i == cnt - 1) ? (tile_y + tile_h - sy) : stack_h; 1511 | wc.width = MAX(1, stack_w - bw2); 1512 | wc.height = MAX(1, h - bw2); 1513 | sy += h + gy; 1514 | } 1515 | 1516 | XConfigureWindow(dpy, c->win, CWX | CWY | CWWidth | CWHeight | CWBorderWidth, &wc); 1517 | c->x = wc.x; 1518 | c->y = wc.y; 1519 | c->w = wc.width; 1520 | c->h = wc.height; 1521 | i++; 1522 | } 1523 | } 1524 | 1525 | update_borders(); 1526 | } 1527 | 1528 | void toggle_floating(void) 1529 | { 1530 | if (!focused) { 1531 | return; 1532 | } 1533 | 1534 | if (focused->fullscreen) { 1535 | focused->fullscreen = False; 1536 | tile(); 1537 | XSetWindowBorderWidth(dpy, focused->win, user_config.border_width); 1538 | } 1539 | 1540 | focused->floating = !focused->floating; 1541 | 1542 | if (focused->floating) { 1543 | XWindowAttributes wa; 1544 | if (XGetWindowAttributes(dpy, focused->win, &wa)) { 1545 | focused->x = wa.x; 1546 | focused->y = wa.y; 1547 | focused->w = wa.width; 1548 | focused->h = wa.height; 1549 | 1550 | XConfigureWindow( 1551 | dpy, focused->win, CWX | CWY | CWWidth | CWHeight, 1552 | &(XWindowChanges){.x = focused->x, .y = focused->y, .width = focused->w, .height = focused->h}); 1553 | } 1554 | } 1555 | else { 1556 | focused->mon = get_monitor_for(focused); 1557 | } 1558 | 1559 | if (!focused->floating) { 1560 | focused->mon = get_monitor_for(focused); 1561 | } 1562 | tile(); 1563 | update_borders(); 1564 | 1565 | /* raise and refocus floating window */ 1566 | if (focused->floating) { 1567 | XRaiseWindow(dpy, focused->win); 1568 | XSetInputFocus(dpy, focused->win, RevertToPointerRoot, CurrentTime); 1569 | } 1570 | } 1571 | 1572 | void toggle_floating_global(void) 1573 | { 1574 | global_floating = !global_floating; 1575 | Bool any_tiled = False; 1576 | for (Client *c = workspaces[current_ws]; c; c = c->next) { 1577 | if (!c->floating) { 1578 | any_tiled = True; 1579 | break; 1580 | } 1581 | } 1582 | 1583 | for (Client *c = workspaces[current_ws]; c; c = c->next) { 1584 | c->floating = any_tiled; 1585 | if (c->floating) { 1586 | XWindowAttributes wa; 1587 | XGetWindowAttributes(dpy, c->win, &wa); 1588 | c->x = wa.x; 1589 | c->y = wa.y; 1590 | c->w = wa.width; 1591 | c->h = wa.height; 1592 | 1593 | XConfigureWindow(dpy, c->win, CWX | CWY | CWWidth | CWHeight, 1594 | &(XWindowChanges){.x = c->x, .y = c->y, .width = c->w, .height = c->h}); 1595 | XRaiseWindow(dpy, c->win); 1596 | } 1597 | } 1598 | 1599 | tile(); 1600 | update_borders(); 1601 | } 1602 | 1603 | void toggle_fullscreen(void) 1604 | { 1605 | if (!focused) { 1606 | return; 1607 | } 1608 | 1609 | if (focused->floating) { 1610 | focused->floating = False; 1611 | } 1612 | 1613 | focused->fullscreen = !focused->fullscreen; 1614 | 1615 | if (focused->fullscreen) { 1616 | XWindowAttributes wa; 1617 | XGetWindowAttributes(dpy, focused->win, &wa); 1618 | focused->orig_x = wa.x; 1619 | focused->orig_y = wa.y; 1620 | focused->orig_w = wa.width; 1621 | focused->orig_h = wa.height; 1622 | 1623 | int m = focused->mon; 1624 | int fs_x = mons[m].x; 1625 | int fs_y = mons[m].y; 1626 | int fs_w = mons[m].w; 1627 | int fs_h = mons[m].h; 1628 | 1629 | XSetWindowBorderWidth(dpy, focused->win, 0); 1630 | XMoveResizeWindow(dpy, focused->win, fs_x, fs_y, fs_w, fs_h); 1631 | XRaiseWindow(dpy, focused->win); 1632 | } 1633 | else { 1634 | XMoveResizeWindow(dpy, focused->win, focused->orig_x, focused->orig_y, focused->orig_w, focused->orig_h); 1635 | XSetWindowBorderWidth(dpy, focused->win, user_config.border_width); 1636 | 1637 | if (!focused->floating) { 1638 | focused->mon = get_monitor_for(focused); 1639 | } 1640 | tile(); 1641 | update_borders(); 1642 | } 1643 | } 1644 | 1645 | void update_borders(void) 1646 | { 1647 | for (Client *c = workspaces[current_ws]; c; c = c->next) { 1648 | XSetWindowBorder(dpy, c->win, (c == focused ? user_config.border_foc_col : user_config.border_ufoc_col)); 1649 | } 1650 | if (focused) { 1651 | Window w = focused->win; 1652 | XChangeProperty(dpy, root, atom_net_active_window, XA_WINDOW, 32, PropModeReplace, (unsigned char *)&w, 1); 1653 | } 1654 | } 1655 | 1656 | void update_monitors(void) 1657 | { 1658 | XineramaScreenInfo *info; 1659 | Monitor *old = mons; 1660 | 1661 | scr_width = XDisplayWidth(dpy, DefaultScreen(dpy)); 1662 | scr_height = XDisplayHeight(dpy, DefaultScreen(dpy)); 1663 | 1664 | for (int s = 0; s < ScreenCount(dpy); s++) { 1665 | Window scr_root = RootWindow(dpy, s); 1666 | XDefineCursor(dpy, scr_root, c_normal); 1667 | } 1668 | 1669 | if (XineramaIsActive(dpy)) { 1670 | info = XineramaQueryScreens(dpy, &monsn); 1671 | mons = malloc(sizeof *mons * monsn); 1672 | for (int i = 0; i < monsn; i++) { 1673 | mons[i].x = info[i].x_org; 1674 | mons[i].y = info[i].y_org; 1675 | mons[i].w = info[i].width; 1676 | mons[i].h = info[i].height; 1677 | } 1678 | XFree(info); 1679 | } 1680 | else { 1681 | monsn = 1; 1682 | mons = malloc(sizeof *mons); 1683 | mons[0].x = 0; 1684 | mons[0].y = 0; 1685 | mons[0].w = scr_width; 1686 | mons[0].h = scr_height; 1687 | } 1688 | 1689 | free(old); 1690 | } 1691 | 1692 | void update_net_client_list(void) 1693 | { 1694 | Window wins[MAXCLIENTS]; 1695 | int n = 0; 1696 | for (int ws = 0; ws < NUM_WORKSPACES; ws++) { 1697 | for (Client *c = workspaces[ws]; c; c = c->next) { 1698 | wins[n++] = c->win; 1699 | } 1700 | } 1701 | Atom prop = XInternAtom(dpy, "_NET_CLIENT_LIST", False); 1702 | XChangeProperty(dpy, root, prop, XA_WINDOW, 32, PropModeReplace, (unsigned char *)wins, n); 1703 | } 1704 | 1705 | int xerr(Display *dpy, XErrorEvent *ee) 1706 | { 1707 | /* ignore noise & non fatal errors */ 1708 | const struct { 1709 | int req, code; 1710 | } ignore[] = { 1711 | {0, BadWindow}, 1712 | {X_GetGeometry, BadDrawable}, 1713 | {X_SetInputFocus, BadMatch}, 1714 | {X_ConfigureWindow, BadMatch}, 1715 | }; 1716 | 1717 | for (size_t i = 0; i < sizeof(ignore) / sizeof(ignore[0]); i++) { 1718 | if ((ignore[i].req == 0 || ignore[i].req == ee->request_code) && (ignore[i].code == ee->error_code)) { 1719 | return 0; 1720 | } 1721 | } 1722 | 1723 | return 0; 1724 | (void)dpy; 1725 | (void)ee; 1726 | } 1727 | 1728 | void xev_case(XEvent *xev) 1729 | { 1730 | if (xev->type >= 0 && xev->type < LASTEvent) { 1731 | evtable[xev->type](xev); 1732 | } 1733 | else { 1734 | printf("sxwm: invalid event type: %d\n", xev->type); 1735 | } 1736 | } 1737 | 1738 | int main(int ac, char **av) 1739 | { 1740 | if (ac > 1) { 1741 | if (strcmp(av[1], "-v") == 0 || strcmp(av[1], "--version") == 0) { 1742 | printf("%s\n%s\n%s", SXWM_VERSION, SXWM_AUTHOR, SXWM_LICINFO); 1743 | exit(0); 1744 | } 1745 | else { 1746 | printf("usage:\n[-v || --version]: See the version of sxwm"); 1747 | exit(0); 1748 | } 1749 | } 1750 | setup(); 1751 | printf("sxwm: starting...\n"); 1752 | run(); 1753 | return 0; 1754 | } 1755 | -------------------------------------------------------------------------------- /sxwm.1: -------------------------------------------------------------------------------- 1 | .TH SXWM 1 "May 2025" "sxwm 1.5" "User Commands" 2 | 3 | .SH NAME 4 | sxwm \- minimal, fast, and configurable tiling window manager for X11 5 | 6 | .SH SYNOPSIS 7 | .B sxwm 8 | 9 | .SH DESCRIPTION 10 | sxwm is a lightweight and efficient tiling window manager for X11, designed to be fast, minimal, and easy to configure. It supports workspaces, floating windows, mouse operations, and dynamic configuration reloading. 11 | 12 | .SH FEATURES 13 | Tiling and floating layouts. 14 | Nine workspaces with full bar support. 15 | Live configuration reload without restart. 16 | Human-friendly configuration file requiring no recompilation. 17 | DWM-style master-stack layout. 18 | Mouse support for moving, resizing, focusing, and swapping windows. 19 | Depends only on libX11 and Xinerama. 20 | Extremely lightweight (single C file). 21 | Multi-monitor support via Xinerama. 22 | Works well with external bars such as sxbar. 23 | 24 | .SH CONFIGURATION 25 | The configuration file is located at 26 | .B ~/.config/sxwmrc 27 | 28 | It uses a simple key : value format. Lines starting with `#` are treated as comments. 29 | 30 | General options include: 31 | 32 | .TP 33 | .B mod_key 34 | Sets the primary modifier key (for example, "alt", "super", or "ctrl"). Default is "super". 35 | 36 | .TP 37 | .B gaps 38 | Pixels between windows and screen edges. Default is 10. 39 | 40 | .TP 41 | .B border_width 42 | Thickness of window borders in pixels. Default is 1. 43 | 44 | .TP 45 | .B focused_border_colour 46 | Border color for the focused window. Default is "#c0cbff". 47 | 48 | .TP 49 | .B unfocused_border_colour 50 | Border color for unfocused windows. Default is "#555555". 51 | 52 | .TP 53 | .B swap_border_colour 54 | Border color highlight when selecting a window to swap with. Default is "#fff4c0". 55 | 56 | .TP 57 | .B master_width 58 | Percentage of screen width allocated to the master window. Default is 60. 59 | 60 | .TP 61 | .B resize_master_amount 62 | Percentage to increase or decrease master width when resizing. Default is 1. 63 | 64 | .TP 65 | .B snap_distance 66 | Pixels from screen edge before a floating window snaps to the edge. Default is 5. 67 | 68 | .TP 69 | .B motion_throttle 70 | Target updates per second for mouse drag operations (move, resize, swap). Default is 60. 71 | 72 | .TP 73 | .B should_float 74 | Lets you change which windows should float by default when opening them. Default is st 75 | 76 | .SH KEYBINDINGS 77 | Keybindings associate key combinations with actions, either running external commands or internal sxwm functions. 78 | 79 | They follow this syntax: 80 | 81 | .TP 82 | .B bind : modifier + modifier + ... + key : action 83 | 84 | Modifiers can be mod, shift, ctrl, alt, or super. The key is the final key name (e.g., Return, q, 1, equal, space). 85 | 86 | Actions can be either a quoted external command or an internal function name. 87 | 88 | Example bindings: 89 | 90 | .TP 91 | .B bind : mod + Return : "st" 92 | 93 | Open the st terminal. 94 | 95 | .TP 96 | .B bind : mod + shift + q : close_window 97 | 98 | Close any window that is selected. 99 | 100 | .TP 101 | .B bind : mod + 3 : change_ws3 102 | 103 | Go to workspace 3. 104 | 105 | .TP 106 | .B bind : mod + shift + 5 : moveto_ws5 107 | 108 | Move selected window to workspace 5. 109 | 110 | .SH AVAILABLE FUNCTIONS 111 | The following internal functions are available for keybindings: 112 | 113 | .TP 114 | .B close_window 115 | Closes the currently focused window. 116 | 117 | .TP 118 | .B decrease_gaps 119 | Decreases the gap size between windows. 120 | 121 | .TP 122 | .B focus_next 123 | Shifts focus to the next window in the stack. 124 | 125 | .TP 126 | .B focus_previous 127 | Shifts focus to the previous window in the stack. 128 | 129 | .TP 130 | .B increase_gaps 131 | Increases the gap size between windows. 132 | 133 | .TP 134 | .B master_next 135 | Moves the focused window down the master/stack order. 136 | 137 | .TP 138 | .B master_previous 139 | Moves the focused window up the master/stack order. 140 | 141 | .TP 142 | .B quit 143 | Exits sxwm. 144 | 145 | .TP 146 | .B reload_config 147 | Reloads the sxwmrc configuration file. 148 | 149 | .TP 150 | .B master_increase 151 | Increases the width allocated to the master area. 152 | 153 | .TP 154 | .B master_decrease 155 | Decreases the width allocated to the master area. 156 | 157 | .TP 158 | .B toggle_floating 159 | Toggles the floating state of the focused window. 160 | 161 | .TP 162 | .B global_floating 163 | Toggles the floating state for all windows on the current workspace. 164 | 165 | .TP 166 | .B fullscreen 167 | Toggles fullscreen mode for the focused window. 168 | 169 | .TP 170 | .B change_ws1 ... change_ws9 171 | Switches focus to the specified workspace (1 to 9). 172 | 173 | .TP 174 | .B moveto_ws1 ... moveto_ws9 175 | Moves the focused window to the specified workspace (1 to 9). 176 | 177 | .SH DEFAULT KEYBINDINGS 178 | Window Management: 179 | 180 | .TP 181 | .B MOD + Return 182 | Launch terminal (default: st). 183 | 184 | .TP 185 | .B MOD + b 186 | Launch browser (default: firefox). 187 | 188 | .TP 189 | .B MOD + p 190 | Run launcher (default: dmenu_run). 191 | 192 | .TP 193 | .B MOD + q 194 | Close focused window. 195 | 196 | .TP 197 | .B MOD + 1 to 9 198 | Switch to workspace 1 through 9. 199 | 200 | .TP 201 | .B MOD + Shift + 1 to 9 202 | Move focused window to workspace 1 through 9. 203 | 204 | .TP 205 | .B MOD + j / k 206 | Focus next or previous window. 207 | 208 | .TP 209 | .B MOD + Shift + j / k 210 | Move window up or down in the master stack. 211 | 212 | .TP 213 | .B MOD + Space 214 | Toggle floating mode for focused window. 215 | 216 | .TP 217 | .B MOD + Shift + Space 218 | Toggle floating mode for all windows. 219 | 220 | .TP 221 | .B MOD + = / - 222 | Increase or decrease gaps. 223 | 224 | .TP 225 | .B MOD + f 226 | Toggle fullscreen mode. 227 | 228 | .TP 229 | .B MOD + Left Mouse 230 | Move window with mouse. 231 | 232 | .TP 233 | .B MOD + Right Mouse 234 | Resize window with mouse. 235 | 236 | .SH FILES 237 | Configuration file: 238 | .B ~/.config/sxwmrc 239 | 240 | .SH AUTHOR 241 | Written by El Bachir (elbachir-one), 2025. 242 | 243 | .SH SEE ALSO 244 | sxbar(1), dmenu(1), st(1), X(7) 245 | 246 | .SH LICENSE 247 | MIT License. See the LICENSE file for full details. 248 | -------------------------------------------------------------------------------- /sxwm.desktop: -------------------------------------------------------------------------------- 1 | [Desktop Entry] 2 | Name=Sxwm 3 | Comment=Simple Xorg Window Manager 4 | Exec=sxwm 5 | TryExec=sxwm 6 | Type=Application 7 | --------------------------------------------------------------------------------