├── .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 |
--------------------------------------------------------------------------------