diff options
| -rw-r--r-- | CHANGELOG.md | 4 | ||||
| -rw-r--r-- | README.md | 113 | ||||
| -rw-r--r-- | default_sxwmrc | 20 | ||||
| -rw-r--r-- | src/config.h | 54 | ||||
| -rw-r--r-- | src/defs.h | 73 | ||||
| -rw-r--r-- | src/parser.c | 126 | ||||
| -rw-r--r-- | src/sxwm.c | 451 | ||||
| -rw-r--r-- | sxwm.1 | 207 |
8 files changed, 907 insertions, 141 deletions
diff --git a/CHANGELOG.md b/CHANGELOG.md index b8d5d86..35d7461 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,10 @@ All notable changes to this project will be documented in this file. - **NEW**: Can switch monitors via keyboard - **NEW**: Can move windows between monitors via keyboard - **NEW**: Can click on a window to set focus to it +- **NEW**: Window swallowing +- **NEW**: New windows can now open as master window +- **NEW**: Scratchpads +- **NEW**: Window centering - **CHANGE**: Renamed `focus_previous` to `focus_prev` - **CHANGE**: Invalid sample config - **CHANGE**: Parser `$HOME` searching order. XDG Compliance @@ -40,6 +40,8 @@ Allows user to use backup keybinds with `sxwm` - **Tiling & Floating**: Switch seamlessly between layouts. - **Workspaces**: 9 workspaces, fully integrated with your bar. +- **Scratchpads**: Floating windows you can summon/hide instantly. +- **Window Swallowing**: Native window swallowing support. - **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. @@ -75,11 +77,16 @@ The file uses a `key : value` format. Lines starting with `#` are ignored. | `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. | +| `resize_stack_amount` | Integer | `20` | How many pixels to increase/decrease stack windows by | | `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 | +| `exec` | String | `Nothing` | Command to run on startup (e.g., `sxbar`, `picom`, "autostart", etc.). | +| `can_swallow` | String | `st` | Windows that can swallow. | +| `can_be_swallowed` | String | `mpv` | Windows that can be swallowed. | +| `new_win_master` | Bool | `false` | New windows will open as master window. | --- @@ -87,23 +94,30 @@ The file uses a `key : value` format. Lines starting with `#` are ignored. ### Syntax +- **Modifiers**: `mod`, `shift`, `ctrl`, `alt`, `super` +- **Key**: Case-insensitive keysym (e.g., `Return`, `q`, `1`) +- **Action**: Either an external command (in quotes) or internal function. +- **move**: Move to that worspace +- **swap**: Swap window to that workspace +- **n**: Workspace / Scratchpad number +- **create**: Creates a scratchpad on that slot +- **toggle**: toggles the visibility of that scratchpad +- **remove**: Removes the scratchpad on that slot + ```sh bind : modifier + modifier + ... + key : action ``` -- **Modifiers**: `mod`, `shift`, `ctrl`, `alt`, `super` -- **Key**: Case-insensitive keysym (e.g., `Return`, `q`, `1`) -- **Action**: Either an external command (in quotes) or internal function. +```sh +scratchpad : modifier + ... + key : create n +scratchpad : modifier + ... + key : toggle n +scratchpad : modifier + ... + key : remove n +``` ```sh workspace : modifier + modifier + ... + key : move n workspace : modifier + modifier + ... + key : swap n ``` -- **Modifiers**: `mod`, `shift`, `ctrl`, `alt`, `super` -- **Key**: Case-insensitive keysym (e.g., `Return`, `q`, `1`) -- **move**: Move to that worspace -- **swap**: Swap window to that workspace -- **n**: Workspace number ### Available Functions @@ -113,10 +127,10 @@ workspace : modifier + modifier + ... + key : swap n | `decrease_gaps` | Shrinks gaps. | | `focus_next` | Moves focus forward in the stack. | | `focus_previous` | Moves focus backward in the stack. | -| `focus_next_mon` | Switches focus to the next monitor. | -| `focus_prev_mon` | Switches focus to the previous monitor. | -| `move_next_mon` | Moves the focused window to the next monitor. | -| `move_prev_mon` | Moves the focused window to the previous monitor. | +| `focus_next_mon` | Switches focus to the next monitor. | +| `focus_prev_mon` | Switches focus to the previous monitor. | +| `move_next_mon` | Moves the focused window to the next monitor. | +| `move_prev_mon` | Moves the focused window to the previous monitor. | | `increase_gaps` | Expands gaps. | | `master_next` | Moves focused window down in master/stack order. | | `master_prev` | Moves focused window up in master/stack order. | @@ -126,7 +140,8 @@ workspace : modifier + modifier + ... + key : swap n | `master_decrease` | Shrinks master width. | | `toggle_floating` | Toggles floating state of current window. | | `global_floating` | Toggles floating state for all windows. | -| `fullscreen` | Fullscreen toggle. | +| `fullscreen` | Fullscreen the focused window. | +| `centre_window` | Centre the focused window. | ### Example Bindings @@ -136,6 +151,11 @@ bind : mod + Return : "st" # Close window bind : mod + shift + q : close_window +# Scratchpads +scratchpad : mod + ctrl + Return : create 1 +scratchpad : mod + shift + b : toggle 2 +scratchpad : mod + alt + b : remove 2 + # Switch workspace workspace : mod + 3 : move 3 # Move window to workspace @@ -147,31 +167,48 @@ workspace : mod + shift + 5 : swap 5 ## Default Keybindings ### Window Management - -| Combo | Action | -| ---------------------------- | ------------------------- | -| Mouse | Focus under cursor | -| `MOD` + Left Mouse | Move window by mouse | -| `MOD` + Right Mouse | Resize window by mouse | -| `MOD` + `j` / `k` | Focus next / previous | -| `MOD` + `,` / `.` | Focus prev / next monitor | -| `MOD` + `Shift` + `,` / `.` | Move window to prev / next monitor | -| `MOD` + `Shift` + `j` / `k` | Move in master stack | -| `MOD` + `Space` | Toggle floating | -| `MOD` + `Shift` + `Space` | Toggle all floating | -| `MOD` + `=` / `-` | Increase/Decrease gaps | -| `MOD` + `Shift` + `f` | Fullscreen toggle | -| `MOD` + `q` | Close focused window | -| `MOD` + `1-9` | Switch workspace 1–9 | -| `MOD` + `Shift` + `1-9` | Move window to WS 1–9 | - -### Programs - -| Combo | Action | Program | -| -------------------- | ---------- | ---------- | -| `MOD` + `Return` | Terminal | `st` | -| `MOD` + `b` | Browser | `firefox` | -| `MOD` + `p` | Launcher | `dmenu_run`| +| Combo | Action | +| --------------------------- | ---------------------------------- | +| `Mouse` | Focus on click under cursor | +| `MOD` + `Left Mouse` | Move window with mouse | +| `MOD` + `Right Mouse` | Resize window with mouse | +| `MOD` + `j` / `k` | Focus next / previous | +| `MOD` + `Shift` + `j` / `k` | Move window in master stack | +| `MOD` + `,` / `.` | Focus prev / next monitor | +| `MOD` + `Shift` + `,` / `.` | Move window to prev / next monitor | +| `MOD` + `h` / `l` | Resize master area (decr/incr) | +| `MOD` + `Ctrl` + `h` / `l` | Resize stack area (decr/incr) | +| `MOD` + `=` / `-` | Increase / decrease gaps | +| `MOD` + `Space` | Toggle floating | +| `MOD` + `Shift` + `Space` | Toggle all floating | +| `MOD` + `Shift` + `f` | Toggle fullscreen mode | +| `MOD` + `Shift` + `q` | Close focused window | +| `MOD` + `Shift` + `e` | Quit sxwm | +| `MOD` + `r` | Reload configuration | +| `MOD` + `c` | Centre window | + +### Scratchpads + +| Combo | Action | +| ---------------------------------- | -------------------------------- | +| `MOD` + `Alt` + `1–4` | Create scratchpad 1–5 | +| `MOD` + `Ctrl` + `1–4` | Toggle scratchpad 1–5 | +| `MOD` + `Alt` + `Shift` + `1–4` | Remove scratchpad 1–5 | + +### Workspaces + +| Combo | Action | +| ----------------------- | ---------------------------- | +| `MOD` + `1–9` | Switch to workspace 1–9 | +| `MOD` + `Shift` + `1–9` | Move window to workspace 1–9 | + +### Applications + +| Combo | Action | Program | +| ---------------- | ------------- | ----------- | +| `MOD` + `Return` | Open terminal | `st` | +| `MOD` + `b` | Open browser | `firefox` | +| `MOD` + `p` | Run launcher | `dmenu_run` | --- diff --git a/default_sxwmrc b/default_sxwmrc index 541afab..e3c29b8 100644 --- a/default_sxwmrc +++ b/default_sxwmrc @@ -14,6 +14,9 @@ motion_throttle : 60 # Set to screen refresh rate for smoothest motions should_float : "pcmanfm" new_win_focus : true warp_cursor : true +can_swallow : "st" +can_be_swallowed : "mpv" +new_win_master : false # Keybinds: # Commands must be surrounded with "" @@ -28,6 +31,7 @@ bind : mod + p : "dmenu_run" # Window Management: call : mod + shift + q : close_window +call : mod + c : centre_window call : mod + shift + e : quit # Focus Movement: @@ -66,6 +70,22 @@ call : mod + shift + f : fullscreen # Reload Config call : mod + r : reload_config +# Scratchpads +scratchpad : mod + alt + 1 : create 1 +scratchpad : mod + alt + 2 : create 2 +scratchpad : mod + alt + 3 : create 3 +scratchpad : mod + alt + 4 : create 4 + +scratchpad : mod + ctrl + 1 : toggle 1 +scratchpad : mod + ctrl + 2 : toggle 2 +scratchpad : mod + ctrl + 3 : toggle 3 +scratchpad : mod + ctrl + 4 : toggle 4 + +scratchpad : mod + alt + shift + 1 : remove 1 +scratchpad : mod + alt + shift + 2 : remove 2 +scratchpad : mod + alt + shift + 3 : remove 3 +scratchpad : mod + alt + shift + 4 : remove 4 + # Workspaces (1-9) workspace : mod + 1 : move 1 workspace : mod + shift + 1 : swap 1 diff --git a/src/config.h b/src/config.h index 3c0f0bc..e257da3 100644 --- a/src/config.h +++ b/src/config.h @@ -40,22 +40,40 @@ const Binding binds[] = { {Mod4Mask, XK_r, {.fn = reload_config}, TYPE_FUNC}, - {Mod4Mask, XK_1, {.ws = 0}, TYPE_CWKSP}, - {Mod4Mask | ShiftMask, XK_1, {.ws = 0}, TYPE_MWKSP}, - {Mod4Mask, XK_2, {.ws = 1}, TYPE_CWKSP}, - {Mod4Mask | ShiftMask, XK_2, {.ws = 1}, TYPE_MWKSP}, - {Mod4Mask, XK_3, {.ws = 2}, TYPE_CWKSP}, - {Mod4Mask | ShiftMask, XK_3, {.ws = 2}, TYPE_MWKSP}, - {Mod4Mask, XK_4, {.ws = 3}, TYPE_CWKSP}, - {Mod4Mask | ShiftMask, XK_4, {.ws = 3}, TYPE_MWKSP}, - {Mod4Mask, XK_5, {.ws = 4}, TYPE_CWKSP}, - {Mod4Mask | ShiftMask, XK_5, {.ws = 4}, TYPE_MWKSP}, - {Mod4Mask, XK_6, {.ws = 5}, TYPE_CWKSP}, - {Mod4Mask | ShiftMask, XK_6, {.ws = 5}, TYPE_MWKSP}, - {Mod4Mask, XK_7, {.ws = 6}, TYPE_CWKSP}, - {Mod4Mask | ShiftMask, XK_7, {.ws = 6}, TYPE_MWKSP}, - {Mod4Mask, XK_8, {.ws = 7}, TYPE_CWKSP}, - {Mod4Mask | ShiftMask, XK_8, {.ws = 7}, TYPE_MWKSP}, - {Mod4Mask, XK_9, {.ws = 8}, TYPE_CWKSP}, - {Mod4Mask | ShiftMask, XK_9, {.ws = 8}, TYPE_MWKSP}, + {Mod4Mask | Mod1Mask, XK_1, {.sp = 0}, TYPE_SP_CREATE}, + {Mod4Mask | Mod1Mask, XK_2, {.sp = 1}, TYPE_SP_CREATE}, + {Mod4Mask | Mod1Mask, XK_3, {.sp = 2}, TYPE_SP_CREATE}, + {Mod4Mask | Mod1Mask, XK_4, {.sp = 3}, TYPE_SP_CREATE}, + {Mod4Mask | Mod1Mask, XK_5, {.sp = 4}, TYPE_SP_CREATE}, + + {Mod4Mask | ControlMask, XK_1, {.sp = 0}, TYPE_SP_TOGGLE}, + {Mod4Mask | ControlMask, XK_2, {.sp = 1}, TYPE_SP_TOGGLE}, + {Mod4Mask | ControlMask, XK_3, {.sp = 2}, TYPE_SP_TOGGLE}, + {Mod4Mask | ControlMask, XK_4, {.sp = 3}, TYPE_SP_TOGGLE}, + {Mod4Mask | ControlMask, XK_5, {.sp = 4}, TYPE_SP_TOGGLE}, + + {Mod4Mask | Mod1Mask | ShiftMask, XK_1, {.sp = 0}, TYPE_SP_REMOVE}, + {Mod4Mask | Mod1Mask | ShiftMask, XK_2, {.sp = 1}, TYPE_SP_REMOVE}, + {Mod4Mask | Mod1Mask | ShiftMask, XK_3, {.sp = 2}, TYPE_SP_REMOVE}, + {Mod4Mask | Mod1Mask | ShiftMask, XK_4, {.sp = 3}, TYPE_SP_REMOVE}, + {Mod4Mask | Mod1Mask | ShiftMask, XK_5, {.sp = 4}, TYPE_SP_REMOVE}, + + {Mod4Mask, XK_1, {.ws = 0}, TYPE_WS_CHANGE}, + {Mod4Mask | ShiftMask, XK_1, {.ws = 0}, TYPE_WS_MOVE}, + {Mod4Mask, XK_2, {.ws = 1}, TYPE_WS_CHANGE}, + {Mod4Mask | ShiftMask, XK_2, {.ws = 1}, TYPE_WS_MOVE}, + {Mod4Mask, XK_3, {.ws = 2}, TYPE_WS_CHANGE}, + {Mod4Mask | ShiftMask, XK_3, {.ws = 2}, TYPE_WS_MOVE}, + {Mod4Mask, XK_4, {.ws = 3}, TYPE_WS_CHANGE}, + {Mod4Mask | ShiftMask, XK_4, {.ws = 3}, TYPE_WS_MOVE}, + {Mod4Mask, XK_5, {.ws = 4}, TYPE_WS_CHANGE}, + {Mod4Mask | ShiftMask, XK_5, {.ws = 4}, TYPE_WS_MOVE}, + {Mod4Mask, XK_6, {.ws = 5}, TYPE_WS_CHANGE}, + {Mod4Mask | ShiftMask, XK_6, {.ws = 5}, TYPE_WS_MOVE}, + {Mod4Mask, XK_7, {.ws = 6}, TYPE_WS_CHANGE}, + {Mod4Mask | ShiftMask, XK_7, {.ws = 6}, TYPE_WS_MOVE}, + {Mod4Mask, XK_8, {.ws = 7}, TYPE_WS_CHANGE}, + {Mod4Mask | ShiftMask, XK_8, {.ws = 7}, TYPE_WS_MOVE}, + {Mod4Mask, XK_9, {.ws = 8}, TYPE_WS_CHANGE}, + {Mod4Mask | ShiftMask, XK_9, {.ws = 8}, TYPE_WS_MOVE}, }; @@ -1,13 +1,13 @@ /* See LICENSE for more information on use */ #pragma once #include <X11/Xlib.h> -#define SXWM_VERSION "sxwm ver. 1.6" -#define SXWM_AUTHOR "(C) Abhinav Prasai 2025" -#define SXWM_LICINFO "See LICENSE for more info" +#define SXWM_VERSION "sxwm ver. 1.6" +#define SXWM_AUTHOR "(C) Abhinav Prasai 2025" +#define SXWM_LICINFO "See LICENSE for more info" -#define ALT Mod1Mask -#define SUPER Mod4Mask -#define SHIFT ShiftMask +#define ALT Mod1Mask +#define SUPER Mod4Mask +#define SHIFT ShiftMask #define MARGIN (gaps + BORDER_WIDTH) #define OUT_IN (2 * BORDER_WIDTH) @@ -17,18 +17,26 @@ #define MAX(a, b) ((a) > (b) ? (a) : (b)) #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 MAXCLIENTS 99 -#define BIND(mod, key, cmdstr) { (mod), XK_##key, { cmdstr }, False } -#define CALL(mod, key, fnptr) { (mod), XK_##key, { .fn = fnptr }, True } -#define CMD(name, ...) \ - const char *name[] = { __VA_ARGS__, NULL } +#define UDIST(a, b) abs((int)(a) - (int)(b)) +#define CLAMP(x, lo, hi) (((x) < (lo)) ? (lo) : ((x) > (hi)) ? (hi) : (x)) +#define MAXCLIENTS 99 +#define MAX_SCRATCHPADS 20 +#define MIN_WINDOW_SIZE 20 +#define BIND(mod, key, cmdstr) {(mod), XK_##key, {cmdstr}, False} +#define CALL(mod, key, fnptr) {(mod), XK_##key, {.fn = fnptr}, True} +#define CMD(name, ...) const char *name[] = {__VA_ARGS__, NULL} -#define TYPE_CWKSP 0 -#define TYPE_MWKSP 1 -#define TYPE_FUNC 2 -#define TYPE_CMD 3 +/* workspaces */ +#define TYPE_FUNC 2 +#define TYPE_WS_CHANGE 0 +#define TYPE_WS_MOVE 1 +/* fn/cmd */ +#define TYPE_FUNC 2 +#define TYPE_CMD 3 +/* scratchpads*/ +#define TYPE_SP_REMOVE 4 +#define TYPE_SP_TOGGLE 5 +#define TYPE_SP_CREATE 6 #define NUM_WORKSPACES 9 #define WORKSPACE_NAMES \ @@ -40,21 +48,16 @@ "6" "\0"\ "7" "\0"\ "8" "\0"\ - "9" "\0"\ - -typedef enum { - DRAG_NONE, - DRAG_MOVE, - DRAG_RESIZE, - DRAG_SWAP -} DragMode; + "9" "\0" +typedef enum { DRAG_NONE, DRAG_MOVE, DRAG_RESIZE, DRAG_SWAP } DragMode; typedef void (*EventHandler)(XEvent *); typedef union { const char **cmd; void (*fn)(void); - int ws; + int ws; /* workspace */ + int sp; /* scratchpad */ } Action; typedef struct { @@ -64,7 +67,7 @@ typedef struct { int type; } Binding; -typedef struct Client{ +typedef struct Client { Window win; int x, y, h, w; int orig_x, orig_y, orig_w, orig_h; @@ -75,7 +78,10 @@ typedef struct Client{ Bool floating; Bool fullscreen; Bool mapped; + pid_t pid; struct Client *next; + struct Client *swallowed; + struct Client *swallower; } Client; typedef struct { @@ -93,9 +99,14 @@ typedef struct { int bindsn; Bool new_win_focus; Bool warp_cursor; + Bool new_win_master; Binding binds[256]; char **should_float[256]; - char *torun[256]; + char **can_swallow[256]; + char **can_be_swallowed[256]; + char **scratchpads[32]; + char **open_in_workspace[256]; + char *torun[256]; } Config; typedef struct { @@ -103,6 +114,12 @@ typedef struct { int w, h; } Monitor; +typedef struct { + Client *client; + Bool enabled; +} Scratchpad; + +extern void centre_window(); extern void close_focused(void); extern void dec_gaps(void); extern void focus_next(void); diff --git a/src/parser.c b/src/parser.c index 3fc2856..79cf29b 100644 --- a/src/parser.c +++ b/src/parser.c @@ -1,6 +1,6 @@ #define _POSIX_C_SOURCE 200809L #include <ctype.h> -#include <limits.h> +#include <linux/limits.h> #include <stdio.h> #include <stdlib.h> #include <string.h> @@ -9,6 +9,7 @@ #include <wordexp.h> #endif #include <X11/keysym.h> +#include <X11/XF86keysym.h> #include <X11/Xlib.h> #include "parser.h" #include "defs.h" @@ -36,6 +37,7 @@ static const struct { {"toggle_floating", toggle_floating}, {"global_floating", toggle_floating_global}, {"fullscreen", toggle_fullscreen}, + {"centre_window", centre_window}, {NULL, NULL}}; static void remap_and_dedupe_binds(Config *cfg) @@ -367,17 +369,58 @@ found: int n; if (sscanf(act, "move %d", &n) == 1 && n >= 1 && n <= NUM_WORKSPACES) { - b->type = TYPE_CWKSP; + b->type = TYPE_WS_CHANGE; b->action.ws = n - 1; } else if (sscanf(act, "swap %d", &n) == 1 && n >= 1 && n <= NUM_WORKSPACES) { - b->type = TYPE_MWKSP; + b->type = TYPE_WS_MOVE; b->action.ws = n - 1; } else { fprintf(stderr, "sxwmrc:%d: invalid workspace action '%s'\n", lineno, act); } } + else if (!strcmp(key, "scratchpad")) { + char *mid = strchr(rest, ':'); + if (!mid) { + fprintf(stderr, "sxwmrc:%d: scratchpad missing action\n", lineno); + continue; + } + *mid = '\0'; + char *combo = strip(rest); + char *act = strip(mid + 1); + + KeySym ks; + unsigned mods = parse_combo(combo, cfg, &ks); + if (ks == NoSymbol) { + fprintf(stderr, "sxwmrc:%d: bad key in '%s'\n", lineno, combo); + continue; + } + + Binding *b = alloc_bind(cfg, mods, ks); + if (!b) { + fputs("sxwm: too many binds\n", stderr); + goto cleanup_file; + } + + int padnum = -1; + + if (sscanf(act, "create %d", &padnum) == 1 && padnum >= 1 && padnum <= MAX_SCRATCHPADS) { + b->type = TYPE_SP_CREATE; + b->action.sp = padnum - 1; + } + else if (sscanf(act, "toggle %d", &padnum) == 1 && padnum >= 1 && padnum <= MAX_SCRATCHPADS) { + b->type = TYPE_SP_TOGGLE; + b->action.sp = padnum - 1; + } + else if (sscanf(act, "remove %d", &padnum) == 1 && padnum >= 1 && padnum <= MAX_SCRATCHPADS) { + b->type = TYPE_SP_REMOVE; + b->action.sp = padnum - 1; + } + else { + fprintf(stderr, "sxwmrc:%d: invalid scratchpad action '%s'\n", lineno, act); + } + } else if (!strcmp(key, "exec")) { if (torun >= 256) { fprintf(stderr, "sxwmrc:%d: too many exec commands\n", lineno); @@ -404,6 +447,81 @@ found: } torun++; } + else if (!strcmp(key, "can_swallow")) { + char *token = strtok(rest, ","); + int i = 0; + while (token && i < 256) { + char *item = strip_quotes(strip(token)); + if (*item) { + cfg->can_swallow[i] = malloc(2 * sizeof(char *)); + if (!cfg->can_swallow[i]) { + fprintf(stderr, "sxwmrc:%d: malloc failed\n", lineno); + break; + } + cfg->can_swallow[i][0] = strdup(item); + cfg->can_swallow[i][1] = NULL; + i++; + } + token = strtok(NULL, ","); + } + } + else if (!strcmp(key, "can_be_swallowed")) { + char *token = strtok(rest, ","); + int i = 0; + while (token && i < 256) { + char *item = strip_quotes(strip(token)); + if (*item) { + cfg->can_be_swallowed[i] = malloc(2 * sizeof(char *)); + if (!cfg->can_be_swallowed[i]) { + break; + } + cfg->can_be_swallowed[i][0] = strdup(item); + cfg->can_be_swallowed[i][1] = NULL; + i++; + } + token = strtok(NULL, ","); + } + } + else if (!strcmp(key, "new_win_master")) { + cfg->new_win_master = !strcmp(rest, "true") ? True : False; + } + else if (!strcmp(key, "open_in_workspace")) { + char *mid = strchr(rest, ':'); + if (!mid) { + fprintf(stderr, "sxwmrc:%d: open_in_workspace missing workspace number\n", lineno); + continue; + } + *mid = '\0'; + char *class_name = strip(rest); + char *ws_str = strip(mid + 1); + + class_name = strip_quotes(class_name); + + int ws = atoi(ws_str); + if (ws < 1 || ws > NUM_WORKSPACES) { + fprintf(stderr, "sxwmrc:%d: invalid workspace number %d\n", lineno, ws); + continue; + } + + /* find free slot in open_in_workspace */ + int slot = -1; + for (int i = 0; i < 256; i++) { + if (!cfg->open_in_workspace[i]) { + slot = i; + break; + } + } + + if (slot >= 0) { + cfg->open_in_workspace[slot] = malloc(2 * sizeof(char *)); + if (cfg->open_in_workspace[slot]) { + cfg->open_in_workspace[slot][0] = strdup(class_name); /* class name */ + char ws_buf[16]; + snprintf(ws_buf, sizeof(ws_buf), "%d", ws - 1); /* 0-indexed workspace */ + cfg->open_in_workspace[slot][1] = strdup(ws_buf); /* workspace number */ + } + } + } else { fprintf(stderr, "sxwmrc:%d: unknown option '%s'\n", lineno, key); } @@ -574,4 +692,4 @@ const char **build_argv(const char *cmd) free(tmp); return argv; #endif -}
\ No newline at end of file +} @@ -12,15 +12,16 @@ * will use this will probably be programmers :) * * (C) Abhinav Prasai 2025 - */ +*/ #include <X11/X.h> #include <err.h> #include <stdio.h> -#include <limits.h> +#include <linux/limits.h> #include <signal.h> #include <stdlib.h> #include <string.h> +#include <strings.h> #include <sys/wait.h> #include <unistd.h> @@ -36,9 +37,8 @@ #include "defs.h" #include "parser.h" -#define MIN_WINDOW_SIZE 20 - Client *add_client(Window w, int ws); +/* void centre_window(); */ void change_workspace(int ws); int clean_mask(int mask); /* void close_focused(void); */ @@ -47,6 +47,8 @@ Window find_toplevel(Window w); /* void focus_next(void); */ /* void focus_prev(void); */ int get_monitor_for(Client *c); +pid_t get_pid(Window w); +int get_workspace_for_window(Window w); void grab_keys(void); void hdl_button(XEvent *xev); void hdl_button_release(XEvent *xev); @@ -62,6 +64,7 @@ void hdl_root_property(XEvent *xev); void hdl_unmap_ntf(XEvent *xev); /* void inc_gaps(void); */ void init_defaults(void); +Bool is_child_proc(pid_t pid1, pid_t pid2); /* void move_master_next(void); */ /* void move_master_prev(void); */ void move_to_workspace(int ws); @@ -70,6 +73,7 @@ int other_wm_err(Display *dpy, XErrorEvent *ee); /* long parse_col(const char *hex); */ /* void quit(void); */ /* void reload_config(void); */ +void remove_scratchpad(int n); /* void resize_master_add(void); */ /* void resize_master_sub(void); */ /* void resize_stack_add(void); */ @@ -79,14 +83,18 @@ void scan_existing_windows(void); void send_wm_take_focus(Window w); void setup(void); void setup_atoms(void); +void set_win_scratchpad(int n); int snap_coordinate(int pos, int size, int screen_size, int snap_dist); void spawn(const char **argv); void startup_exec(void); +void swallow_window(Client *swallower, Client *swallowed); void swap_clients(Client *a, Client *b); void tile(void); /* void toggle_floating(void); */ /* void toggle_floating_global(void); */ /* void toggle_fullscreen(void); */ +void toggle_scratchpad(int n); +void unswallow_window(Client *c); void update_borders(void); void update_client_desktop_properties(void); void update_monitors(void); @@ -130,6 +138,9 @@ Display *dpy; Window root; Window wm_check_win; Monitor *mons = NULL; +Scratchpad scratchpads[MAX_SCRATCHPADS]; +int scratchpad_count = 0; +int current_scratchpad = 0; int monsn = 0; int current_monitor = 0; Bool global_floating = False; @@ -162,16 +173,25 @@ Client *add_client(Window w, int ws) c->win = w; c->next = NULL; c->ws = ws; + c->pid = get_pid(w); + c->swallowed = NULL; + c->swallower = NULL; if (!workspaces[ws]) { workspaces[ws] = c; } else { - Client *tail = workspaces[ws]; - while (tail->next) { - tail = tail->next; + if (user_config.new_win_master) { + c->next = workspaces[ws]; + workspaces[ws] = c; + } + else { + Client *tail = workspaces[ws]; + while (tail->next) { + tail = tail->next; + } + tail->next = c; } - tail->next = c; } open_windows++; @@ -234,6 +254,20 @@ Client *add_client(Window w, int ws) return c; } +void centre_window() +{ + if (!focused || !focused->mapped || !focused->floating) { + return; + } + + int x = mons[focused->mon].x + (mons[focused->mon].w - focused->w) / 2; + int y = mons[focused->mon].y + (mons[focused->mon].h - focused->h) / 2; + + focused->x = x; + focused->y = y; + XMoveWindow(dpy, focused->win, x, y); +} + void change_workspace(int ws) { if (ws >= NUM_WORKSPACES || ws == current_ws) { @@ -296,6 +330,14 @@ void close_focused(void) return; } + for (int i = 0; i < MAX_SCRATCHPADS; i++) { + if (scratchpads[i].client == focused) { + scratchpads[i].client = NULL; + scratchpads[i].enabled = False; + break; + } + } + Atom *protos; int n; if (XGetWMProtocols(dpy, focused->win, &protos, &n) && protos) { @@ -621,6 +663,56 @@ int get_monitor_for(Client *c) return 0; } +pid_t get_pid(Window w) +{ + pid_t pid = 0; + Atom actual_type; + int actual_format; + unsigned long nitems, bytes_after; + unsigned char *prop = NULL; + Atom atom_pid = XInternAtom(dpy, "_NET_WM_PID", False); + + if (XGetWindowProperty(dpy, w, atom_pid, 0, 1, False, XA_CARDINAL, &actual_type, &actual_format, &nitems, + &bytes_after, &prop) == Success && + prop) { + if (actual_format == 32 && nitems == 1) { + pid = *(pid_t *)prop; + } + XFree(prop); + } + return pid; +} + +int get_workspace_for_window(Window w) +{ + XClassHint ch; + if (!XGetClassHint(dpy, w, &ch)) { + return current_ws; /* default to current workspace */ + } + + for (int i = 0; i < 256; i++) { + if (!user_config.open_in_workspace[i]) { + break; + } + + char *rule_class = user_config.open_in_workspace[i][0]; + char *rule_ws = user_config.open_in_workspace[i][1]; + + if (rule_class && rule_ws) { + if ((ch.res_class && strcasecmp(ch.res_class, rule_class) == 0) || + (ch.res_name && strcasecmp(ch.res_name, rule_class) == 0)) { + XFree(ch.res_class); + XFree(ch.res_name); + return atoi(rule_ws); + } + } + } + + XFree(ch.res_class); + XFree(ch.res_name); + return current_ws; /* default to current workspace */ +} + void grab_keys(void) { const int guards[] = {0, @@ -636,8 +728,8 @@ void grab_keys(void) for (int i = 0; i < user_config.bindsn; i++) { 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))) { + if ((b->type == TYPE_WS_CHANGE && b->mods != user_config.modkey) || + (b->type == TYPE_WS_MOVE && b->mods != (user_config.modkey | ShiftMask))) { continue; } @@ -831,6 +923,21 @@ void hdl_destroy_ntf(XEvent *xev) c = c->next; } if (c) { + if (c->swallower) { + unswallow_window(c); + } + + if (c->swallowed) { + Client *swallowed = c->swallowed; + c->swallowed = NULL; + swallowed->swallower = NULL; + + /* show swallowed window */ + XMapWindow(dpy, swallowed->win); + swallowed->mapped = True; + focused = swallowed; + } + if (focused == c) { if (c->next) { focused = c->next; @@ -888,20 +995,54 @@ void hdl_keypress(XEvent *xev) b->action.fn(); } break; - case TYPE_CWKSP: + case TYPE_WS_CHANGE: change_workspace(b->action.ws); update_net_client_list(); break; - case TYPE_MWKSP: + case TYPE_WS_MOVE: move_to_workspace(b->action.ws); update_net_client_list(); break; + case TYPE_SP_REMOVE: + remove_scratchpad(b->action.sp); + break; + case TYPE_SP_TOGGLE: + toggle_scratchpad(b->action.sp); + break; + case TYPE_SP_CREATE: + set_win_scratchpad(b->action.sp); + break; } return; } } } +void swallow_window(Client *swallower, Client *swallowed) +{ + if (!swallower || !swallowed || swallower->swallowed || swallowed->swallower) { + return; + } + + XUnmapWindow(dpy, swallower->win); + swallower->mapped = False; + + swallower->swallowed = swallowed; + swallowed->swallower = swallower; + + swallowed->floating = swallower->floating; + if (swallowed->floating) { + swallowed->x = swallower->x; + swallowed->y = swallower->y; + swallowed->w = swallower->w; + swallowed->h = swallower->h; + XMoveResizeWindow(dpy, swallowed->win, swallowed->x, swallowed->y, swallowed->w, swallowed->h); + } + + tile(); + update_borders(); +} + void swap_clients(Client *a, Client *b) { if (!a || !b || a == b) { @@ -1022,7 +1163,8 @@ void hdl_map_req(XEvent *xev) return; } - Client *c = add_client(w, current_ws); + int target_ws = get_workspace_for_window(w); + Client *c = add_client(w, target_ws); if (!c) { return; } @@ -1057,6 +1199,11 @@ void hdl_map_req(XEvent *xev) XSetWindowBorderWidth(dpy, w, user_config.border_width); } + if (target_ws != current_ws) { + update_net_client_list(); + return; + } + /* map & borders */ update_net_client_list(); if (!global_floating && !c->floating) { @@ -1066,6 +1213,67 @@ void hdl_map_req(XEvent *xev) XRaiseWindow(dpy, w); } + /* check for swallowing opportunities */ + { + XClassHint ch; + Bool can_be_swallowed = False; + + if (XGetClassHint(dpy, w, &ch)) { + /* check if new window can be swallowed */ + for (int i = 0; i < 256; i++) { + if (!user_config.can_be_swallowed[i] || !user_config.can_be_swallowed[i][0]) { + break; + } + + if ((ch.res_class && strcasecmp(ch.res_class, user_config.can_be_swallowed[i][0]) == 0) || + (ch.res_name && strcasecmp(ch.res_name, user_config.can_be_swallowed[i][0]) == 0)) { + can_be_swallowed = True; + break; + } + } + + /* if window can be swallowed look for a potential swallower */ + if (can_be_swallowed) { + for (Client *p = workspaces[current_ws]; p; p = p->next) { + if (p == c || p->swallowed || !p->mapped) { + continue; + } + + XClassHint pch; + Bool can_swallow = False; + + if (XGetClassHint(dpy, p->win, &pch)) { + /* check if this existing window can swallow others */ + for (int i = 0; i < 256; i++) { + if (!user_config.can_swallow[i] || !user_config.can_swallow[i][0]) { + break; + } + + if ((pch.res_class && strcasecmp(pch.res_class, user_config.can_swallow[i][0]) == 0) || + (pch.res_name && strcasecmp(pch.res_name, user_config.can_swallow[i][0]) == 0)) { + can_swallow = True; + break; + } + } + + /* check process relationship */ + if (can_swallow) { + /* we know class matches — swallow now */ + swallow_window(p, c); + XFree(pch.res_class); + XFree(pch.res_name); + break; + } + XFree(pch.res_class); + XFree(pch.res_name); + } + } + } + XFree(ch.res_class); + XFree(ch.res_name); + } + } + XMapWindow(dpy, w); c->mapped = True; @@ -1293,10 +1501,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++) { default_config.master_width[i] = 50 / 100.0f; } + for (int i = 0; i < 256; i++) { + default_config.can_be_swallowed[i] = NULL; + default_config.can_swallow[i] = NULL; + default_config.open_in_workspace[i] = NULL; + } + default_config.motion_throttle = 60; default_config.resize_master_amt = 5; default_config.resize_stack_amt = 20; @@ -1304,6 +1519,7 @@ void init_defaults(void) default_config.bindsn = 0; default_config.new_win_focus = True; default_config.warp_cursor = True; + default_config.new_win_master = False; if (backup_binds) { for (unsigned long i = 0; i < LENGTH(binds); i++) { @@ -1318,6 +1534,47 @@ void init_defaults(void) user_config = default_config; } +Bool is_child_proc(pid_t parent_pid, pid_t child_pid) +{ + if (parent_pid <= 0 || child_pid <= 0) { + return False; + } + + char path[PATH_MAX]; + FILE *f; + pid_t current_pid = child_pid; + int max_iterations = 20; + + while (current_pid > 1 && max_iterations-- > 0) { + snprintf(path, sizeof(path), "/proc/%d/stat", current_pid); + f = fopen(path, "r"); + if (!f) { + printf("sxwm: could not open %s\n", path); + return False; + } + + int ppid = 0; + if (fscanf(f, "%*d %*s %*c %d", &ppid) != 1) { + printf("sxwm: failed to read ppid from %s\n", path); + fclose(f); + return False; + } + fclose(f); + + if (ppid == parent_pid) { + return True; + } + + if (ppid <= 1) { /* Reached init or kernel */ + printf("sxwm: reached init/kernel, no relationship found\n"); + break; + } + + current_pid = ppid; + } + return False; +} + void move_master_next(void) { if (!workspaces[current_ws] || !workspaces[current_ws]->next) { @@ -1475,8 +1732,11 @@ void reload_config(void) { puts("sxwm: reloading config..."); + /* free binding commands without */ for (int i = 0; i < user_config.bindsn; i++) { - free(user_config.binds[i].action.cmd); + if (user_config.binds[i].type == TYPE_CMD && user_config.binds[i].action.cmd) { + free(user_config.binds[i].action.cmd); + } user_config.binds[i].action.cmd = NULL; user_config.binds[i].action.fn = NULL; user_config.binds[i].type = -1; @@ -1484,14 +1744,62 @@ void reload_config(void) user_config.binds[i].mods = 0; } - memset(&user_config, 0, sizeof(user_config)); + /* free swallow-related arrays */ + for (int i = 0; i < 256; i++) { + if (user_config.can_swallow[i]) { + if (user_config.can_swallow[i][0]) { + free(user_config.can_swallow[i][0]); + } + free(user_config.can_swallow[i]); + user_config.can_swallow[i] = NULL; + } + if (user_config.can_be_swallowed[i]) { + if (user_config.can_be_swallowed[i][0]) { + free(user_config.can_be_swallowed[i][0]); + } + free(user_config.can_be_swallowed[i]); + user_config.can_be_swallowed[i] = NULL; + } + if (user_config.open_in_workspace[i]) { + if (user_config.open_in_workspace[i][0]) { + free(user_config.open_in_workspace[i][0]); + } + if (user_config.open_in_workspace[i][1]) { + free(user_config.open_in_workspace[i][1]); + } + free(user_config.open_in_workspace[i]); + user_config.open_in_workspace[i] = NULL; + } + } + + /* free should_float arrays */ + for (int i = 0; i < 256; i++) { + if (user_config.should_float[i]) { + if (user_config.should_float[i][0]) { + free(user_config.should_float[i][0]); + } + free(user_config.should_float[i]); + user_config.should_float[i] = NULL; + } + } + + /* free any exec strings */ + for (int i = 0; i < 256; i++) { + if (user_config.torun[i]) { + free(user_config.torun[i]); + user_config.torun[i] = NULL; + } + } + /* wipe everything else */ + memset(&user_config, 0, sizeof(user_config)); init_defaults(); if (parser(&user_config)) { - fprintf(stderr, "sxrc: error parsing config file\n"); + fprintf(stderr, "sxwmrc: error parsing config file\n"); init_defaults(); } + /* regrab all key/button bindings */ grab_keys(); XUngrabButton(dpy, AnyButton, AnyModifier, root); for (int ws = 0; ws < NUM_WORKSPACES; ws++) { @@ -1499,16 +1807,12 @@ void reload_config(void) XUngrabButton(dpy, AnyButton, AnyModifier, c->win); } } - XGrabButton(dpy, Button1, user_config.modkey, root, True, ButtonPressMask | ButtonReleaseMask | PointerMotionMask, GrabModeAsync, GrabModeAsync, None, None); - XGrabButton(dpy, Button1, user_config.modkey | ShiftMask, root, True, ButtonPressMask | ButtonReleaseMask | PointerMotionMask, GrabModeAsync, GrabModeAsync, None, None); - XGrabButton(dpy, Button3, user_config.modkey, root, True, ButtonPressMask | ButtonReleaseMask | PointerMotionMask, GrabModeAsync, GrabModeAsync, None, None); - for (int ws = 0; ws < NUM_WORKSPACES; ws++) { for (Client *c = workspaces[ws]; c; c = c->next) { XGrabButton(dpy, Button1, 0, c->win, False, ButtonPressMask, GrabModeSync, GrabModeAsync, None, None); @@ -1523,12 +1827,23 @@ void reload_config(void) update_client_desktop_properties(); update_net_client_list(); - XSync(dpy, False); + tile(); update_borders(); } +void remove_scratchpad(int n) +{ + if (scratchpads[n].client == NULL) { + return; + } + + XMapWindow(dpy, scratchpads[n].client->win); + scratchpads[n].client = NULL; + scratchpads[n].enabled = False; +} + void resize_master_add(void) { /* pick the monitor of the focused window (or 0 if none) */ @@ -1676,6 +1991,7 @@ void setup(void) GrabModeAsync, GrabModeAsync, None, None); XGrabButton(dpy, Button1, user_config.modkey | ShiftMask, root, True, ButtonPressMask | ButtonReleaseMask | PointerMotionMask, GrabModeAsync, GrabModeAsync, None, None); + XGrabButton(dpy, Button3, user_config.modkey, root, True, ButtonPressMask | ButtonReleaseMask | PointerMotionMask, GrabModeAsync, GrabModeAsync, None, None); XSync(dpy, False); @@ -1761,6 +2077,23 @@ void setup_atoms(void) update_workarea(); } +void set_win_scratchpad(int n) +{ + if (focused == NULL) { + return; + } + + Client *pad_client = focused; + if (scratchpads[n].client != NULL) { + XMapWindow(dpy, scratchpads[n].client->win); + scratchpads[n].enabled = False; + scratchpads[n].client = NULL; + } + scratchpads[n].client = pad_client; + XUnmapWindow(dpy, scratchpads[n].client->win); + scratchpads[n].enabled = False; +} + Bool window_should_float(Window w) { XClassHint ch; @@ -2177,6 +2510,82 @@ void toggle_fullscreen(void) } } +void toggle_scratchpad(int n) +{ + if (scratchpads[n].client == NULL) { + return; + } + + if (scratchpads[n].client->ws != current_ws) { + Client *c = scratchpads[n].client; + + /* unlink from old workspace */ + Client **pp = &workspaces[c->ws]; + while (*pp && *pp != c) { + pp = &(*pp)->next; + } + if (*pp) { + *pp = c->next; + } + + /* link to new workspace */ + c->next = workspaces[current_ws]; + workspaces[current_ws] = c; + + c->ws = current_ws; + c->mon = get_monitor_for(c); + + tile(); + update_borders(); + update_client_desktop_properties(); + update_net_client_list(); + } + + if (scratchpads[n].enabled) { + XUnmapWindow(dpy, scratchpads[n].client->win); + scratchpads[n].enabled = False; + focus_prev(); + send_wm_take_focus(focused->win); + update_borders(); + } + else { + XMapWindow(dpy, scratchpads[n].client->win); + XRaiseWindow(dpy, scratchpads[n].client->win); + scratchpads[n].enabled = True; + focused = scratchpads[n].client; + XSetInputFocus(dpy, focused->win, RevertToPointerRoot, CurrentTime); + send_wm_take_focus(scratchpads[n].client->win); + if (user_config.warp_cursor) { + warp_cursor(focused); + } + tile(); + update_borders(); + } +} + +void unswallow_window(Client *c) +{ + if (!c || !c->swallower) { + return; + } + + Client *swallower = c->swallower; + + /* unlink windows */ + swallower->swallowed = NULL; + c->swallower = NULL; + + XMapWindow(dpy, swallower->win); + swallower->mapped = True; + + focused = swallower; + XSetInputFocus(dpy, swallower->win, RevertToPointerRoot, CurrentTime); + XRaiseWindow(dpy, swallower->win); + + tile(); + update_borders(); +} + void update_borders(void) { for (Client *c = workspaces[current_ws]; c; c = c->next) { @@ -5,6 +5,7 @@ sxwm \- minimal, fast, and configurable tiling window manager for X11 .SH SYNOPSIS .B sxwm +[\fIOPTION\fR] .SH DESCRIPTION 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. @@ -12,23 +13,27 @@ sxwm is a lightweight and efficient tiling window manager for X11, designed to b .SH FEATURES Tiling and floating layouts. Nine workspaces with full bar support. +Scratchpads for instant access to floating windows. +Native windwo swallowing. Live configuration reload without restart. Human-friendly configuration file requiring no recompilation. DWM-style master-stack layout. Mouse support for moving, resizing, and swapping windows. -Depends only on libX11 and Xinerama. +Depends only on libX11, Xinerama, and XCursor. Extremely lightweight (single C file). Multi-monitor support via Xinerama. Works well with external bars such as sxbar. +Zero dependencies beyond X11 libraries. +Fast and low resource usage. .SH LAUNCH ARGUMENTS .TP -.B -v || --version +.B \-v, \-\-version Displays current sxwm version .TP -.B -b || --backup +.B \-b, \-\-backup Uses default config along side the custom sxwmrc .SH CONFIGURATION @@ -41,7 +46,7 @@ General options include: .TP .B mod_key -Sets the primary modifier key (for example, "alt", "super", or "ctrl"). Default is "super". +Sets the primary modifier key ("alt", "super", or "ctrl"). Default is "super". .TP .B gaps @@ -72,6 +77,10 @@ Percentage of screen width allocated to the master window. Default is 60. Percentage to increase or decrease master width when resizing. Default is 1. .TP +.B resize_stack_amount +How many pixels to increase/decrease stack windows by. Default is 20. + +.TP .B snap_distance Pixels from screen edge before a floating window snaps to the edge. Default is 5. @@ -81,7 +90,7 @@ Target updates per second for mouse drag operations (move, resize, swap). Defaul .TP .B should_float -Lets you change which windows should float by default when opening them. Default is st +Lets you change which windows should float by default when opening them. Multiple entries should be comma-separated. Optionally, entries can be enclosed in quotes. Default is "st". .TP .B new_win_focus @@ -91,40 +100,80 @@ Whether newly opened windows should automatically gain focus. Default is true. .B warp_cursor If true, warps the mouse cursor to the center of newly focused windows. Default is true. +.TP +.B exec +Command to run on startup (e.g., "sxbar", "picom", "autostart", etc.). + +.TP +.B can_swallow +Windows that can swallow other windows. Default is "st". + +.TP +.B can_be_swallowed +Windows that can be swallowed by others. Default is "mpv". + +.TP +.B new_win_as_master +New windows will take place as the master window + .SH KEYBINDINGS Keybindings associate key combinations with actions, either running external commands or internal sxwm functions. They follow this syntax: .TP -.B bind : modifier + modifier + ... + key : action +.B bind : modifier + ... + key : action Modifiers can be mod, shift, ctrl, alt, or super. The key is the final key name (e.g., Return, q, 1, equal, space). Actions can be either a quoted external command or an internal function name. +.TP +.B workspace : modifier + ... + key : move n +.TP +.B workspace : modifier + ... + key : swap n + +For workspace switching and moving windows. n is the workspace number (1-9). + +.TP +.B scratchpad : modifier + ... + key : create n +.TP +.B scratchpad : modifier + ... + key : toggle n +.TP +.B scratchpad : modifier + ... + key : remove n + +For scratchpad management. create assigns the focused window to scratchpad n, toggle shows/hides scratchpad n, and remove returns the scratchpad window to normal tiling. n is the scratchpad number (1-5). + Example bindings: .TP .B bind : mod + Return : "st" - Open the st terminal. .TP .B bind : mod + shift + q : close_window - Close any window that is selected. .TP -.B bind : mod + 3 : change_ws3 - +.B workspace : mod + 3 : move 3 Go to workspace 3. .TP -.B bind : mod + shift + 5 : moveto_ws5 - +.B workspace : mod + shift + 5 : swap 5 Move selected window to workspace 5. +.TP +.B scratchpad : mod + alt + 1 : create 1 +Assign focused window to scratchpad 1. + +.TP +.B scratchpad : mod + ctrl + 1 : toggle 1 +Show or hide scratchpad 1. + +.TP +.B scratchpad : mod + alt + shift + 1 : remove 1 +Remove window from scratchpad 1 and return to normal tiling. + .SH AVAILABLE FUNCTIONS The following internal functions are available for keybindings: @@ -133,6 +182,10 @@ The following internal functions are available for keybindings: Closes the currently focused window. .TP +.B centre_window +Centers the focused window. + +.TP .B decrease_gaps Decreases the gap size between windows. @@ -145,6 +198,22 @@ Shifts focus to the next window in the stack. Shifts focus to the previous window in the stack. .TP +.B focus_next_mon +Switches focus to the next monitor. + +.TP +.B focus_prev_mon +Switches focus to the previous monitor. + +.TP +.B move_next_mon +Moves the focused window to the next monitor. + +.TP +.B move_prev_mon +Moves the focused window to the previous monitor. + +.TP .B increase_gaps Increases the gap size between windows. @@ -153,7 +222,7 @@ Increases the gap size between windows. Moves the focused window down the master/stack order. .TP -.B master_previous +.B master_prev Moves the focused window up the master/stack order. .TP @@ -220,6 +289,18 @@ Moves the focused window to the specified workspace (1 to 9). Window Management: .TP +.B Mouse Click +Focus window under cursor. + +.TP +.B MOD + Left Mouse +Move window with mouse. + +.TP +.B MOD + Right Mouse +Resize window with mouse. + +.TP .B MOD + Return Launch terminal (default: st). @@ -232,10 +313,22 @@ Launch browser (default: firefox). Run launcher (default: dmenu_run). .TP -.B MOD + q +.B MOD + Shift + q Close focused window. .TP +.B MOD + Shift + e +Quit sxwm. + +.TP +.B MOD + r +Reload configuration. + +.TP +.B MOD + c +Center focused window. + +.TP .B MOD + 1 to 9 Switch to workspace 1 through 9. @@ -244,6 +337,18 @@ Switch to workspace 1 through 9. Move focused window to workspace 1 through 9. .TP +.B MOD + Alt + 1 to 4 +Create scratchpad 1 through 4. + +.TP +.B MOD + Ctrl + 1 to 4 +Toggle scratchpad 1 through 4. + +.TP +.B MOD + Alt + Shift + 1 to 4 +Remove scratchpad 1 through 4. + +.TP .B MOD + j / k Focus next or previous window. @@ -252,12 +357,12 @@ 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. +.B MOD + h / l +Resize master area (decrease/increase). .TP -.B MOD + Ctrl h / l -Increase or decrease stack height. +.B MOD + Ctrl + h / l +Resize stack area (decrease/increase). .TP .B MOD + Space @@ -272,42 +377,80 @@ Toggle floating mode for all windows. Increase or decrease gaps. .TP -.B MOD + f +.B MOD + Shift + f Toggle fullscreen mode. .TP -.B MOD + . -Focus next monitor. +.B MOD + . / , +Focus next/previous monitor. .TP -.B MOD + , -Focus previous monitor. +.B MOD + Shift + . / , +Move focused window to next/previous monitor. + +.SH DEPENDENCIES +sxwm requires the following libraries: .TP -.B MOD + Shift + . -Move focused window to next monitor. +.B libX11 +Xorg client library .TP -.B MOD + Shift + , -Move focused window to previous monitor. +.B libXinerama +Multi-monitor support .TP -.B MOD + Left Mouse -Move window with mouse. +.B libXcursor +Cursor support .TP -.B MOD + Right Mouse -Resize window with mouse. +.B GCC or Clang & Make +For building from source .SH FILES Configuration file: .B ~/.config/sxwmrc +.SH EXAMPLES +Example configuration snippets: + +.RS +.nf +# Set gaps and borders +gaps : 15 +border_width : 2 +focused_border_colour : "#ff6b6b" + +# Custom keybindings +bind : mod + Return : "alacritty" +bind : mod + d : "rofi -show drun" + +# Workspace switching +workspace : mod + 1 : move 1 +workspace : mod + shift + 1 : swap 1 + +# Scratchpad management +scratchpad : mod + alt + 1 : create 1 +scratchpad : mod + ctrl + 1 : toggle 1 +scratchpad : mod + alt + shift + 1 : remove 1 + +# Window swallowing +can_swallow : "st" +can_be_swallowed : "sxiv", "mpv", "zathura" +.fi +.RE + .SH AUTHOR -Written by El Bachir (elbachir-one), 2025. +Written by Abhinav Prasai (uint23), 2025. .SH SEE ALSO sxbar(1), dmenu(1), st(1), X(7) .SH LICENSE MIT License. See the LICENSE file for full details. + +.SH BUGS +Report bugs at: https://github.com/uint23/sxwm/issues + +.SH INSPIRATION +sxwm draws inspiration from dwm, i3, sowm, and tinywm. |
