summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--CHANGELOG.md23
-rw-r--r--CONTRIBUTIONS.md90
-rw-r--r--README.md14
-rw-r--r--default_sxwmrc8
-rw-r--r--images/1.pngbin234882 -> 0 bytes
-rw-r--r--images/3.pngbin1629407 -> 0 bytes
-rw-r--r--images/4.pngbin1538304 -> 0 bytes
-rw-r--r--images/sxwm_logo.psdbin477842 -> 0 bytes
-rw-r--r--images/x.pngbin875729 -> 0 bytes
-rw-r--r--src/config.h3
-rw-r--r--src/defs.h9
-rw-r--r--src/parser.c88
-rw-r--r--src/sxwm.c423
-rw-r--r--sxwm.128
-rw-r--r--sxwm.desktop2
15 files changed, 518 insertions, 170 deletions
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 1db9d69..0c24308 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,17 +2,32 @@
All notable changes to this project will be documented in this file.
-#### v1.6 (current)
+#### v1.6 (git)
- **NEW**: True multi-monitor support
+- **NEW**: Vertical stack resizing
+- **NEW**: Mouse warping
+- **NEW**: Floating window rules
+- **NEW**: Ctrl key as modifier
+- **NEW**: Focus window on creation
+- **NEW**: Copy config to `/usr/local/share/sxwmrc`
+- **CHANGE**: Renamed `focus_previous` to `focus_prev`
+- **CHANGE**: Invalid sample config
+- **CHANGE**: Parser `$HOME` searching order. XDG Compliance
+- **FIXED**: Improved parsing now supporting commands with `"` and `'`
+- **FIXED**: (mouse warping) Switching to master doesn't automatically shift cursor to it
+- **FIXED**: `ctrl` and `shift` key works as a modifier
+- **FIXED**: Fixed build error (#64).
+- **FIXED**: Removed debug logs
+- **FIXED**: Fixed new window getting interrupted by mouse
+- **FIXED**: Fixed `should_float` segfalt
- **FIXED**: Invisible windows of minimized programs
- **FIXED**: Zombie processes spawned from apps
-- **FIXED**: Invalid sample config
- **FIXED**: Undefined behaviour in `parse_col`
-#### v1.5
+#### v1.5 (current)
- **NEW**: Using XCursor instead of cursor font && new logo.
-- **FIXED**: Proper bind resetting on refresh config. && Multi-arg binds now work due to new and improved spawn function
- **CHANGE**: No longer using INIT_WORKSPACE macro, proper workspace handling. New sxwmrc
+- **FIXED**: Proper bind resetting on refresh config. && Multi-arg binds now work due to new and improved spawn function
#### v1.4
- **CHANGE**: Added motion throttle && master width general options
diff --git a/CONTRIBUTIONS.md b/CONTRIBUTIONS.md
new file mode 100644
index 0000000..5132eab
--- /dev/null
+++ b/CONTRIBUTIONS.md
@@ -0,0 +1,90 @@
+# Contributing to sxwm
+
+Firstly, thanks for taking the time to contribute to sxwm! I appreciate your interest in improving the project.
+
+## Code Style and Formatting
+
+There is an included clangd formatting file with this inside, but it wont do everything for you.
+
+### Indentation
+Indentation must always be **tabs** not spaces.
+
+### Blocks
+- All blocks of C code must be encased with curly braces `{}`
+- Blocks must also be seperated, each statement on a new line
+- Statements must be formatted with a space between the keyword and the brackets
+
+**Example**:
+``` c
+if (x) {
+ y();
+}
+else {
+ z();
+}
+```
+
+### Comments
+Comments must be like this to keep the look consistent across the whole program.
+`/* this is a comment */`
+
+### Function Declarations
+They must look like this
+``` c
+void function_x(void)
+{
+}
+
+void function_y(int y)
+{
+}
+```
+
+### Variable Naming
+Variables and function names must be in `snake_case`
+``` c
+void function_a(void);
+void function_b(void);
+```
+``` c
+int some_variable = 2;
+float other_variable = 5;
+```
+
+## Build
+
+- Make sure `make` succeeds with no warnings on your system
+- Don't commit any build artifacts or backup files
+- Test your changes on **multiple monitors (Xephyr)**
+
+## File Layouts
+
+- Please try to not make a new C file or header unless it is necissary. Most of the codebase is kept in the `sxwm.c` file.
+
+## Submitting Changes
+
+- Open a pull request with a **clear report** of the change(s)
+- Keep commits having one purpose per commit
+- If you fix a bug, please describe how to reproduce it
+- If adding a feature, make sure it is consistent with sxwm’s minimalist philosophy
+- Please don't keep multiple changes in one PR. Open a new one for that
+
+## Documentation
+
+- If applicable, **update `sxwm.1`, `README.md` and `default_sxwmrc`**, update `CHANGELOG.md`
+
+## Respect the Existing Structure
+
+- Before large changes, open an issue discussion
+- Please don’t change for style unless it improves clarity or fixes a problem
+
+## Testing
+
+- [x] Builds with no warnings
+- [x] Tested to work fully
+- [x] All changes are explained in the PR
+- [x] Configuration reload works (if relevant)
+
+---
+
+**Happy hacking!**
diff --git a/README.md b/README.md
index ad58ed8..a08fac6 100644
--- a/README.md
+++ b/README.md
@@ -1,8 +1,11 @@
> ⚠️ **Note:** I won’t be updating this project for a month or so due to exams.
> Issues & PRs are welcome, just don't expect a quick response 🥀🥀
+
> **24/05/25:** I have very _little_ time but I am able to develop some features
> Thank you to the wonderful people who have sumbitted fixes and other PR's
+> **01/06/25:** I will be back to exams so I will have little to no time but I'm _finally_ nearly done with them!
+
<div align="center">
<img src="images/sxwm_logo.png" width="50%">
<br>
@@ -37,7 +40,7 @@
- **Live Config Reload**: Change your config and reload instantly with a keybind.
- **Easy Configuration**: Human-friendly `sxwmrc` file, no C required.
- **Master-Stack Layout**: DWM-inspired productive workflow.
-- **Mouse Support**: Move, swap, resize, and focus windows with the mouse.
+- **Mouse Support**: Move, swap, and resize windows with the mouse.
- **Zero Dependencies**: Only `libX11` and `Xinerama` required.
- **Lightweight**: Single C file, minimal headers, compiles in seconds.
- **Bar Friendly**: Works great with [sxbar](https://github.com/uint23/sxbar).
@@ -66,13 +69,14 @@ The file uses a `key : value` format. Lines starting with `#` are ignored.
| `border_width` | Integer | `1` | Thickness of window borders in pixels. |
| `focused_border_colour` | Hex | `#c0cbff` | Border color for the currently focused window. |
| `unfocused_border_colour`| Hex | `#555555` | Border color for unfocused windows. |
-| `swap_border_colour` | Hex | `#fff4c0` | Border color when selecting a window to swap (`MOD+Shift+Drag`). |
+| `swap_border_colour` | Hex | `#fff4c0` | Border color when selecting a window to swap (`MOD+Shift+Drag`). |
| `master_width` | Integer | `60` | Percentage of the screen width for the master window. |
| `resize_master_amount` | Integer | `1` | Percent to increase/decrease master width. |
| `snap_distance` | Integer | `5` | Distance (px) before a floating window snaps to edge. |
| `motion_throttle` | Integer | `60` | Target FPS for mouse drag actions. |
| `should_float` | String | `"st"` | Always-float rule. Multiple entries should be comma-seperated. Optionally, entries can be enclosed in quotes.|
| `new_win_focus` | Bool | `true` | Whether openening new windows should also set focus to them or keep on current window.|
+| `warp_cursor` | Bool | `true` | Warp the cursor to the middle of newly focused windows |
---
@@ -108,7 +112,7 @@ workspace : modifier + modifier + ... + key : swap n
| `focus_previous` | Moves focus backward in the stack. |
| `increase_gaps` | Expands gaps. |
| `master_next` | Moves focused window down in master/stack order. |
-| `master_previous` | Moves focused window up in master/stack order. |
+| `master_prev` | Moves focused window up in master/stack order. |
| `quit` | Exits `sxwm`. |
| `reload_config` | Reloads config. |
| `master_increase` | Expands master width. |
@@ -197,7 +201,7 @@ sudo xbps-install libX11-devel libXinerama-devel gcc make</code></pre>
<details>
<summary>Fedora / RHEL / AlmaLinux / Rocky</summary>
<pre><code>sudo dnf update
-sudo dnf install libX11-devel libXinerama-devel gcc make</code></pre>
+sudo dnf install libX11-devel libXcursor-devel libXinerama-devel gcc make</code></pre>
</details>
<details>
@@ -301,5 +305,5 @@ exec sxwm
---
<p align="center">
- <em>Contributions welcome! Open issues or submit PRs.</em>
+ <em>Contributions welcome, Please read CONTRIBUTIONS.md for more info!</em>
</p>
diff --git a/default_sxwmrc b/default_sxwmrc
index 53e3f32..6efede7 100644
--- a/default_sxwmrc
+++ b/default_sxwmrc
@@ -8,10 +8,12 @@ gaps : 10
border_width : 1
master_width : 60 # Percentage of screen width
resize_master_amount : 1
+resize_stack_amt : 20
snap_distance : 5
motion_throttle : 60 # Set to screen refresh rate for smoothest motions
-should_float : st
+should_float : "pcmanfm"
new_win_focus : true
+warp_cursor : true
# Keybinds:
# Commands must be surrounded with ""
@@ -40,6 +42,10 @@ call : mod + shift + k : master_previous
call : mod + l : master_increase
call : mod + h : master_decrease
+# Stack Window Resize
+call : mod + ctrl + l : stack_increase
+call : mod + ctrl + h : stack_decrease
+
# Gaps
call : mod + equal : increase_gaps
call : mod + minus : decrease_gaps
diff --git a/images/1.png b/images/1.png
deleted file mode 100644
index 7f742d4..0000000
--- a/images/1.png
+++ /dev/null
Binary files differ
diff --git a/images/3.png b/images/3.png
deleted file mode 100644
index dba0bca..0000000
--- a/images/3.png
+++ /dev/null
Binary files differ
diff --git a/images/4.png b/images/4.png
deleted file mode 100644
index aac1783..0000000
--- a/images/4.png
+++ /dev/null
Binary files differ
diff --git a/images/sxwm_logo.psd b/images/sxwm_logo.psd
deleted file mode 100644
index d8d90e2..0000000
--- a/images/sxwm_logo.psd
+++ /dev/null
Binary files differ
diff --git a/images/x.png b/images/x.png
deleted file mode 100644
index a5b8f8a..0000000
--- a/images/x.png
+++ /dev/null
Binary files differ
diff --git a/src/config.h b/src/config.h
index ccc28ea..7c42764 100644
--- a/src/config.h
+++ b/src/config.h
@@ -19,6 +19,9 @@ const Binding binds[] = {
{Mod4Mask, XK_l, {.fn = resize_master_add}, TYPE_FUNC},
{Mod4Mask, XK_h, {.fn = resize_master_sub}, TYPE_FUNC},
+ {Mod4Mask | ControlMask, XK_l, {.fn = resize_stack_add}, TYPE_FUNC},
+ {Mod4Mask | ControlMask, XK_h, {.fn = resize_stack_sub}, TYPE_FUNC},
+
{Mod4Mask, XK_equal, {.fn = inc_gaps}, TYPE_FUNC},
{Mod4Mask, XK_minus, {.fn = dec_gaps}, TYPE_FUNC},
diff --git a/src/defs.h b/src/defs.h
index 3b8d7fc..53c7c6a 100644
--- a/src/defs.h
+++ b/src/defs.h
@@ -1,7 +1,7 @@
/* See LICENSE for more information on use */
#pragma once
#include <X11/Xlib.h>
-#define SXWM_VERSION "sxwm ver. 1.5"
+#define SXWM_VERSION "sxwm ver. 1.6"
#define SXWM_AUTHOR "(C) Abhinav Prasai 2025"
#define SXWM_LICINFO "See LICENSE for more info"
@@ -18,7 +18,7 @@
#define MIN(a, b) ((a) < (b) ? (a) : (b))
#define LENGTH(X) (sizeof X / sizeof X[0])
#define UDIST(a,b) abs((int)(a) - (int)(b))
-# define CLAMP(x, lo, hi) (( (x) < (lo) ) ? (lo) : ( (x) > (hi) ) ? (hi) : (x))
+#define CLAMP(x, lo, hi) (( (x) < (lo) ) ? (lo) : ( (x) > (hi) ) ? (hi) : (x))
#define MAXCLIENTS 99
#define BIND(mod, key, cmdstr) { (mod), XK_##key, { cmdstr }, False }
#define CALL(mod, key, fnptr) { (mod), XK_##key, { .fn = fnptr }, True }
@@ -68,6 +68,7 @@ typedef struct Client{
Window win;
int x, y, h, w;
int orig_x, orig_y, orig_w, orig_h;
+ int custom_stack_height;
int mon;
int ws;
Bool fixed;
@@ -87,9 +88,11 @@ typedef struct {
float master_width[MAX_MONITORS];
int motion_throttle;
int resize_master_amt;
+ int resize_stack_amt;
int snap_distance;
int bindsn;
Bool new_win_focus;
+ Bool warp_cursor;
Binding binds[256];
char **should_float[256];
char *torun[256];
@@ -112,6 +115,8 @@ extern void quit(void);
extern void reload_config(void);
extern void resize_master_add(void);
extern void resize_master_sub(void);
+extern void resize_stack_add(void);
+extern void resize_stack_sub(void);
extern void toggle_floating(void);
extern void toggle_floating_global(void);
extern void toggle_fullscreen(void);
diff --git a/src/parser.c b/src/parser.c
index cfd7c00..e9b4d16 100644
--- a/src/parser.c
+++ b/src/parser.c
@@ -1,12 +1,13 @@
#define _POSIX_C_SOURCE 200809L
-#include <X11/Xlib.h>
#include <ctype.h>
#include <limits.h>
#include <stdio.h>
#include <stdlib.h>
-#include <unistd.h>
#include <string.h>
+#include <unistd.h>
+#include <wordexp.h>
#include <X11/keysym.h>
+#include <X11/Xlib.h>
#include "parser.h"
#include "defs.h"
@@ -25,6 +26,8 @@ static const struct {
{"reload_config", reload_config},
{"master_increase", resize_master_add},
{"master_decrease", resize_master_sub},
+ {"stack_increase", resize_stack_add},
+ {"stack_decrease", resize_stack_sub},
{"toggle_floating", toggle_floating},
{"global_floating", toggle_floating_global},
{"fullscreen", toggle_fullscreen},
@@ -132,7 +135,7 @@ int parser(Config *cfg)
return -1;
}
- // Determine config file path
+ /* determine config file path */
const char *xdg_config_home = getenv("XDG_CONFIG_HOME");
if (xdg_config_home) {
snprintf(path, sizeof path, "%s/sxwmrc", xdg_config_home);
@@ -156,7 +159,7 @@ int parser(Config *cfg)
goto found;
}
- // Nothing found
+ /* Nothing found */
fprintf(stderr, "sxwmrc: no configuration file found\n");
return -1;
@@ -173,7 +176,7 @@ found:
int should_floatn = 0;
int torun = 0;
- // Initialize should_float matrix
+ /* Initialize should_float matrix */
for (int j = 0; j < 256; j++) {
cfg->should_float[j] = calloc(256, sizeof(char *));
if (!cfg->should_float[j]) {
@@ -202,7 +205,7 @@ found:
if (!strcmp(key, "mod_key")) {
unsigned m = parse_mods(rest, cfg);
- if (m & (Mod1Mask | Mod4Mask)) {
+ if (m & (Mod1Mask | Mod4Mask | ShiftMask | ControlMask)) {
cfg->modkey = m;
}
else {
@@ -232,6 +235,14 @@ found:
cfg->new_win_focus = False;
}
}
+ else if (!strcmp(key, "warp_cursor")) {
+ if (!strcmp(rest, "true")) {
+ cfg->warp_cursor = True;
+ }
+ else {
+ cfg->warp_cursor = False;
+ }
+ }
else if (!strcmp(key, "master_width")) {
float mf = (float)atoi(rest) / 100.0f;
for (int i = 0; i < MAX_MONITORS; i++) {
@@ -244,6 +255,9 @@ found:
else if (!strcmp(key, "resize_master_amount")) {
cfg->resize_master_amt = atoi(rest);
}
+ else if (!strcmp(key, "resize_stack_amount")) {
+ cfg->resize_stack_amt = atoi(rest);
+ }
else if (!strcmp(key, "snap_distance")) {
cfg->snap_distance = atoi(rest);
}
@@ -266,15 +280,16 @@ found:
/* store each comma separated value in a seperate row */
while (comma && should_floatn < 256) {
comma = strip(comma);
- if (*comma == '"')
+ if (*comma == '"') {
comma++;
+ }
char *end = comma + strlen(comma) - 1;
- if (*end == '"')
+ if (*end == '"') {
*end = '\0';
+ }
/* store each programs name in its own row at index 0 */
cfg->should_float[should_floatn][0] = strdup(comma);
- printf("DEBUG: should_float[%d][0] = '%s'\n", should_floatn, cfg->should_float[should_floatn][0]);
should_floatn++;
comma = strtok_r(NULL, ",", &comma_ptr);
}
@@ -305,7 +320,12 @@ found:
if (*act == '"' && !strcmp(key, "bind")) {
b->type = TYPE_CMD;
b->action.cmd = build_argv(strip_quotes(act));
+ if (!b->action.cmd) {
+ fprintf(stderr, "sxwmrc:%d: failed to parse command: %s\n", lineno, act);
+ b->type = -1;
+ }
}
+
else {
b->type = TYPE_FUNC;
Bool found = False;
@@ -445,39 +465,23 @@ KeySym parse_keysym(const char *key)
const char **build_argv(const char *cmd)
{
- char *dup = strdup(cmd);
- char *saveptr = NULL;
- const char **argv = malloc(MAX_ARGS * sizeof(*argv));
- int i = 0;
-
- char *tok = strtok_r(dup, " \t", &saveptr);
- while (tok && i < MAX_ARGS - 1) {
- if (*tok == '"') {
- char *end = tok + strlen(tok) - 1;
- if (*end == '"') {
- *end = '\0';
- argv[i++] = strdup(tok + 1);
- }
- else {
- char *quoted = strdup(tok + 1);
- while ((tok = strtok_r(NULL, " \t", &saveptr)) && *tok != '"') {
- quoted = realloc(quoted, strlen(quoted) + strlen(tok) + 2);
- strcat(quoted, " ");
- strcat(quoted, tok);
- }
- if (tok && *tok == '"') {
- quoted = realloc(quoted, strlen(quoted) + strlen(tok));
- strcat(quoted, tok);
- }
- argv[i++] = quoted;
- }
- }
- else {
- argv[i++] = strdup(tok);
- }
- tok = strtok_r(NULL, " \t", &saveptr);
+ wordexp_t p;
+ if (wordexp(cmd, &p, 0) != 0 || p.we_wordc == 0) {
+ fprintf(stderr, "sxwm: wordexp failed for cmd: '%s'\n", cmd);
+ return NULL;
+ }
+
+ const char **argv = malloc((p.we_wordc + 1) * sizeof(char *));
+ if (!argv) {
+ wordfree(&p);
+ return NULL;
}
- argv[i] = NULL;
- free(dup);
+
+ for (size_t i = 0; i < p.we_wordc; i++) {
+ argv[i] = strdup(p.we_wordv[i]);
+ }
+ argv[p.we_wordc] = NULL;
+
+ wordfree(&p);
return argv;
}
diff --git a/src/sxwm.c b/src/sxwm.c
index 7de1a28..bc6f922 100644
--- a/src/sxwm.c
+++ b/src/sxwm.c
@@ -52,7 +52,6 @@ void hdl_config_ntf(XEvent *xev);
void hdl_config_req(XEvent *xev);
void hdl_dummy(XEvent *xev);
void hdl_destroy_ntf(XEvent *xev);
-void hdl_enter(XEvent *xev);
void hdl_keypress(XEvent *xev);
void hdl_map_req(XEvent *xev);
void hdl_motion(XEvent *xev);
@@ -70,6 +69,8 @@ int other_wm_err(Display *dpy, XErrorEvent *ee);
/* void reload_config(void); */
/* void resize_master_add(void); */
/* void resize_master_sub(void); */
+/* void resize_stack_add(void); */
+/* void resize_stack_sub(void); */
void run(void);
void scan_existing_windows(void);
void send_wm_take_focus(Window w);
@@ -86,6 +87,7 @@ void update_borders(void);
void update_monitors(void);
void update_net_client_list(void);
void update_struts(void);
+void warp_cursor(Client *c);
int xerr(Display *dpy, XErrorEvent *ee);
void xev_case(XEvent *xev);
#include "config.h"
@@ -122,6 +124,7 @@ Monitor *mons = NULL;
int monsn = 0;
Bool global_floating = False;
Bool in_ws_switch = False;
+Bool running = False;
long last_motion_time = 0;
int scr_width;
@@ -154,8 +157,9 @@ Client *add_client(Window w, int ws)
}
else {
Client *tail = workspaces[ws];
- while (tail->next)
+ while (tail->next) {
tail = tail->next;
+ }
tail->next = c;
}
@@ -194,6 +198,7 @@ Client *add_client(Window w, int ws)
c->floating = False;
c->fullscreen = False;
c->mapped = True;
+ c->custom_stack_height = 0;
if (global_floating) {
c->floating = True;
@@ -209,30 +214,36 @@ Client *add_client(Window w, int ws)
void change_workspace(int ws)
{
- if (ws >= NUM_WORKSPACES || ws == current_ws)
+ if (ws >= NUM_WORKSPACES || ws == current_ws) {
return;
+ }
in_ws_switch = True;
XGrabServer(dpy);
/* unmap those still marked mapped */
for (Client *c = workspaces[current_ws]; c; c = c->next) {
- if (c->mapped)
+ if (c->mapped) {
XUnmapWindow(dpy, c->win);
+ }
}
current_ws = ws;
/* map those still marked mapped */
for (Client *c = workspaces[current_ws]; c; c = c->next) {
- if (c->mapped)
+ if (c->mapped) {
XMapWindow(dpy, c->win);
+ }
}
tile();
if (workspaces[current_ws]) {
focused = workspaces[current_ws];
XSetInputFocus(dpy, focused->win, RevertToPointerRoot, CurrentTime);
+ if (user_config.warp_cursor) {
+ warp_cursor(focused);
+ }
}
long cd = current_ws;
@@ -258,7 +269,7 @@ void close_focused(void)
Atom *protos;
int n;
if (XGetWMProtocols(dpy, focused->win, &protos, &n) && protos) {
- for (int i = 0; i < n; i++)
+ for (int i = 0; i < n; i++) {
if (protos[i] == atom_wm_delete) {
XEvent ev = {.xclient = {.type = ClientMessage,
.window = focused->win,
@@ -270,6 +281,7 @@ void close_focused(void)
XFree(protos);
return;
}
+ }
XUnmapWindow(dpy, focused->win);
XFree(protos);
}
@@ -295,6 +307,9 @@ void focus_next(void)
focused = (focused->next ? focused->next : workspaces[current_ws]);
XSetInputFocus(dpy, focused->win, RevertToPointerRoot, CurrentTime);
XRaiseWindow(dpy, focused->win);
+ if (user_config.warp_cursor) {
+ warp_cursor(focused);
+ }
update_borders();
}
@@ -311,8 +326,9 @@ void focus_prev(void)
}
if (!prev) {
- while (p->next)
+ while (p->next) {
p = p->next;
+ }
focused = p;
}
else {
@@ -321,6 +337,9 @@ void focus_prev(void)
XSetInputFocus(dpy, focused->win, RevertToPointerRoot, CurrentTime);
XRaiseWindow(dpy, focused->win);
+ if (user_config.warp_cursor) {
+ warp_cursor(focused);
+ }
update_borders();
}
@@ -328,8 +347,9 @@ int get_monitor_for(Client *c)
{
int cx = c->x + c->w / 2, cy = c->y + c->h / 2;
for (int i = 0; i < monsn; i++) {
- 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)
+ 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) {
return i;
+ }
}
return 0;
}
@@ -350,15 +370,18 @@ void grab_keys(void)
Binding *b = &user_config.binds[i];
if ((b->type == TYPE_CWKSP && b->mods != user_config.modkey) ||
- (b->type == TYPE_MWKSP && b->mods != (user_config.modkey | ShiftMask)))
+ (b->type == TYPE_MWKSP && b->mods != (user_config.modkey | ShiftMask))) {
continue;
+ }
KeyCode kc = XKeysymToKeycode(dpy, b->keysym);
- if (!kc)
+ if (!kc) {
continue;
+ }
- for (size_t g = 0; g < sizeof guards / sizeof *guards; g++)
+ for (size_t g = 0; g < sizeof guards / sizeof *guards; g++) {
XGrabKey(dpy, kc, b->mods | guards[g], root, True, GrabModeAsync, GrabModeAsync);
+ }
}
}
@@ -506,11 +529,13 @@ void hdl_config_req(XEvent *xev)
XConfigureRequestEvent *e = &xev->xconfigurerequest;
Client *c = NULL;
- for (int ws = 0; ws < NUM_WORKSPACES && !c; ws++)
- for (c = workspaces[ws]; c; c = c->next)
+ for (int ws = 0; ws < NUM_WORKSPACES && !c; ws++) {
+ for (c = workspaces[ws]; c; c = c->next) {
if (c->win == e->window) {
break;
}
+ }
+ }
if (!c || c->floating || c->fullscreen) {
/* allow client to configure itself */
@@ -585,21 +610,6 @@ void hdl_destroy_ntf(XEvent *xev)
}
}
-void hdl_enter(XEvent *xev)
-{
- Window w = xev->xcrossing.window;
-
- Client *head = workspaces[current_ws];
- for (Client *c = head; c; c = c->next) {
- if (c->win == w) {
- focused = c;
- XSetInputFocus(dpy, w, RevertToPointerRoot, CurrentTime);
- update_borders();
- break;
- }
- }
-}
-
void hdl_keypress(XEvent *xev)
{
KeySym ks = XkbKeycodeToKeysym(dpy, xev->xkey.keycode, 0, 0);
@@ -614,8 +624,9 @@ void hdl_keypress(XEvent *xev)
break;
case TYPE_FUNC:
- if (b->action.fn)
+ if (b->action.fn) {
b->action.fn();
+ }
break;
case TYPE_CWKSP:
change_workspace(b->action.ws);
@@ -640,10 +651,12 @@ void swap_clients(Client *a, Client *b)
Client **head = &workspaces[current_ws];
Client **pa = head, **pb = head;
- while (*pa && *pa != a)
+ while (*pa && *pa != a) {
pa = &(*pa)->next;
- while (*pb && *pb != b)
+ }
+ while (*pb && *pb != b) {
pb = &(*pb)->next;
+ }
if (!*pa || !*pb) {
return;
@@ -726,12 +739,14 @@ void hdl_map_req(XEvent *xev)
}
Client *c = add_client(w, current_ws);
- if (!c)
+ if (!c) {
return;
+ }
Window tr;
- if (!should_float && XGetTransientForHint(dpy, w, &tr))
+ if (!should_float && XGetTransientForHint(dpy, w, &tr)) {
should_float = True;
+ }
XSizeHints sh;
long sup;
if (!should_float && XGetWMNormalHints(dpy, w, &sh, &sup) && (sh.flags & PMinSize) && (sh.flags & PMaxSize) &&
@@ -760,22 +775,27 @@ void hdl_map_req(XEvent *xev)
/* map & borders */
update_net_client_list();
- if (!global_floating && !c->floating)
+ if (!global_floating && !c->floating) {
tile();
- else if (c->floating)
+ }
+ else if (c->floating) {
XRaiseWindow(dpy, w);
+ }
+ XMapWindow(dpy, w);
+ for (Client *c = workspaces[current_ws]; c; c = c->next) {
+ if (c->win == w) {
+ c->mapped = True;
+ }
+ }
if (user_config.new_win_focus) {
focused = c;
XSetInputFocus(dpy, c->win, RevertToPointerRoot, CurrentTime);
send_wm_take_focus(c->win);
+ if (user_config.warp_cursor) {
+ warp_cursor(c);
+ }
}
-
- XMapWindow(dpy, w);
- for (Client *c = workspaces[current_ws]; c; c = c->next)
- if (c->win == w)
- c->mapped = True;
-
update_borders();
}
@@ -913,8 +933,9 @@ void update_struts(void)
Window root_ret, parent_ret, *children;
unsigned int nchildren;
- if (!XQueryTree(dpy, root, &root_ret, &parent_ret, &children, &nchildren))
+ if (!XQueryTree(dpy, root, &root_ret, &parent_ret, &children, &nchildren)) {
return;
+ }
for (unsigned int i = 0; i < nchildren; i++) {
Window w = children[i];
@@ -926,8 +947,9 @@ void update_struts(void)
if (XGetWindowProperty(dpy, w, atom_wm_window_type, 0, 4, False, XA_ATOM, &actual_type, &actual_format, &nitems,
&bytes_after, (unsigned char **)&types) != Success ||
- !types)
+ !types) {
continue;
+ }
Bool is_dock = False;
for (unsigned long j = 0; j < nitems; j++) {
@@ -937,8 +959,9 @@ void update_struts(void)
}
}
XFree(types);
- if (!is_dock)
+ if (!is_dock) {
continue;
+ }
long *str = NULL;
Atom actual;
@@ -981,14 +1004,17 @@ void init_defaults(void)
default_config.border_foc_col = parse_col("#c0cbff");
default_config.border_ufoc_col = parse_col("#555555");
default_config.border_swap_col = parse_col("#fff4c0");
- for (int i = 0; i < MAX_MONITORS; i++)
+ for (int i = 0; i < MAX_MONITORS; i++) {
default_config.master_width[i] = 50 / 100.0f;
+ }
default_config.motion_throttle = 60;
default_config.resize_master_amt = 5;
+ default_config.resize_stack_amt = 20;
default_config.snap_distance = 5;
default_config.bindsn = 0;
default_config.new_win_focus = True;
+ default_config.warp_cursor = True;
for (unsigned long i = 0; i < LENGTH(binds); i++) {
default_config.binds[i].mods = binds[i].mods;
@@ -1006,7 +1032,10 @@ void move_master_next(void)
if (!workspaces[current_ws] || !workspaces[current_ws]->next) {
return;
}
+
Client *first = workspaces[current_ws];
+ Client *old_focused = focused;
+
workspaces[current_ws] = first->next;
first->next = NULL;
@@ -1017,6 +1046,12 @@ void move_master_next(void)
tail->next = first;
tile();
+ if (user_config.warp_cursor && old_focused) {
+ warp_cursor(old_focused);
+ }
+ if (old_focused) {
+ send_wm_take_focus(old_focused->win);
+ }
update_borders();
}
@@ -1025,15 +1060,29 @@ void move_master_prev(void)
if (!workspaces[current_ws] || !workspaces[current_ws]->next) {
return;
}
+
Client *prev = NULL, *cur = workspaces[current_ws];
+ Client *old_focused = focused;
+
while (cur->next) {
prev = cur;
cur = cur->next;
}
- prev->next = NULL;
+
+ if (prev) {
+ prev->next = NULL;
+ }
+
cur->next = workspaces[current_ws];
workspaces[current_ws] = cur;
+
tile();
+ if (user_config.warp_cursor && old_focused) {
+ warp_cursor(old_focused);
+ }
+ if (old_focused) {
+ send_wm_take_focus(old_focused->win);
+ }
update_borders();
}
@@ -1052,8 +1101,9 @@ void move_to_workspace(int ws)
XUnmapWindow(dpy, focused->win);
/* remove from current list */
Client **pp = &workspaces[current_ws];
- while (*pp && *pp != focused)
+ while (*pp && *pp != focused) {
pp = &(*pp)->next;
+ }
if (*pp) {
*pp = focused->next;
}
@@ -1121,14 +1171,14 @@ void quit(void)
XFreeCursor(dpy, c_move);
XFreeCursor(dpy, c_normal);
XFreeCursor(dpy, c_resize);
- errx(0, "quitting...");
+ printf("quitting...\n");
+ running = False;
}
void reload_config(void)
{
puts("sxwm: reloading config...");
memset(&user_config, 0, sizeof(user_config));
-
for (int i = 0; i < user_config.bindsn; i++) {
free(user_config.binds[i].action.cmd);
user_config.binds[i].action.cmd = NULL;
@@ -1184,10 +1234,44 @@ void resize_master_sub(void)
update_borders();
}
+void resize_stack_add(void)
+{
+ if (!focused || focused->floating || focused == workspaces[current_ws]) {
+ return;
+ }
+
+ int bw2 = 2 * user_config.border_width;
+ int raw_cur = (focused->custom_stack_height > 0) ? focused->custom_stack_height : (focused->h + bw2);
+
+ int raw_new = raw_cur + user_config.resize_stack_amt;
+ focused->custom_stack_height = raw_new;
+ tile();
+}
+
+void resize_stack_sub(void)
+{
+ if (!focused || focused->floating || focused == workspaces[current_ws]) {
+ return;
+ }
+
+ int bw2 = 2 * user_config.border_width;
+ int raw_cur = (focused->custom_stack_height > 0) ? focused->custom_stack_height : (focused->h + bw2);
+
+ int raw_new = raw_cur - user_config.resize_stack_amt;
+ int min_raw = bw2 + 1;
+
+ if (raw_new < min_raw) {
+ raw_new = min_raw;
+ }
+ focused->custom_stack_height = raw_new;
+ tile();
+}
+
void run(void)
{
+ running = True;
XEvent xev;
- for (;;) {
+ while (running) {
XNextEvent(dpy, &xev);
xev_case(&xev);
}
@@ -1310,7 +1394,6 @@ void setup(void)
evtable[ConfigureNotify] = hdl_config_ntf;
evtable[ConfigureRequest] = hdl_config_req;
evtable[DestroyNotify] = hdl_destroy_ntf;
- evtable[EnterNotify] = hdl_enter;
evtable[KeyPress] = hdl_keypress;
evtable[MapRequest] = hdl_map_req;
evtable[MotionNotify] = hdl_motion;
@@ -1385,13 +1468,8 @@ Bool window_should_float(Window w)
break;
}
- printf("[DEBUG] Checking window class '%s' and instance '%s' against should_float[%d][0] = '%s'\n",
- ch.res_class ? ch.res_class : "NULL", ch.res_name ? ch.res_name : "NULL", i,
- user_config.should_float[i][0]);
-
if ((ch.res_class && !strcmp(ch.res_class, user_config.should_float[i][0])) ||
(ch.res_name && !strcmp(ch.res_name, user_config.should_float[i][0]))) {
- printf("[DEBUG] Window should float based on class/instance match\n");
XFree(ch.res_class);
XFree(ch.res_name);
return True;
@@ -1460,95 +1538,201 @@ void spawn(const char **argv)
void tile(void)
{
- update_struts(); /* fills reserve_top, reserve_bottom */
-
+ update_struts();
Client *head = workspaces[current_ws];
int total = 0;
for (Client *c = head; c; c = c->next) {
- if (!c->mapped || c->floating || c->fullscreen)
- continue;
- total++;
+ if (c->mapped && !c->floating && !c->fullscreen) {
+ total++;
+ }
}
if (total == 1) {
for (Client *c = head; c; c = c->next) {
- if (!c->floating && c->fullscreen)
+ if (!c->floating && c->fullscreen) {
return;
+ }
}
}
for (int m = 0; m < monsn; m++) {
- int mon_x = mons[m].x;
- int mon_y = mons[m].y + reserve_top;
- int mon_w = mons[m].w;
- int mon_h = mons[m].h - reserve_top - reserve_bottom;
+ int mon_x = mons[m].x, mon_y = mons[m].y + reserve_top;
+ int mon_w = mons[m].w, mon_h = mons[m].h - reserve_top - reserve_bottom;
- int cnt = 0;
+ Client *stackers[MAXCLIENTS];
+ int N = 0;
for (Client *c = head; c; c = c->next) {
- if (!c->mapped || c->floating || c->fullscreen || c->mon != m)
- continue;
- cnt++;
+ if (c->mapped && !c->floating && !c->fullscreen && c->mon == m) {
+ stackers[N++] = c;
+ }
}
- if (!cnt)
+
+ if (N == 0) {
continue;
+ }
- int gx = user_config.gaps;
- int gy = user_config.gaps;
- int tile_x = mon_x + gx;
- int tile_y = mon_y + gy;
- int tile_w = mon_w - 2 * gx;
- int tile_h = mon_h - 2 * gy;
- if (tile_w < 1)
- tile_w = 1;
- if (tile_h < 1)
- tile_h = 1;
-
- /* master‐stack split */
- float mf = user_config.master_width[m];
- if (mf < MF_MIN)
- mf = MF_MIN;
- if (mf > MF_MAX)
- mf = MF_MAX;
- int master_w = (cnt > 1) ? (int)(tile_w * mf) : tile_w;
- int stack_w = tile_w - master_w - ((cnt > 1) ? gx : 0);
- int stack_h = (cnt > 1) ? (tile_h - (cnt - 1) * gy) / (cnt - 1) : 0;
-
- int i = 0, sy = tile_y;
- for (Client *c = head; c; c = c->next) {
- if (!c->mapped || c->floating || c->fullscreen || c->mon != m)
- continue;
+ int gx = user_config.gaps, gy = user_config.gaps;
+ int tile_x = mon_x + gx, tile_y = mon_y + gy;
+ int tile_w = MAX(1, mon_w - 2 * gx);
+ int tile_h = MAX(1, mon_h - 2 * gy);
+ float mf = CLAMP(user_config.master_width[m], MF_MIN, MF_MAX);
+ int master_w = (N > 1) ? (int)(tile_w * mf) : tile_w;
+ int stack_w = (N > 1) ? (tile_w - master_w - gx) : 0;
- XWindowChanges wc = {.border_width = user_config.border_width};
+ {
+ Client *c = stackers[0];
int bw2 = 2 * user_config.border_width;
+ XWindowChanges wc = {.x = tile_x,
+ .y = tile_y,
+ .width = MAX(1, master_w - bw2),
+ .height = MAX(1, tile_h - bw2),
+ .border_width = user_config.border_width};
+ XConfigureWindow(dpy, c->win, CWX | CWY | CWWidth | CWHeight | CWBorderWidth, &wc);
+ c->x = wc.x;
+ c->y = wc.y;
+ c->w = wc.width;
+ c->h = wc.height;
+ }
+
+ if (N == 1) {
+ update_borders();
+ continue;
+ }
- if (i == 0) {
- /* master */
- wc.x = tile_x;
- wc.y = tile_y;
- wc.width = MAX(1, master_w - bw2);
- wc.height = MAX(1, tile_h - bw2);
+ int num_stack = N - 1;
+ int idx_focus = -1;
+ for (int i = 1; i < N; i++) {
+ if (stackers[i] == focused) {
+ idx_focus = i;
+ }
+ }
+
+ Bool is_fixed[MAXCLIENTS] = {0};
+ int bw2 = 2 * user_config.border_width;
+ for (int i = 1; i < N; i++) {
+ if (stackers[i]->custom_stack_height > 0) {
+ is_fixed[i] = True;
+ }
+ }
+ if (idx_focus >= 1 && stackers[idx_focus]->custom_stack_height > 0) {
+ is_fixed[idx_focus] = True;
+ }
+
+ int total_fixed_heights = 0;
+ for (int i = 1; i < N; i++) {
+ if (!is_fixed[i]) {
+ continue;
+ }
+ int h = stackers[i]->custom_stack_height > 0 ? stackers[i]->custom_stack_height : stackers[i]->h + bw2;
+ total_fixed_heights += h;
+ }
+
+ int auto_count = 0;
+ for (int i = 1; i < N; i++) {
+ if (!is_fixed[i]) {
+ auto_count++;
+ }
+ }
+
+ int total_vgaps = (num_stack - 1) * gy;
+ int remaining = tile_h - total_fixed_heights - total_vgaps;
+ int min_raw = bw2 + 1;
+ int heights_final[MAXCLIENTS] = {0};
+
+ if (auto_count > 0) {
+ if (remaining >= auto_count * min_raw) {
+
+ int auto_h = remaining / auto_count, used = 0, count = 0;
+ for (int i = 1; i < N; i++) {
+ if (!is_fixed[i]) {
+ count++;
+ heights_final[i] = (count < auto_count) ? auto_h : remaining - used;
+ used += auto_h;
+ }
+ }
+ for (int i = 1; i < N; i++) {
+ if (is_fixed[i]) {
+ heights_final[i] = stackers[i]->custom_stack_height > 0 ? stackers[i]->custom_stack_height
+ : stackers[i]->h + bw2;
+ }
+ }
}
else {
- /* stack */
- wc.x = tile_x + master_w + gx;
- wc.y = sy;
- int h = (i == cnt - 1) ? (tile_y + tile_h - sy) : stack_h;
- wc.width = MAX(1, stack_w - bw2);
- wc.height = MAX(1, h - bw2);
- sy += h + gy;
+ for (int i = 1; i < N; i++) {
+ heights_final[i] = is_fixed[i]
+ ? (stackers[i]->custom_stack_height > 0 ? stackers[i]->custom_stack_height
+ : stackers[i]->h + bw2)
+ : min_raw;
+ }
+ }
+ }
+ else {
+ int sum_raw = 0;
+ for (int i = 1; i < N; i++) {
+ sum_raw +=
+ stackers[i]->custom_stack_height > 0 ? stackers[i]->custom_stack_height : stackers[i]->h + bw2;
+ }
+ int remaining_slack = tile_h - total_vgaps - sum_raw;
+ for (int i = 1; i < N; i++) {
+ int base_h =
+ stackers[i]->custom_stack_height > 0 ? stackers[i]->custom_stack_height : stackers[i]->h + bw2;
+
+ /* only grow the bottom window if it isn’t fixed */
+ if (i == N - 1 && remaining_slack > 0 && stackers[i]->custom_stack_height == 0) {
+ heights_final[i] = base_h + remaining_slack;
+ }
+ else {
+ heights_final[i] = base_h;
+ }
+ }
+ }
+
+ int total_height = total_vgaps;
+ for (int i = 1; i < N; i++) {
+ total_height += heights_final[i];
+ }
+
+ int overfill = total_height - tile_h;
+ if (overfill > 0) {
+ /* shrink from top down, excluding bottom */
+ for (int i = 1; i < N - 1 && overfill > 0; i++) {
+ int shrink = MIN(overfill, heights_final[i] - min_raw);
+ heights_final[i] -= shrink;
+ overfill -= shrink;
}
+ }
+
+ /* if its not perfectly filled stretch bottom to absorb remainder */
+ int actual_stack_height = total_vgaps;
+ for (int i = 1; i < N; i++) {
+ actual_stack_height += heights_final[i];
+ }
+
+ int shortfall = tile_h - actual_stack_height;
+ if (shortfall > 0) {
+ heights_final[N - 1] += shortfall;
+ }
+ int sy = tile_y;
+ int bw = user_config.border_width;
+ for (int i = 1; i < N; i++) {
+ Client *c = stackers[i];
+ XWindowChanges wc = {.x = tile_x + master_w + gx,
+ .y = sy,
+ .width = MAX(1, stack_w - (2 * bw)),
+ .height = MAX(1, heights_final[i] - (2 * bw)),
+ .border_width = bw};
XConfigureWindow(dpy, c->win, CWX | CWY | CWWidth | CWHeight | CWBorderWidth, &wc);
c->x = wc.x;
c->y = wc.y;
c->w = wc.width;
c->h = wc.height;
- i++;
+ sy += heights_final[i] + gy;
}
- }
- update_borders();
+ update_borders();
+ }
}
void toggle_floating(void)
@@ -1728,6 +1912,19 @@ void update_net_client_list(void)
XChangeProperty(dpy, root, prop, XA_WINDOW, 32, PropModeReplace, (unsigned char *)wins, n);
}
+void warp_cursor(Client *c)
+{
+ if (!c) {
+ return;
+ }
+
+ int center_x = c->x + (c->w / 2);
+ int center_y = c->y + (c->h / 2);
+
+ XWarpPointer(dpy, None, root, 0, 0, 0, 0, center_x, center_y);
+ XSync(dpy, False);
+}
+
int xerr(Display *dpy, XErrorEvent *ee)
{
/* ignore noise & non fatal errors */
diff --git a/sxwm.1 b/sxwm.1
index 646c7d4..323905d 100644
--- a/sxwm.1
+++ b/sxwm.1
@@ -1,4 +1,4 @@
-.TH SXWM 1 "May 2025" "sxwm 1.5" "User Commands"
+.TH sxwm 1 "June 2025" "sxwm 1.6" "User Commands"
.SH NAME
sxwm \- minimal, fast, and configurable tiling window manager for X11
@@ -15,7 +15,7 @@ Nine workspaces with full bar support.
Live configuration reload without restart.
Human-friendly configuration file requiring no recompilation.
DWM-style master-stack layout.
-Mouse support for moving, resizing, focusing, and swapping windows.
+Mouse support for moving, resizing, and swapping windows.
Depends only on libX11 and Xinerama.
Extremely lightweight (single C file).
Multi-monitor support via Xinerama.
@@ -73,6 +73,14 @@ Target updates per second for mouse drag operations (move, resize, swap). Defaul
.B should_float
Lets you change which windows should float by default when opening them. Default is st
+.TP
+.B new_win_focus
+Whether newly opened windows should automatically gain focus. Default is true.
+
+.TP
+.B warp_cursor
+If true, warps the mouse cursor to the center of newly focused windows. Default is true.
+
.SH KEYBINDINGS
Keybindings associate key combinations with actions, either running external commands or internal sxwm functions.
@@ -155,6 +163,14 @@ Increases the width allocated to the master area.
Decreases the width allocated to the master area.
.TP
+.B stack_increase
+Increases the height of stack window.
+
+.TP
+.B stack_decrease
+Decreases the height of stack window.
+
+.TP
.B toggle_floating
Toggles the floating state of the focused window.
@@ -210,6 +226,14 @@ Focus next or previous window.
Move window up or down in the master stack.
.TP
+.B MOD + Shift + h / l
+Increase or decrease master size.
+
+.TP
+.B MOD + Ctrl h / l
+Increase or decrease stack height.
+
+.TP
.B MOD + Space
Toggle floating mode for focused window.
diff --git a/sxwm.desktop b/sxwm.desktop
index f841c2b..ef08338 100644
--- a/sxwm.desktop
+++ b/sxwm.desktop
@@ -1,5 +1,5 @@
[Desktop Entry]
-Name=Sxwm
+Name=sxwm
Comment=Simple Xorg Window Manager
Exec=sxwm
TryExec=sxwm