diff options
| -rw-r--r-- | CHANGELOG.md | 23 | ||||
| -rw-r--r-- | CONTRIBUTIONS.md | 90 | ||||
| -rw-r--r-- | README.md | 14 | ||||
| -rw-r--r-- | default_sxwmrc | 8 | ||||
| -rw-r--r-- | images/1.png | bin | 234882 -> 0 bytes | |||
| -rw-r--r-- | images/3.png | bin | 1629407 -> 0 bytes | |||
| -rw-r--r-- | images/4.png | bin | 1538304 -> 0 bytes | |||
| -rw-r--r-- | images/sxwm_logo.psd | bin | 477842 -> 0 bytes | |||
| -rw-r--r-- | images/x.png | bin | 875729 -> 0 bytes | |||
| -rw-r--r-- | src/config.h | 3 | ||||
| -rw-r--r-- | src/defs.h | 9 | ||||
| -rw-r--r-- | src/parser.c | 88 | ||||
| -rw-r--r-- | src/sxwm.c | 423 | ||||
| -rw-r--r-- | sxwm.1 | 28 | ||||
| -rw-r--r-- | sxwm.desktop | 2 |
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!** @@ -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 Binary files differdeleted file mode 100644 index 7f742d4..0000000 --- a/images/1.png +++ /dev/null diff --git a/images/3.png b/images/3.png Binary files differdeleted file mode 100644 index dba0bca..0000000 --- a/images/3.png +++ /dev/null diff --git a/images/4.png b/images/4.png Binary files differdeleted file mode 100644 index aac1783..0000000 --- a/images/4.png +++ /dev/null diff --git a/images/sxwm_logo.psd b/images/sxwm_logo.psd Binary files differdeleted file mode 100644 index d8d90e2..0000000 --- a/images/sxwm_logo.psd +++ /dev/null diff --git a/images/x.png b/images/x.png Binary files differdeleted file mode 100644 index a5b8f8a..0000000 --- a/images/x.png +++ /dev/null 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}, @@ -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; } @@ -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 */ @@ -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 |
