From b67cbfbc0acb51ae36548ed6b34e96676eac3851 Mon Sep 17 00:00:00 2001 From: h8d13 Date: Sun, 2 Aug 2026 01:11:51 +0200 Subject: [PATCH 01/25] fix(edbbd2f): missing `{` --- cfg/ragnar.cfg | 1 + 1 file changed, 1 insertion(+) diff --git a/cfg/ragnar.cfg b/cfg/ragnar.cfg index 822a84c..d7a9182 100644 --- a/cfg/ragnar.cfg +++ b/cfg/ragnar.cfg @@ -550,6 +550,7 @@ keybinds = ( key = "KeyC"; do = "reloadconfigfile"; }, + { key = "KeyAudioLowerVolume"; do = "runcmd"; cmd = "wpctl set-volume -l 1.0 @DEFAULT_AUDIO_SINK@ 5%-"; From 142a6553a2265322d35ea9e4b6bfc3de1bdc763e Mon Sep 17 00:00:00 2001 From: h8d13 Date: Sun, 2 Aug 2026 01:55:12 +0200 Subject: [PATCH 02/25] feat(core): add optional `bg_color` keys * build: add `make check` to `Makefile` and `PKGBUILD` --- Makefile | 9 +++++++++ PKGBUILD | 9 +++++++++ cfg/config_check.c | 21 +++++++++++++++++++++ cfg/ragnar.cfg | 4 ++++ src/config.c | 8 ++++++++ src/funcs.h | 6 ++++++ src/ragnar.c | 45 +++++++++++++++++++++++++++++++++++++++++++++ src/structs.h | 9 +++++++++ 8 files changed, 111 insertions(+) create mode 100644 cfg/config_check.c diff --git a/Makefile b/Makefile index 974bc2c..c66d402 100644 --- a/Makefile +++ b/Makefile @@ -40,6 +40,15 @@ install: install -Dm644 ragnar.desktop -t $(DESTDIR)$(PREFIX)/share/xsessions install -Dm644 cfg/ragnar.cfg -t $(DESTDIR)$(SYSCONFDIR)/ragnarwm +# the shipped cfg is the only fallback a fresh install has: readconfig +# terminates when neither the user nor the global path parses, so a syntax +# error here means a fresh install cannot start at all. +.PHONY: check +check: + mkdir -p ./bin + $(CC) -o bin/config_check $(ALL_CFLAGS) $(LDFLAGS) cfg/config_check.c -lconfig + ./bin/config_check cfg/ragnar.cfg + .PHONY: clean clean: $(RM) bin/* diff --git a/PKGBUILD b/PKGBUILD index 7a120e3..8cf085d 100644 --- a/PKGBUILD +++ b/PKGBUILD @@ -29,11 +29,13 @@ optdepends=( 'picom: for proper vsync/fix tearing' 'alacritty: default terminal keybind' 'polybar: optional status/desktops bars' + 'feh: optional wallpapers' 'ttf-dejavu: fonts for both the above' 'wireplumber: default volume keybinds' 'brightnessctl: default brightness keybinds' 'playerctl: default media player keybinds' ) + provides=('ragnarwm') options=('!debug') backup=('etc/ragnarwm/ragnar.cfg') @@ -43,6 +45,7 @@ install="${pkgname}.install" source=("${_pkgname}::git+${url}.git#branch=${_branch}") sha256sums=('SKIP') +# TODO add a release flow that bumps on gh automatically pkgver() { cd $_pkgname || exit 1 echo $pkgver @@ -53,6 +56,12 @@ build() { make } +# check the shipped config is not borken +check() { + cd $_pkgname || exit 1 + make check +} + package() { cd $_pkgname || exit 1 # single source of truth: same install target ./install.sh drives. diff --git a/cfg/config_check.c b/cfg/config_check.c new file mode 100644 index 0000000..71f895e --- /dev/null +++ b/cfg/config_check.c @@ -0,0 +1,21 @@ +#include +#include + +int main(int argc, char** argv) { + if(argc != 2) { + fprintf(stderr, "usage: %s \n", argv[0]); + return 2; + } + + config_t cfg; + config_init(&cfg); + if(config_read_file(&cfg, argv[1])) { + config_destroy(&cfg); + printf("sample config is valid."); + return 0; + } + + fprintf(stderr, "%s:%d: %s\n", argv[1], config_error_line(&cfg), config_error_text(&cfg)); + config_destroy(&cfg); + return 1; +} diff --git a/cfg/ragnar.cfg b/cfg/ragnar.cfg index d7a9182..434c1ce 100644 --- a/cfg/ragnar.cfg +++ b/cfg/ragnar.cfg @@ -208,6 +208,10 @@ max_struts = 8; # Specifies the cursor image to use for the root window cursor_image = "arrow"; +# Specifies the colour the root window is painted with. +# Optional: when unset does nothing. (Let ex: `feh` take-over) +# bg_color = 0x1A1A1A; + # Logging (log file is ~/.ragnarwm.log) # Specifies whether or not to log messages diff --git a/src/config.c b/src/config.c index 69387e3..916a3ef 100644 --- a/src/config.c +++ b/src/config.c @@ -677,6 +677,11 @@ readconfig(state_t* s, config_data_t* data) { success = cfgreadstr(s, (const char**)&data->cursorimage, "cursor_image"); + // optional, unset means the root window is left alone + int32_t bgcolor = 0; + data->bgcolor_set = (bool)config_lookup_int(&cfghndl, "bg_color", &bgcolor); + data->bgcolor = (uint32_t)bgcolor; + // optional, the session's layout is used when unset data->kblayout = NULL; const char* kblayout = NULL; @@ -717,6 +722,9 @@ reloadconfig(state_t* s, config_data_t* data) { // Load the default root cursor image loaddefaultcursor(s); + // Repaint the root background, the image or colour may have changed + setbackground(s); + // Grab the window manager's keybinds grabkeybinds(s); diff --git a/src/funcs.h b/src/funcs.h index 27bb178..c4cb3b6 100644 --- a/src/funcs.h +++ b/src/funcs.h @@ -531,6 +531,12 @@ void applykblayout(state_t* s); * */ void loaddefaultcursor(state_t* s); +/** + * @brief Paints the root window with the configured background colour. + * Does nothing when 'bg_color' is unset. + * */ +void setbackground(state_t* s); + void setcursorhidden(state_t* s, bool hidden); /** diff --git a/src/ragnar.c b/src/ragnar.c index 1f32ab8..4dead7c 100644 --- a/src/ragnar.c +++ b/src/ragnar.c @@ -181,6 +181,9 @@ setup(state_t* s) { // Load the default root cursor image loaddefaultcursor(s); + // Paint the root background before any window is mapped over it + setbackground(s); + // Apply the configured keyboard layout before resolving keybinds applykblayout(s); @@ -2367,6 +2370,48 @@ loaddefaultcursor(state_t* s) { setcursorhidden(s, true); } +/** + * @brief Paints the root window with the configured background colour. + * Does nothing when 'bg_color' is unset, leaving the root window to + * whatever external wallpaper setter the user runs. + * */ +void +setbackground(state_t* s) { + if(!s->config.bgcolor_set) return; + + uint16_t w = s->screen->width_in_pixels, h = s->screen->height_in_pixels; + + // a pixmap rather than just XCB_CW_BACK_PIXEL: compositors read the root + // pixmap off _XROOTPMAP_ID to blend and blur against, and a background + // pixel leaves them nothing to point at. + xcb_pixmap_t pm = xcb_generate_id(s->con); + xcb_create_pixmap(s->con, s->screen->root_depth, pm, s->root, w, h); + + xcb_gcontext_t gc = xcb_generate_id(s->con); + xcb_create_gc(s->con, gc, pm, XCB_GC_FOREGROUND, &s->config.bgcolor); + xcb_rectangle_t rect = { 0, 0, w, h }; + xcb_poly_fill_rectangle(s->con, pm, gc, 1, &rect); + xcb_free_gc(s->con, gc); + + xcb_change_window_attributes(s->con, s->root, XCB_CW_BACK_PIXMAP, &pm); + xcb_clear_area(s->con, 0, s->root, 0, 0, 0, 0); + // both names, different tools historically picked one or the other + xcb_change_property(s->con, XCB_PROP_MODE_REPLACE, s->root, + getatom(s, "_XROOTPMAP_ID"), XCB_ATOM_PIXMAP, 32, 1, &pm); + xcb_change_property(s->con, XCB_PROP_MODE_REPLACE, s->root, + getatom(s, "ESETROOT_PMAP_ID"), XCB_ATOM_PIXMAP, 32, 1, &pm); + xcb_flush(s->con); + + // the root stops referencing the old pixmap only once it points at the new + // one, so the free happens here and not before. close down mode is left + // alone on purpose: RetainPermanent is per connection, it would outlive + // ragnar with every frame window it owns and not just this pixmap. + if(s->bgpixmap) { + xcb_free_pixmap(s->con, s->bgpixmap); + } + s->bgpixmap = pm; +} + /** * @brief Hides or shows the cursor (no-op without XFixes). The cursor * starts hidden and appears on the first real pointer motion. diff --git a/src/structs.h b/src/structs.h index 2cdcc97..fd0673d 100644 --- a/src/structs.h +++ b/src/structs.h @@ -496,6 +496,11 @@ typedef struct { // optional startup keyboard layout, setxkbmap syntax ("be nodeadkeys") char* kblayout; + + // optional root window background colour. when unset the root is left + // alone so an external setter (xwallpaper, feh) keeps working. + uint32_t bgcolor; + bool bgcolor_set; } config_data_t; typedef struct { @@ -531,6 +536,10 @@ struct state_t { bool xfixes_ok; bool cursorhidden; + // root background pixmap ragnar owns, freed when it paints the next one. + // 0 when it has never painted one. + xcb_pixmap_t bgpixmap; + client_t* focus; popup_list_t popups; From c3eb151f6bfa44815e6b21843c409c07e377b7f7 Mon Sep 17 00:00:00 2001 From: h8d13 Date: Sun, 2 Aug 2026 02:12:51 +0200 Subject: [PATCH 03/25] fix(reload): + show working example in docs --- PKGBUILD | 2 +- README.md | 1 + src/ragnar.c | 26 +++++++++++++++++++++++--- 3 files changed, 25 insertions(+), 4 deletions(-) diff --git a/PKGBUILD b/PKGBUILD index 8cf085d..fa9ecf9 100644 --- a/PKGBUILD +++ b/PKGBUILD @@ -29,7 +29,7 @@ optdepends=( 'picom: for proper vsync/fix tearing' 'alacritty: default terminal keybind' 'polybar: optional status/desktops bars' - 'feh: optional wallpapers' + 'feh: optional set wallpaper for X displays' 'ttf-dejavu: fonts for both the above' 'wireplumber: default volume keybinds' 'brightnessctl: default brightness keybinds' diff --git a/README.md b/README.md index 95d877c..e5dde7f 100644 --- a/README.md +++ b/README.md @@ -84,6 +84,7 @@ The configuration is loaded on startup and can be reloaded while the window mana Example contents of `.xinitrc`: ```console +feh --bg-fill ~/Downloads/nerd.jpg & picom -b; polybar & exec ragnar ``` diff --git a/src/ragnar.c b/src/ragnar.c index 4dead7c..c6d42e1 100644 --- a/src/ragnar.c +++ b/src/ragnar.c @@ -2372,12 +2372,32 @@ loaddefaultcursor(state_t* s) { /** * @brief Paints the root window with the configured background colour. - * Does nothing when 'bg_color' is unset, leaving the root window to - * whatever external wallpaper setter the user runs. + * Does nothing when 'bg_color' is unset and ragnar never painted, leaving + * the root window to whatever external wallpaper setter the user runs. + * Drops back to black when the key is removed across a reload. * */ void setbackground(state_t* s) { - if(!s->config.bgcolor_set) return; + if(!s->config.bgcolor_set) { + // an unset key on a root ragnar never painted means an external setter + // owns it, so leave it alone. one it did paint has to be handed back, + // and black is what an untouched root shows. + if(!s->bgpixmap) return; + + uint32_t black = 0x000000; + xcb_change_window_attributes(s->con, s->root, XCB_CW_BACK_PIXEL, &black); + xcb_clear_area(s->con, 0, s->root, 0, 0, 0, 0); + // drop these before the pixmap goes, a compositor still holding the id + // would otherwise be pointed at freed storage + xcb_delete_property(s->con, s->root, getatom(s, "_XROOTPMAP_ID")); + xcb_delete_property(s->con, s->root, getatom(s, "ESETROOT_PMAP_ID")); + xcb_flush(s->con); + + xcb_free_pixmap(s->con, s->bgpixmap); + s->bgpixmap = 0; + xcb_flush(s->con); + return; + } uint16_t w = s->screen->width_in_pixels, h = s->screen->height_in_pixels; From 57ca7068bb0546e6c58f24486b275a9e32956d99 Mon Sep 17 00:00:00 2001 From: h8d13 Date: Sun, 2 Aug 2026 02:21:36 +0200 Subject: [PATCH 04/25] chore(cfg): clean-up dead left-over stray keys from bace32d --- cfg/ragnar.cfg | 22 ---------------------- 1 file changed, 22 deletions(-) diff --git a/cfg/ragnar.cfg b/cfg/ragnar.cfg index 434c1ce..54bc7bc 100644 --- a/cfg/ragnar.cfg +++ b/cfg/ragnar.cfg @@ -120,28 +120,6 @@ num_desktops = 9; # in order. desktop_names = ["1", "2", "3", "4", "5", "6", "7", "8", "9"]; -# Specifies whether or not server-side window -# decorations should be enabled. -use_decoration = false; -# Specifies whether or not server-side titlebars -# should be shown on startup. (Ignored when -# use_decoration is set to false.) -show_titlebars_init = false; - -# Specifies the height (in pixels) of the -# titlebar of client windows. -titlebar_height = 30 -# Specifies the color of titlebars of -# client windows -titlebar_color = 0xffffff; - -# Specifies the color of the font that is used -# across the window manager's UI -font_color = 0xff0000ff; -# Specifies the path to the font file to use as -# the window manager's font -font_path = "/usr/share/fonts/TTF/JetBrainsMonoNerdFont-Bold.ttf"; - # Specifies the area that the master window takes # up in master-slave layouts initially. # (in 0.0-1.0 %) From 2dfba1e589f81ee159548cc616bd58281040fbb7 Mon Sep 17 00:00:00 2001 From: h8d13 Date: Sun, 2 Aug 2026 02:28:35 +0200 Subject: [PATCH 05/25] chore(build): add missing `dmenu` to opt --- PKGBUILD | 1 + 1 file changed, 1 insertion(+) diff --git a/PKGBUILD b/PKGBUILD index fa9ecf9..d2f3b79 100644 --- a/PKGBUILD +++ b/PKGBUILD @@ -27,6 +27,7 @@ depends=( makedepends=('git' 'make' 'gcc') optdepends=( 'picom: for proper vsync/fix tearing' + 'dmenu: default launcher' 'alacritty: default terminal keybind' 'polybar: optional status/desktops bars' 'feh: optional set wallpaper for X displays' From 009b639b868b6de2d7415a94a48feb4f6aed30be Mon Sep 17 00:00:00 2001 From: h8d13 Date: Sun, 2 Aug 2026 02:35:23 +0200 Subject: [PATCH 06/25] Add hint in readme for hot-reload --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index e5dde7f..3e9d8a5 100644 --- a/README.md +++ b/README.md @@ -79,7 +79,7 @@ Ragnar uses `libconfig` to load an external configuration file: ~/.config/ragnarwm/ragnar.cfg ``` -The configuration is loaded on startup and can be reloaded while the window manager is running, typically through a keybinding. +The configuration is loaded on startup and can be reloaded while the window manager is running, typically through a keybinding (Super+C). Example contents of `.xinitrc`: From 8711e813614b89f89f9d7ec4254f54e7db558ecd Mon Sep 17 00:00:00 2001 From: h8d13 Date: Sun, 2 Aug 2026 02:39:10 +0200 Subject: [PATCH 07/25] correct usage --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 3e9d8a5..a2ee472 100644 --- a/README.md +++ b/README.md @@ -84,7 +84,7 @@ The configuration is loaded on startup and can be reloaded while the window mana Example contents of `.xinitrc`: ```console -feh --bg-fill ~/Downloads/nerd.jpg & +feh --bg-fill ~/Downloads/nerd.jpg; picom -b; polybar & exec ragnar ``` From ce2e85484d6a9665a05dee6ce717e33d8ddf1a7b Mon Sep 17 00:00:00 2001 From: h8d13 Date: Sun, 2 Aug 2026 10:07:13 +0200 Subject: [PATCH 08/25] chore(cfg): remove one more dead key * add numbering to cfg keys * ignore src/ragnar/ Signed-off-by: h8d13 --- .gitignore | 1 + PKGBUILD | 4 ++-- cfg/ragnar.cfg | 47 +++++++++++++++++++++++++++++++++++++++++++++-- src/config.c | 10 +--------- src/ragnar.c | 7 ------- src/structs.h | 2 -- 6 files changed, 49 insertions(+), 22 deletions(-) diff --git a/.gitignore b/.gitignore index 01de4a4..fe6cae4 100644 --- a/.gitignore +++ b/.gitignore @@ -5,6 +5,7 @@ bin/ pkg/ /ragnar/ +src/ragnar/ **.tar.zst .gdb.log compile_commands.json diff --git a/PKGBUILD b/PKGBUILD index d2f3b79..57d990a 100644 --- a/PKGBUILD +++ b/PKGBUILD @@ -7,8 +7,8 @@ pkgver='2' pkgrel=1 pkgdesc="Minimal, flexible & user-friendly X tiling window manager" arch=('x86_64') -url="https://github.com/cococry/ragnar" -#url="https://github.com/h8d13/ragnar" +#url="https://github.com/cococry/ragnar" +url="https://github.com/h8d13/ragnar" _branch="main" license=('GPL') groups=() diff --git a/cfg/ragnar.cfg b/cfg/ragnar.cfg index 54bc7bc..d8c2f3c 100644 --- a/cfg/ragnar.cfg +++ b/cfg/ragnar.cfg @@ -58,6 +58,7 @@ # - setfloatingmode # - updatebarslayout # - cycledownlayout +# - cycleuplayout # - addmasterlayout # - removemasterlayout # - incmasterarealayout @@ -73,98 +74,131 @@ # - cyclefocusmonitordown # - cyclefocusmonitorup # - togglescratchpad +# - reloadconfigfile # # ---------------------- # NOTE: For all key options, see the functions at: # https://github.com/cococry/ragnar/blob/main/src/structs.h#L17 +# The same list is the dispatch table 'keycbmappings' in src/config.c; a +# name that is in one and not the other is a bind that silently does +# nothing. # ---------------------- # +# cfg -> src resolution +# ---------------------- +# Every key below carries an (N) marker naming the config_data_t field +# (src/structs.h) it lands in. N is the order readconfig() (src/config.c) +# reads the key, not the order it appears here, so the numbers run out of +# sequence going down the file. +# ---------------------- # Specifies the width of the border around client # windows +# (4) win_border_width = 2; # Specifies the color of the border around client # windows +# (5) win_border_color = 0x3B3B3B; # Specifies the color of the border around # selected/focused client windows +# (6) win_border_color_selected = 0xE6DFDC; # Specifies the main modifier key that is # used to execute window manager shortcuts +# (7) mod_key = "Super"; # Specifies the modifier key that is used # to interact with clients windows +# (8) win_mod = "Super"; # Specfies the mouse button that needs to be # held in order to move client windows +# (9) move_button = "LeftMouse"; # Specfies the mouse button that needs to be # held in order to resize client windows +# (10) resize_button = "RightMouse"; # Specifies the keyboard layout that is applied # on startup (setxkbmap syntax, e.g "be" or # "be nodeadkeys"). Uses the session's layout # when unset. +# (27) # keyboard_layout = "us"; # Specifies the desktop index that is initially # selected on every monitor +# (11) initial_desktop = 0; # Specfies the number of allocated virtual # desktops +# (2) num_desktops = 9; # Specifies the name of every virtual desktop # in order. +# (12) desktop_names = ["1", "2", "3", "4", "5", "6", "7", "8", "9"]; # Specifies the area that the master window takes # up in master-slave layouts initially. # (in 0.0-1.0 %) +# (13) layout_master_area = 0.5; # Specifies the minimum area that master windows # need to take up in master-slave layouts # (in 0.0-1.0 %) +# (14) layout_master_area_min = 0.1; # Specifies the maximum a2rea that master windows # can take up in master-slave layouts # (in 0.0-1.0 %) +# (15) layout_master_area_max = 0.9; # Specifies the amount that the master area changes/steps # when it is decreased/increased. # (in 0.0-1.0 %) +# (16) layout_master_area_step = 0.1; # Specifies the amount that areas of windows # within layouts change when they are increased/decreased # (in px) +# (17) layout_size_step = 100.0; # Specifies the minimum area that windows within # layouts need to take up # (in px) +# (18) layout_size_min = 150.0; # Specifies the amount that windows are moved # by when using 'move' shortcuts for floating windows # (in px) +# (19) key_win_move_step = 100.0; # Specifies the initial gap between windows # within layouts (in px) +# (20) win_layout_gap = 5; # Specifies the maximum gap that windows within # layouts can have around each other (in px) +# (21) win_layout_gap_max = 150; # Specifies the amount that the gap between # non-floating windows changes when it's # decreased/increased (in px) # (in px) +# (22) win_layout_gap_step = 5; # Specifies the layout that is initially used # for every virtual desktop +# (23) initial_layout = "LayoutTiledMaster"; # Advanced Configuration @@ -175,39 +209,48 @@ initial_layout = "LayoutTiledMaster"; # performance. Especially on high polling rate mouses, # lag can be very noticable when not throtteling motion # notify events. +# (24) motion_notify_debounce_fps = 60; # Specifies the maximum number of 'strut'-window that # the window manager can capture. Struts are information # about window positions and sizes that are used to correctly # establish window layouts with bars or other status windows. +# (1) max_struts = 8; # Specifies the cursor image to use for the root window +# (25) cursor_image = "arrow"; # Specifies the colour the root window is painted with. # Optional: when unset does nothing. (Let ex: `feh` take-over) +# (26) # bg_color = 0x1A1A1A; -# Logging (log file is ~/.ragnarwm.log) - # Specifies whether or not to log messages # to the console +# (28) log_messages = true; # Specifies whether or not to log messages # to the log file +# (29) +# builds it as $HOME/.ragnarwm.log should_log_to_file = true; # Specifies the maximum number of scratchpads # that can be allocated +# (3) max_scratchpads = 10; # -------------------------------- # =========== Key bindings =========== +# (30) +# four sub-keys per entry: mod (optional), key, do, and then cmd / i as the +# passthrough payload. Any other sub-key here is ignored silently. keybinds = ( { mod = "Super"; diff --git a/src/config.c b/src/config.c index 916a3ef..65673b6 100644 --- a/src/config.c +++ b/src/config.c @@ -654,8 +654,6 @@ readconfig(state_t* s, config_data_t* data) { data->desktopnames = cfgevalstrarr(s, "desktop_names"); success = data->desktopnames != NULL; - success = cfgreadbool(s, &data->usedecoration, "use_decoration"); - success = cfgreadfloat(s, &data->layoutmasterarea, "layout_master_area"); success = cfgreadfloat(s, &data->layoutmasterarea_min, "layout_master_area_min"); success = cfgreadfloat(s, &data->layoutmasterarea_max, "layout_master_area_max"); @@ -714,8 +712,6 @@ void reloadconfig(state_t* s, config_data_t* data) { destroyconfig(); - bool using_decoration = s->config.usedecoration; - initconfig(s); readconfig(s, data); @@ -728,11 +724,7 @@ reloadconfig(state_t* s, config_data_t* data) { // Grab the window manager's keybinds grabkeybinds(s); - if(!using_decoration) { - s->config.usedecoration = false; - } - - // Reload struts + // Reload struts s->nwinstruts = 0; getwinstruts(s, s->root); diff --git a/src/ragnar.c b/src/ragnar.c index c6d42e1..fa9a314 100644 --- a/src/ragnar.c +++ b/src/ragnar.c @@ -3429,13 +3429,6 @@ evpropertynotify(state_t* s, xcb_generic_event_t* ev) { if(prop_ev->atom == s->ewmh_atoms[EWMHwindowType]) { setwintype(s, cl); } - if(s->config.usedecoration) { - if(prop_ev->atom == s->ewmh_atoms[EWMHname]) { - if(cl->name) - free(cl->name); - cl->name = getclientname(s, cl); - } - } } xcb_flush(s->con); } diff --git a/src/structs.h b/src/structs.h index fd0673d..8081bc2 100644 --- a/src/structs.h +++ b/src/structs.h @@ -465,8 +465,6 @@ typedef struct { char** desktopnames; - bool usedecoration; - double layoutmasterarea; double layoutmasterarea_min; double layoutmasterarea_max; From 2367be922b1bd0d6d2d0e0fc65a2c90dbe65146c Mon Sep 17 00:00:00 2001 From: h8d13 Date: Sun, 2 Aug 2026 10:16:36 +0200 Subject: [PATCH 09/25] chore(build): add `make package` verb for Arch builds Signed-off-by: h8d13 --- Makefile | 9 +++++++++ PKGBUILD | 4 ++-- 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/Makefile b/Makefile index c66d402..d6a9a90 100644 --- a/Makefile +++ b/Makefile @@ -49,6 +49,15 @@ check: $(CC) -o bin/config_check $(ALL_CFLAGS) $(LDFLAGS) cfg/config_check.c -lconfig ./bin/config_check cfg/ragnar.cfg +# makepkg's $srcdir defaults to ./src, which collides with this project's +# own src/. build out of tree so the clone never lands in the worktree. +BUILDDIR ?= /tmp/makepkg +SRCDEST ?= $(HOME)/.cache/makepkg/sources + +.PHONY: package +package: + BUILDDIR=$(BUILDDIR) SRCDEST=$(SRCDEST) makepkg -f + .PHONY: clean clean: $(RM) bin/* diff --git a/PKGBUILD b/PKGBUILD index 57d990a..d2f3b79 100644 --- a/PKGBUILD +++ b/PKGBUILD @@ -7,8 +7,8 @@ pkgver='2' pkgrel=1 pkgdesc="Minimal, flexible & user-friendly X tiling window manager" arch=('x86_64') -#url="https://github.com/cococry/ragnar" -url="https://github.com/h8d13/ragnar" +url="https://github.com/cococry/ragnar" +#url="https://github.com/h8d13/ragnar" _branch="main" license=('GPL') groups=() From 85078ae3f9943baeee8c0ea82a1d7e20f57cfaa0 Mon Sep 17 00:00:00 2001 From: h8d13 Date: Tue, 4 Aug 2026 00:37:09 +0200 Subject: [PATCH 10/25] chore(build): add `-xset` to opt depends --- PKGBUILD | 1 + 1 file changed, 1 insertion(+) diff --git a/PKGBUILD b/PKGBUILD index d2f3b79..8d23b8f 100644 --- a/PKGBUILD +++ b/PKGBUILD @@ -35,6 +35,7 @@ optdepends=( 'wireplumber: default volume keybinds' 'brightnessctl: default brightness keybinds' 'playerctl: default media player keybinds' + 'xorg-xset: for screenblanking idle/DPMS' ) provides=('ragnarwm') From 2f5b07592a2e6112c0bf5cf07f95b307c6b11b19 Mon Sep 17 00:00:00 2001 From: h8d13 Date: Tue, 4 Aug 2026 00:39:39 +0200 Subject: [PATCH 11/25] add example to docs, never go idle --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index a2ee472..8d23f15 100644 --- a/README.md +++ b/README.md @@ -86,6 +86,7 @@ Example contents of `.xinitrc`: ```console feh --bg-fill ~/Downloads/nerd.jpg; picom -b; polybar & +xset s off -dpms exec ragnar ``` From 4f9a9271f00acfd6bf93952e6c71ab18c9ff65ce Mon Sep 17 00:00:00 2001 From: h8d13 Date: Tue, 4 Aug 2026 00:49:16 +0200 Subject: [PATCH 12/25] correct usage --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 8d23f15..0e2b70c 100644 --- a/README.md +++ b/README.md @@ -85,8 +85,8 @@ Example contents of `.xinitrc`: ```console feh --bg-fill ~/Downloads/nerd.jpg; +xset s off -dpms; picom -b; polybar & -xset s off -dpms exec ragnar ``` From cba1b055010fd6a21cd37d1dbd9f6f5565ebb18b Mon Sep 17 00:00:00 2001 From: h8d13 Date: Tue, 4 Aug 2026 02:44:22 +0200 Subject: [PATCH 13/25] chore(build): correct `picom` description --- PKGBUILD | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/PKGBUILD b/PKGBUILD index 8d23b8f..d1e3960 100644 --- a/PKGBUILD +++ b/PKGBUILD @@ -26,7 +26,7 @@ depends=( ) makedepends=('git' 'make' 'gcc') optdepends=( - 'picom: for proper vsync/fix tearing' + 'picom: additional compositor features' 'dmenu: default launcher' 'alacritty: default terminal keybind' 'polybar: optional status/desktops bars' From 1b1b497e84dbc82a8d0290fa8c4112baa71a9a6f Mon Sep 17 00:00:00 2001 From: h8d13 Date: Tue, 4 Aug 2026 02:45:12 +0200 Subject: [PATCH 14/25] chore(docs): add hint to close --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 0e2b70c..e22385f 100644 --- a/README.md +++ b/README.md @@ -90,7 +90,7 @@ picom -b; polybar & exec ragnar ``` -Then simply; `startx` +Then simply; `startx` (and `pkill xinit` to stop it.) > By default it uses `alacritty` (Super+Return) and `dmenu` (Super+S), if you haven't edited these yet. > Both of these need fonts file; for instance `ttf-dejavu` From b824bab0c644bb6b424fc25e48fa1ff1ab373caf Mon Sep 17 00:00:00 2001 From: h8d13 Date: Tue, 4 Aug 2026 11:46:22 +0200 Subject: [PATCH 15/25] fix(ewmh): set _NET_ACTIVE_WINDOW on root, publish WM_STATE * _NET_ACTIVE_WINDOW was written to the client window instead of the root, so it read as absent to panels and pagers * WM_STATE was interned and read but never set; reparenting WMs must publish it (ICCCM 4.1.3.1) or DND target lookup stops at the frame * getstate: read the full 32-bit CARDINAL, not its first byte --- src/funcs.h | 9 +++++++++ src/ragnar.c | 39 ++++++++++++++++++++++++++++++++++++--- 2 files changed, 45 insertions(+), 3 deletions(-) diff --git a/src/funcs.h b/src/funcs.h index c4cb3b6..d40f20e 100644 --- a/src/funcs.h +++ b/src/funcs.h @@ -283,6 +283,15 @@ void frameclient(state_t* s, client_t* cl); */ void unframeclient(state_t* s, client_t* cl); +/** + * @brief Sets the ICCCM WM_STATE property on a client's window. + * + * @param s The window manager's state + * @param cl The client to set the state on + * @param state One of XCB_ICCCM_WM_STATE_{WITHDRAWN,NORMAL,ICONIC} + */ +void setwmstate(state_t* s, client_t* cl, uint32_t state); + /** * @brief Removes the focus from client's window by setting the * X input focus to the root window and unsetting the highlight color diff --git a/src/ragnar.c b/src/ragnar.c index fa9a314..beae140 100644 --- a/src/ragnar.c +++ b/src/ragnar.c @@ -311,11 +311,35 @@ getstate(state_t* s, xcb_window_t w) return -1; } - result = *((unsigned char *) xcb_get_property_value(prop_reply)); + result = *((uint32_t *) xcb_get_property_value(prop_reply)); free(prop_reply); return result; } +/** + * @brief Sets the ICCCM WM_STATE property on a client's window. + * + * Mandatory for reparenting window managers (ICCCM 4.1.3.1): WM_STATE is + * what marks a window as a real client toplevel rather than a WM frame. + * Drag-and-drop target lookup, pagers and xprop/xwininfo all walk down + * from the root looking for it, and stop at the frame when it is absent. + * + * @param s The window manager's state + * @param cl The client to set the state on + * @param state One of XCB_ICCCM_WM_STATE_{WITHDRAWN,NORMAL,ICONIC} + */ +void +setwmstate(state_t* s, client_t* cl, uint32_t state) { + if(!cl) { + return; + } + // WM_STATE is [state, icon window]; ragnar provides no icon window. + // uint32_t, not long: xcb writes the raw bytes with no 64->32 conversion. + uint32_t data[2] = { state, XCB_NONE }; + xcb_change_property(s->con, XCB_PROP_MODE_REPLACE, cl->win, s->wm_atoms[WMstate], + s->wm_atoms[WMstate], 32, 2, data); +} + bool wait_for_mapped(state_t* s, xcb_window_t win) { for (int i = 0; i < 10; i++) { // Try up to 10 times @@ -1117,8 +1141,9 @@ setxfocus(state_t* s, client_t* cl) { // Set input focus to client xcb_set_input_focus(s->con, XCB_INPUT_FOCUS_POINTER_ROOT, cl->win, XCB_CURRENT_TIME); - // Set active window hint - xcb_change_property(s->con, XCB_PROP_MODE_REPLACE, cl->win, s->ewmh_atoms[EWMHactiveWindow], + // Set active window hint. EWMH puts this on the root window, not on the + // client; unfocusclient() already deletes it from the root. + xcb_change_property(s->con, XCB_PROP_MODE_REPLACE, s->root, s->ewmh_atoms[EWMHactiveWindow], XCB_ATOM_WINDOW, 32, 1, &cl->win); // Raise take-focus event on the client @@ -1253,6 +1278,7 @@ void hideclient(state_t* s, client_t* cl) { cl->ignoreunmap = true; cl->hidden = true; + setwmstate(s, cl, XCB_ICCCM_WM_STATE_ICONIC); xcb_unmap_window(s->con, cl->frame); } @@ -1265,6 +1291,7 @@ hideclient(state_t* s, client_t* cl) { void showclient(state_t* s, client_t* cl) { cl->hidden = false; + setwmstate(s, cl, XCB_ICCCM_WM_STATE_NORMAL); xcb_map_window(s->con, cl->frame); } @@ -2870,6 +2897,9 @@ evunmapnotify(state_t* s, xcb_generic_event_t* ev) { if(cl->is_scratchpad) { removescratchpad(s, cl->scratchpad_index); } + // Window is going away but still exists here, so withdraw it properly. + // Not done on DestroyNotify: the window is already gone by then. + setwmstate(s, cl, XCB_ICCCM_WM_STATE_WITHDRAWN); unframeclient(s, cl); } else { xcb_unmap_window(s->con, unmap_ev->window); @@ -3528,6 +3558,9 @@ addclient(state_t* s, client_t** clients, xcb_window_t win) { // Create frame window for the client frameclient(s, cl); + // Publish WM_STATE now that the window is framed and managed + setwmstate(s, cl, XCB_ICCCM_WM_STATE_NORMAL); + // Insert the new client at the beginning of the list cl->next = *clients; // Update the head of the list to the new client From 6d0630016c2555e3991437bd9691b5e1f6b9c37d Mon Sep 17 00:00:00 2001 From: h8d13 Date: Tue, 4 Aug 2026 11:49:05 +0200 Subject: [PATCH 16/25] fix(core): clang-tidy warning use integer ms for motion throttle timestamps --- src/ragnar.c | 6 +++++- src/structs.h | 4 +++- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/src/ragnar.c b/src/ragnar.c index beae140..a62520f 100644 --- a/src/ragnar.c +++ b/src/ragnar.c @@ -3172,7 +3172,11 @@ evmotionnotify(state_t* s, xcb_generic_event_t* ev) { // 0 disables the throttle rather than dividing by zero. uint32_t curtime = motion_ev->time; uint32_t fps = s->config.motion_notify_debounce_fps; - if (fps && (curtime - s->lastmotiontime) <= (1000 / fps)) { + // Unsigned subtraction is modular, so this stays correct across the + // ~49.7 day timestamp wrap. Multiply rather than divide by fps to keep + // the threshold exact; widen first so a large delta cannot overflow. + uint32_t delta = curtime - s->lastmotiontime; + if (fps && (uint64_t)delta * fps < 1000) { return; } s->lastmotiontime = curtime; diff --git a/src/structs.h b/src/structs.h index 8081bc2..9b91ff7 100644 --- a/src/structs.h +++ b/src/structs.h @@ -518,7 +518,9 @@ struct state_t { xcb_window_t root; xcb_screen_t* screen; - float lastexposetime, lastmotiontime; + // X server timestamps: milliseconds, wrapping. Must stay uint32_t; + // a float only holds consecutive ms up to ~4.7h of server uptime. + uint32_t lastexposetime, lastmotiontime; // modifier bit num lock lives on, looked up at grab time. keybinds are // grabbed with and without it, and it is masked out when matching. From 1b76a76f41e7761538f17ae21cf91cde894ed825 Mon Sep 17 00:00:00 2001 From: h8d13 Date: Tue, 4 Aug 2026 12:05:07 +0200 Subject: [PATCH 17/25] fix(desktops): wrap cycledesktopdown to the last slot mirror logic from super O/P: * from desktop 0, prev jumped to ninit-1 (desktop 1 with two initialized), so prev moved forward * bound on maxdesktops like cycledesktopup does; desktops initialize lazily so an empty slot is a valid destination --- src/keycallbacks.h | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/src/keycallbacks.h b/src/keycallbacks.h index 8db43d6..9d004fd 100644 --- a/src/keycallbacks.h +++ b/src/keycallbacks.h @@ -147,14 +147,10 @@ inline void cycledesktopdown(state_t* s, passthrough_data_t data) { if(newdesktop - 1 >= 0) { newdesktop--; } else { - uint32_t ninit = 0; - for(uint32_t i = 0; i < s->monfocus->desktopcount; i++) { - if(!s->monfocus->activedesktops[i].init) continue; - ninit++; - } - newdesktop = ninit - 1; - - + // Wrap to the last slot, mirroring cycledesktopup's wrap to 0. + // Bound on maxdesktops, not on the initialized count: desktops are + // initialized lazily, so an empty slot is a valid destination. + newdesktop = (int32_t)s->config.maxdesktops - 1; } switchdesktop(s, (passthrough_data_t){.i = newdesktop}); } From 35b7ee39b044ddb97afc5dfd34d36fcb4d1193be Mon Sep 17 00:00:00 2001 From: h8d13 Date: Tue, 4 Aug 2026 12:08:56 +0200 Subject: [PATCH 18/25] explicit cast --- src/keycallbacks.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/keycallbacks.h b/src/keycallbacks.h index 9d004fd..036e5c5 100644 --- a/src/keycallbacks.h +++ b/src/keycallbacks.h @@ -188,7 +188,7 @@ inline void cyclefocusdesktopdown(state_t* s, passthrough_data_t data) { if(new_desktop - 1 >= 0) { new_desktop--; } else { - new_desktop = s->config.maxdesktops - 1; + new_desktop = (int32_t)s->config.maxdesktops - 1; } switchclientdesktop(s, s->focus, new_desktop); } From 19e8d52bf2ee858083700f6bc9087b17d05f711c Mon Sep 17 00:00:00 2001 From: h8d13 Date: Tue, 4 Aug 2026 12:15:52 +0200 Subject: [PATCH 19/25] fix(keycallbacks): make header self-contained, narrow explicitly * add stdio.h/stdlib.h; perror and EXIT_* resolved only because ragnar.c happened to include them before this header * cast the movefocus * results to float at the v2_t boundary; keywinmove_step is double since libconfig reads floats as double --- src/keycallbacks.h | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/src/keycallbacks.h b/src/keycallbacks.h index 036e5c5..68391ec 100644 --- a/src/keycallbacks.h +++ b/src/keycallbacks.h @@ -3,6 +3,8 @@ #include "funcs.h" #include "structs.h" +#include +#include #include #include #include @@ -695,8 +697,10 @@ inline void movefocusup(state_t* s, passthrough_data_t data) { if(!s->focus || !s->focus->floating) return; v2_t pos = s->focus->area.pos; + // keywinmove_step is a double (libconfig reads floats as double), so + // narrow explicitly at the v2_t boundary rather than implicitly v2_t dest = (v2_t){pos.x, - MIN(MAX(pos.y - s->config.keywinmove_step, s->monfocus->area.pos.y), + (float)MIN(MAX(pos.y - s->config.keywinmove_step, s->monfocus->area.pos.y), s->monfocus->area.pos.y + s->monfocus->area.size.y - s->focus->area.size.y)}; moveclient(s, s->focus, dest, true); @@ -716,8 +720,8 @@ inline void movefocusdown(state_t* s, passthrough_data_t data) { v2_t pos = s->focus->area.pos; v2_t dest = (v2_t){pos.x, - MIN(MAX(pos.y + s->config.keywinmove_step, s->monfocus->area.pos.y), - s->monfocus->area.pos.y + s->monfocus->area.size.y + (float)MIN(MAX(pos.y + s->config.keywinmove_step, s->monfocus->area.pos.y), + s->monfocus->area.pos.y + s->monfocus->area.size.y - s->focus->area.size.y)}; moveclient(s, s->focus, dest, true); ignoreenterlayout(s); @@ -735,8 +739,8 @@ inline void movefocusleft(state_t* s, passthrough_data_t data) { if(!s->focus || !s->focus->floating) return; v2_t pos = s->focus->area.pos; - v2_t dest = (v2_t){MIN(MAX(pos.x - s->config.keywinmove_step, s->monfocus->area.pos.x), - s->monfocus->area.pos.x + s->monfocus->area.size.x + v2_t dest = (v2_t){(float)MIN(MAX(pos.x - s->config.keywinmove_step, s->monfocus->area.pos.x), + s->monfocus->area.pos.x + s->monfocus->area.size.x - s->focus->area.size.x), pos.y}; moveclient(s, s->focus, dest, true); ignoreenterlayout(s); @@ -754,7 +758,7 @@ inline void movefocusright(state_t* s, passthrough_data_t data) { if(!s->focus || !s->focus->floating) return; v2_t pos = s->focus->area.pos; - v2_t dest = (v2_t){MIN(MAX(pos.x + s->config.keywinmove_step, s->monfocus->area.pos.x), + v2_t dest = (v2_t){(float)MIN(MAX(pos.x + s->config.keywinmove_step, s->monfocus->area.pos.x), s->monfocus->area.pos.x + s->monfocus->area.size.x - s->focus->area.size.x), pos.y}; moveclient(s, s->focus, dest, true); From 41c5335549c95ec74bd406fa3546b4843e76b07f Mon Sep 17 00:00:00 2001 From: h8d13 Date: Tue, 4 Aug 2026 12:59:28 +0200 Subject: [PATCH 20/25] fix(log): popen leak and fork-per-line + default to off for release fix(client): name/edges leaks in releaseclient, calloc, NULL guards fix(ipc): per-display socket path XDG instead of /tmp chore(build): debug ASAN targets fix(core): remaining null/UB hardening from the analyzer pass --- .gitignore | 5 +-- Makefile | 20 +++++++++-- api/api.c | 4 +-- api/include/ragnar/api.h | 30 ++++++++++++++++ cfg/ragnar.cfg | 4 +-- src/config.c | 63 ++++++++++++++++++-------------- src/funcs.h | 6 +++- src/ipc/sockets.c | 19 ++++++---- src/ragnar.c | 78 +++++++++++++++++++++++++++++++++------- src/structs.h | 6 +++- 10 files changed, 178 insertions(+), 57 deletions(-) diff --git a/.gitignore b/.gitignore index fe6cae4..e6efd45 100644 --- a/.gitignore +++ b/.gitignore @@ -1,13 +1,14 @@ *.o *.a *.patch +*.data +.*.md +**.tar.zst .cache/ bin/ pkg/ /ragnar/ src/ragnar/ -**.tar.zst .gdb.log compile_commands.json compile_flags.txt -.*.md diff --git a/Makefile b/Makefile index d6a9a90..8dfd710 100644 --- a/Makefile +++ b/Makefile @@ -4,7 +4,13 @@ CC = cc # bought nothing and -ffinite-math-only would let divisions by a zero-sized # monitor fold away instead of showing up. CFLAGS ?= -O2 -ALL_CFLAGS = $(CFLAGS) $(CPPFLAGS) -Wall -Wextra -pedantic -isystem api/include +WARN_CFLAGS = -Wall -Wextra -pedantic -isystem api/include +ALL_CFLAGS = $(CFLAGS) $(CPPFLAGS) $(WARN_CFLAGS) + +# -O1 : for readable ASAN traces. Separate from CFLAGS because that is ?= +# and a distro build must not pick these up. +DEBUG_CFLAGS = -O1 -g -fno-omit-frame-pointer -fsanitize=address,undefined +ALL_DEBUG_CFLAGS = $(DEBUG_CFLAGS) $(CPPFLAGS) $(WARN_CFLAGS) LDLIBS = -lxcb -lxcb-keysyms -lxcb-icccm -lxcb-cursor -lxcb-randr -lxcb-xfixes -lX11 -lX11-xcb -lconfig @@ -20,6 +26,16 @@ all: mkdir -p ./bin $(CC) -o bin/$(BIN) $(ALL_CFLAGS) $(LDFLAGS) $(SRC) $(LDLIBS) +# ASan + UBSan + LSan build, overwrites bin/ragnar. LSan only reports on a +# normal exit, so quit ragnar through its own keybind: a SIGKILL prints +# nothing. .PHONY matters here, the ./debug Xephyr script shares the name +# and make would otherwise call the target up to date. +# make debug && ./debug +.PHONY: debug +debug: + mkdir -p ./bin + $(CC) -o bin/$(BIN) $(ALL_DEBUG_CFLAGS) $(LDFLAGS) $(SRC) $(LDLIBS) + # client-side IPC library. the WM links none of it, only the headers under # api/include are needed to build. opt-in, for writing external clients. .PHONY: api @@ -37,7 +53,6 @@ install-api: .PHONY: install install: install -Dm755 bin/$(BIN) -t $(DESTDIR)$(BINDIR) - install -Dm644 ragnar.desktop -t $(DESTDIR)$(PREFIX)/share/xsessions install -Dm644 cfg/ragnar.cfg -t $(DESTDIR)$(SYSCONFDIR)/ragnarwm # the shipped cfg is the only fallback a fresh install has: readconfig @@ -65,5 +80,4 @@ clean: .PHONY: uninstall uninstall: $(RM) $(DESTDIR)$(BINDIR)/$(BIN) - $(RM) $(DESTDIR)$(PREFIX)/share/xsessions/ragnar.desktop $(RM) -r $(DESTDIR)$(SYSCONFDIR)/ragnarwm diff --git a/api/api.c b/api/api.c index f8865c9..d39d3eb 100644 --- a/api/api.c +++ b/api/api.c @@ -106,10 +106,10 @@ clientinit(socket_client_t* cl) { memset(&cl->addr, 0, sizeof(struct sockaddr_un)); cl->addr.sun_family = AF_UNIX; - strncpy(cl->addr.sun_path, SOCKPATH, sizeof(cl->addr.sun_path) - 1); + rg_socket_path(cl->addr.sun_path, sizeof(cl->addr.sun_path)); if(s_logging) { - printf("ragnar api: created unix domain socket on: '%s'\n", SOCKPATH); + printf("ragnar api: created unix domain socket on: '%s'\n", cl->addr.sun_path); } return 0; } diff --git a/api/include/ragnar/api.h b/api/include/ragnar/api.h index 8fd0f50..8e666f5 100644 --- a/api/include/ragnar/api.h +++ b/api/include/ragnar/api.h @@ -2,6 +2,36 @@ #include #include +#include +#include + +/* + * IPC socket path, computed rather than fixed. XDG_RUNTIME_DIR scopes it + * per user and DISPLAY per X server: one hardcoded path meant a second + * instance (a nested Xephyr session, a second seat) unlinked the running + * WM's socket out from under it at startup + * + * static inline in the public header on purpose. The WM links none of + * api.c and only includes these headers, so this is the single place both + * sides can share the path and not drift apart + */ +static inline void +rg_socket_path(char* buf, size_t len) { + const char* dir = getenv("XDG_RUNTIME_DIR"); + if(!dir) { + dir = "/tmp"; + } + const char* display = getenv("DISPLAY"); + if(!display) { + display = ":0"; + } + // DISPLAY is ":1" or ":0.0"; drop the leading colon so the result reads + // as ragnar-1.socket rather than ragnar-:1.socket + if(*display == ':') { + display++; + } + snprintf(buf, len, "%s/ragnar-%s.socket", dir, display); +} typedef int32_t RgWindow; diff --git a/cfg/ragnar.cfg b/cfg/ragnar.cfg index d8c2f3c..1d447c7 100644 --- a/cfg/ragnar.cfg +++ b/cfg/ragnar.cfg @@ -231,13 +231,13 @@ cursor_image = "arrow"; # Specifies whether or not to log messages # to the console # (28) -log_messages = true; +log_messages = false; # Specifies whether or not to log messages # to the log file # (29) # builds it as $HOME/.ragnarwm.log -should_log_to_file = true; +should_log_to_file = false; # Specifies the maximum number of scratchpads # that can be allocated diff --git a/src/config.c b/src/config.c index 65673b6..f41d639 100644 --- a/src/config.c +++ b/src/config.c @@ -635,45 +635,45 @@ readconfig(state_t* s, config_data_t* data) { bool success = false; - success = cfgreadint(s, (int32_t*)&data->maxstruts, "max_struts"); - success = cfgreadint(s, (int32_t*)&data->maxdesktops, "num_desktops"); - success = cfgreadint(s, (int32_t*)&data->maxscratchpads, "max_scratchpads"); + cfgreadint(s, (int32_t*)&data->maxstruts, "max_struts"); + cfgreadint(s, (int32_t*)&data->maxdesktops, "num_desktops"); + cfgreadint(s, (int32_t*)&data->maxscratchpads, "max_scratchpads"); - success = cfgreadint(s, (int32_t*)&data->winborderwidth, "win_border_width"); - success = cfgreadint(s, (int32_t*)&data->winbordercolor, "win_border_color"); - success = cfgreadint(s, (int32_t*)&data->winbordercolor_selected, "win_border_color_selected"); + cfgreadint(s, (int32_t*)&data->winborderwidth, "win_border_width"); + cfgreadint(s, (int32_t*)&data->winbordercolor, "win_border_color"); + cfgreadint(s, (int32_t*)&data->winbordercolor_selected, "win_border_color_selected"); - success = cfgevalkbmod(s, &data->modkey, "mod_key"); - success = cfgevalkbmod(s, &data->winmod, "win_mod"); + cfgevalkbmod(s, &data->modkey, "mod_key"); + cfgevalkbmod(s, &data->winmod, "win_mod"); - success = cfgevalmousebtn(s, &data->movebtn, "move_button"); - success = cfgevalmousebtn(s, &data->resizebtn, "resize_button"); + cfgevalmousebtn(s, &data->movebtn, "move_button"); + cfgevalmousebtn(s, &data->resizebtn, "resize_button"); - success = cfgreadint(s, (int32_t*)&data->desktopinit, "initial_desktop"); + cfgreadint(s, (int32_t*)&data->desktopinit, "initial_desktop"); data->desktopnames = cfgevalstrarr(s, "desktop_names"); success = data->desktopnames != NULL; - success = cfgreadfloat(s, &data->layoutmasterarea, "layout_master_area"); - success = cfgreadfloat(s, &data->layoutmasterarea_min, "layout_master_area_min"); - success = cfgreadfloat(s, &data->layoutmasterarea_max, "layout_master_area_max"); - success = cfgreadfloat(s, &data->layoutmasterarea_step, "layout_master_area_step"); + cfgreadfloat(s, &data->layoutmasterarea, "layout_master_area"); + cfgreadfloat(s, &data->layoutmasterarea_min, "layout_master_area_min"); + cfgreadfloat(s, &data->layoutmasterarea_max, "layout_master_area_max"); + cfgreadfloat(s, &data->layoutmasterarea_step, "layout_master_area_step"); - success = cfgreadfloat(s, &data->layoutsize_step, "layout_size_step"); - success = cfgreadfloat(s, &data->layoutsize_min, "layout_size_min"); + cfgreadfloat(s, &data->layoutsize_step, "layout_size_step"); + cfgreadfloat(s, &data->layoutsize_min, "layout_size_min"); - success = cfgreadfloat(s, &data->keywinmove_step, "key_win_move_step"); + cfgreadfloat(s, &data->keywinmove_step, "key_win_move_step"); - success = cfgreadint(s, (int32_t*)&data->winlayoutgap, "win_layout_gap"); - success = cfgreadint(s, (int32_t*)&data->winlayoutgap_max, "win_layout_gap_max"); - success = cfgreadint(s, (int32_t*)&data->winlayoutgap_step, "win_layout_gap_step"); + cfgreadint(s, (int32_t*)&data->winlayoutgap, "win_layout_gap"); + cfgreadint(s, (int32_t*)&data->winlayoutgap_max, "win_layout_gap_max"); + cfgreadint(s, (int32_t*)&data->winlayoutgap_step, "win_layout_gap_step"); data->initlayout = cfgevallayouttype(s, "initial_layout"); - success = cfgreadint(s, (int32_t*)&data->motion_notify_debounce_fps, "motion_notify_debounce_fps"); + cfgreadint(s, (int32_t*)&data->motion_notify_debounce_fps, "motion_notify_debounce_fps"); - success = cfgreadstr(s, (const char**)&data->cursorimage, "cursor_image"); + cfgreadstr(s, (const char**)&data->cursorimage, "cursor_image"); // optional, unset means the root window is left alone int32_t bgcolor = 0; @@ -687,14 +687,23 @@ readconfig(state_t* s, config_data_t* data) { data->kblayout = strdup(kblayout); } + // HOME is unset under su and some service managers, and strlen(NULL) + // segfaults before any of the logging guards get a chance to run const char* home = getenv("HOME"); + if(!home) { + home = "/tmp"; + } const char* relpath = "/.ragnarwm.log"; - char* logpath = malloc(strlen(home) + strlen(relpath) + 2); - sprintf(logpath, "%s%s", home, relpath); + size_t logpathlen = strlen(home) + strlen(relpath) + 1; + char* logpath = malloc(logpathlen); + if(logpath) { + snprintf(logpath, logpathlen, "%s%s", home, relpath); + } + // NULL on allocation failure; every use site checks before opening data->logfile = logpath; - success = cfgreadbool(s, &data->logmessages, "log_messages"); - success = cfgreadbool(s, &data->shouldlogtofile, "should_log_to_file"); + cfgreadbool(s, &data->logmessages, "log_messages"); + cfgreadbool(s, &data->shouldlogtofile, "should_log_to_file"); data->keybinds = cfgevalkeybinds(s, (uint32_t*)&data->numkeybinds, "keybinds"); diff --git a/src/funcs.h b/src/funcs.h index d40f20e..097297d 100644 --- a/src/funcs.h +++ b/src/funcs.h @@ -43,8 +43,12 @@ void loop(state_t* s); * This function terminates the window manager by * giving up the connection to the X server and * exiting the program. + * + * _Noreturn because it always ends in exit(). Without it every caller + * looks like it can fall through, which the analyzer reports as use of + * values the error path already ruled out */ -void terminate(state_t* s, int32_t exitcode); +_Noreturn void terminate(state_t* s, int32_t exitcode); /** * @brief Manages all windows that are avaiable on the diff --git a/src/ipc/sockets.c b/src/ipc/sockets.c index b640316..25858ed 100644 --- a/src/ipc/sockets.c +++ b/src/ipc/sockets.c @@ -16,7 +16,6 @@ #include "../config.h" #include -#define SOCKPATH "/tmp/ragnar_socket" #define MSGSIZE 256 #include "sockets.h" @@ -118,7 +117,10 @@ cmdgetwins(state_t* s, const uint8_t* data, int32_t clientfd) { numwins++; } } - RgWindow wins[numwins]; + // +1 so the array is never zero-length, which is undefined for a VLA. + // The write below sizes itself on numwins, so the spare slot is never + // sent and the wire format is unchanged + RgWindow wins[numwins + 1]; uint32_t i = 0; for(monitor_t* mon = s->monitors; mon != NULL; mon = mon->next) { for(client_t* cl = mon->clients; cl != NULL; cl = cl->next) { @@ -130,7 +132,7 @@ cmdgetwins(state_t* s, const uint8_t* data, int32_t clientfd) { logmsg(s, LogLevelError, "ipc: RgCommandGetWindows: failed to send number of client windows."); } - if(write(clientfd, wins, sizeof(wins)) == -1) { + if(write(clientfd, wins, numwins * sizeof(*wins)) == -1) { logmsg(s, LogLevelError, "ipc: RgCommandGetWindows: failed to send array of client windows."); } @@ -321,13 +323,16 @@ ipcserverthread(void* arg) { terminate(s, EXIT_FAILURE); } - // Set up the address structure + // Set up the address structure. Written straight into sun_path: it is + // already zeroed and snprintf always terminates, so no strncpy dance memset(&addr, 0, sizeof(struct sockaddr_un)); addr.sun_family = AF_UNIX; - strncpy(addr.sun_path, SOCKPATH, sizeof(addr.sun_path) - 1); + rg_socket_path(addr.sun_path, sizeof(addr.sun_path)); - // Remove any existing socket file - unlink(SOCKPATH); + // Remove any existing socket file. Per display, so this only ever + // clears this server's own stale socket, never another instance's + unlink(addr.sun_path); + logmsg(s, LogLevelTrace, "ipc: binding socket at '%s'.", addr.sun_path); // Bind the socket if (bind(serverfd, (struct sockaddr*)&addr, sizeof(struct sockaddr_un)) < 0) { diff --git a/src/ragnar.c b/src/ragnar.c index a62520f..cbebc2e 100644 --- a/src/ragnar.c +++ b/src/ragnar.c @@ -113,7 +113,16 @@ setup(state_t* s) { initconfig(s); readconfig(s, &s->config); - fclose(fopen(s->config.logfile, "w")); + // Truncate the log for this run. The path can be unwritable or NULL, + // and fclose(NULL) is undefined rather than a no-op + if(s->config.logfile) { + FILE* logf = fopen(s->config.logfile, "w"); + if(logf) { + fclose(logf); + } else { + perror("ragnar: error truncating log file."); + } + } s->lastexposetime = 0; s->lastmotiontime = 0; @@ -260,7 +269,7 @@ loop(state_t* s) { * diss->conecting the s->conection to the X server and * exiting the program. */ -void +_Noreturn void terminate(state_t* s, int32_t exitcode) { // Release every client for(monitor_t* mon = s->monitors; mon != NULL; mon = mon->next) { @@ -416,7 +425,9 @@ void managewins(state_t* s) { s->nwinstruts = 0; getwinstruts(s, s->root); - if(!cl->floating) { + // makeclient returns NULL when the window vanishes mid-scan or its + // geometry cannot be read + if(cl && !cl->floating) { addtolayout(s, cl); } } @@ -506,6 +517,11 @@ toggleedgewindows(state_t* s, client_t* cl, bool toggle) { } void createwindowedges(state_t* s, client_t* cl) { + // addclient bails when the calloc fails, so this holds; keep the guard + // so the loop below can never walk a null array + if(!cl->edges) { + return; + } xcb_window_t parent = cl->win; uint32_t mask = XCB_CW_EVENT_MASK; uint32_t val = XCB_EVENT_MASK_ENTER_WINDOW | @@ -591,7 +607,14 @@ makeclient(state_t* s, xcb_window_t win) { { bool success; cl->area = winarea(s, cl->frame, &success); - if(!success) return NULL; + if(!success) { + // addclient already linked cl into the monitor list, so bailing out + // here has to unlink it too; otherwise the list keeps a client with + // no valid geometry that every layout pass then walks over + unframeclient(s, cl); + releaseclient(s, cl->win); + return NULL; + } } cl->area.size = applysizehints(s, cl, cl->area.size); @@ -2006,6 +2029,11 @@ swapclients(state_t* s, client_t* c1, client_t* c2) { */ void uploaddesktopnames(state_t* s, monitor_t* mon) { + // monfocus is NULL before the first cursormon() and on a monitorless + // setup, and every caller passes it straight through + if(!mon) { + return; + } // Calculate the total length of the property value size_t total_length = 0; for (uint32_t i = 0; i < mon->desktopcount; i++) { @@ -2013,8 +2041,13 @@ uploaddesktopnames(state_t* s, monitor_t* mon) { total_length += strlen(mon->activedesktops[i].name) + 1; // +1 for the null byte } - // Allocate memory for the data + // Allocate memory for the data. total_length is 0 when no desktop is + // initialized yet, and malloc(0) may legally hand back NULL char* data = malloc(total_length); + if(total_length && !data) { + logmsg(s, LogLevelError, "failed to allocate desktop name buffer."); + return; + } // Concatenate the desktop names into the data buffer char* ptr = data; @@ -3538,7 +3571,10 @@ evfocusin(state_t* s, xcb_generic_event_t* ev) { client_t* addclient(state_t* s, client_t** clients, xcb_window_t win) { // Allocate client structure - client_t* cl = (client_t*)malloc(sizeof(*cl)); + // calloc, not malloc: addclient reads some fields (frame, decorated, + // floating) before every field has been assigned, and garbage there + // decides real branches + client_t* cl = (client_t*)calloc(1, sizeof(*cl)); cl->win = win; cl->edges = NULL; @@ -3570,8 +3606,15 @@ addclient(state_t* s, client_t** clients, xcb_window_t win) { // Update the head of the list to the new client *clients = cl; - edgegrab_t* edges = calloc(9, sizeof(edgegrab_t)); - cl->edges = edges; // Add this pointer to your client_t struct + // 9 so the window_edge_t values index directly, EdgeNone at 0 unused. + // createwindowedges walks this unconditionally, so a failed allocation + // is a null deref rather than a degraded client + cl->edges = calloc(9, sizeof(edgegrab_t)); + if(!cl->edges) { + logmsg(s, LogLevelError, "failed to allocate edge grabs for client."); + releaseclient(s, cl->win); + return NULL; + } logmsg(s, LogLevelTrace, "Added client ('%s') to the linked list of clients.", cl->name ? cl->name : "No name"); @@ -3636,7 +3679,11 @@ releaseclient(state_t* s, xcb_window_t win) { if(wasfocus) { s->focus = NULL; } - // Freeing memory allocated for client + // Freeing memory allocated for client. name is strndup'd by + // getclientname and edges is the calloc'd array of 9 grab + // handles; both die with the client or they leak per window + free(cl->name); + free(cl->edges); free(cl); // Released client held focus; hand it to the first client // (master slot) still on the visible desktop @@ -4153,7 +4200,7 @@ void logmsg(state_t* s, log_level_t lvl, const char* fmt, ...) { * @param fmt The format string * @param args The variadic arguments list */ void logtofile(log_level_t lvl, state_t* s, const char* fmt, va_list args) { - if (!s->config.shouldlogtofile) return; + if (!s->config.shouldlogtofile || !s->config.logfile) return; FILE *file = fopen(s->config.logfile, "a"); @@ -4163,8 +4210,15 @@ void logtofile(log_level_t lvl, state_t* s, const char* fmt, va_list args) { return; } - // Write the date to the file - char* date = cmdoutput("date +\"%d.%m.%y %H:%M:%S\""); + // Write the date to the file. strftime, not popen("date"): logmsg runs + // in the event path of a SCHED_RR process, so a fork+exec of a shell per + // log line is both a leak (the buffer was never freed) and orders of + // magnitude slower than formatting it here + char date[32]; + time_t now = time(NULL); + struct tm tm; + localtime_r(&now, &tm); + strftime(date, sizeof(date), "%d.%m.%y %H:%M:%S", &tm); fprintf(file, "%s | ", date); switch(lvl) { diff --git a/src/structs.h b/src/structs.h index 9b91ff7..73ea247 100644 --- a/src/structs.h +++ b/src/structs.h @@ -66,9 +66,13 @@ void reloadconfigfile(state_t* s, passthrough_data_t data); #define VEC_INIT_CAP 4 +/* No allocation here on purpose: cap 0 makes the first vector_append call + * realloc(NULL, ...), which is malloc. The previous malloc sized itself + * with sizeof(*(vec)), the list struct rather than the element type, and + * cap = 0 discarded the block on first append regardless */ #define vector_init(vec) \ do { \ - (vec)->items = malloc(sizeof(*(vec)) * VEC_INIT_CAP); \ + (vec)->items = NULL; \ (vec)->size = 0; \ (vec)->cap = 0; \ } while (0) From b44caf48ca25cd4a36101328aa1e81c8038e0eff Mon Sep 17 00:00:00 2001 From: h8d13 Date: Tue, 4 Aug 2026 13:13:41 +0200 Subject: [PATCH 21/25] chore(clean): remove mentions of cmdoutput now that we use strftime --- src/funcs.h | 7 ------- src/ragnar.c | 47 ----------------------------------------------- src/realtime.c | 5 ++--- 3 files changed, 2 insertions(+), 57 deletions(-) diff --git a/src/funcs.h b/src/funcs.h index 097297d..0a7b809 100644 --- a/src/funcs.h +++ b/src/funcs.h @@ -977,11 +977,4 @@ void logmsg(state_t* s, log_level_t lvl, const char* fmt, ...); void logtofile(log_level_t lvl, state_t* s, const char* fmt, va_list args); -/** - * @brief Returns the output of a given command - * - * @param cmd The command to get the output of - * - * @return The output of the given command */ -char* cmdoutput(const char* cmd); diff --git a/src/ragnar.c b/src/ragnar.c index cbebc2e..85b3fcf 100644 --- a/src/ragnar.c +++ b/src/ragnar.c @@ -4244,53 +4244,6 @@ void logtofile(log_level_t lvl, state_t* s, const char* fmt, va_list args) { fclose(file); } -/** - * @brief Returns the output of a given command - * - * @param cmd The command to get the output of - * - * @return The output of the given command */ -char* -cmdoutput(const char* cmd) { - FILE *fp; - char buffer[512]; - char *result = NULL; - size_t result_len = 0; - - // Open a pipe to the command - fp = popen(cmd, "r"); - if (fp == NULL) { - perror("popen"); - return NULL; - } - - // Read the command's output - while (fgets(buffer, sizeof(buffer), fp) != NULL) { - size_t buffer_len = strlen(buffer); - char *new_result = realloc(result, result_len + buffer_len + 1); - if (new_result == NULL) { - perror("realloc"); - free(result); - pclose(fp); - return NULL; - } - result = new_result; - memcpy(result + result_len, buffer, buffer_len); - result_len += buffer_len; - result[result_len] = '\0'; - } - - // Close the pipe - if (pclose(fp) == -1) { - perror("pclose"); - free(result); - return NULL; - } - - return result; -} - - int main(void) { state_t* wm_state = calloc(1, sizeof(state_t)); diff --git a/src/realtime.c b/src/realtime.c index 1c5f69a..8a916af 100644 --- a/src/realtime.c +++ b/src/realtime.c @@ -25,9 +25,8 @@ * puts every child back on SCHED_OTHER at nice 0 across fork, so the * terminals and browsers started by runcmd keybinds never inherit * real-time priority. sway does the same containment with a - * pthread_atfork handler, which would not cover the popen() in - * cmdoutput(): glibc implements popen with posix_spawn, and posix_spawn - * does not run atfork handlers. The kernel flag covers both paths. + * pthread_atfork handler; the kernel flag needs no handler and holds + * across exec. * * pid 0 is the calling thread, so the IPC thread started in setup() * stays on SCHED_OTHER. From 66e029634219aa7adff4fa66f5d0403130819751 Mon Sep 17 00:00:00 2001 From: h8d13 Date: Tue, 4 Aug 2026 13:16:11 +0200 Subject: [PATCH 22/25] chore(clean): nuke .desktop file, not intended flow. --- ragnar.desktop | 7 ------- 1 file changed, 7 deletions(-) delete mode 100644 ragnar.desktop diff --git a/ragnar.desktop b/ragnar.desktop deleted file mode 100644 index c2e9d46..0000000 --- a/ragnar.desktop +++ /dev/null @@ -1,7 +0,0 @@ -[Desktop Entry] -Encoding=UTF-8 -Name=Ragnar -Comment=Ragnar Window Manager -Exec=ragnar -Icon=ragnar -Type=XSession From dfc64de0f281b5babaa49f6bf7dd97c9fe69f0fb Mon Sep 17 00:00:00 2001 From: h8d13 Date: Tue, 4 Aug 2026 13:23:11 +0200 Subject: [PATCH 23/25] fix(cache) --- src/config.c | 7 ++++++- src/ragnar.c | 20 ++++++++++++-------- src/structs.h | 7 ++++++- 3 files changed, 24 insertions(+), 10 deletions(-) diff --git a/src/config.c b/src/config.c index f41d639..9c0c78e 100644 --- a/src/config.c +++ b/src/config.c @@ -651,8 +651,13 @@ readconfig(state_t* s, config_data_t* data) { cfgreadint(s, (int32_t*)&data->desktopinit, "initial_desktop"); + // Fatal rather than dead: every desktop lookup indexes this array + // unguarded, so an unset desktop_names segfaults later instead of + // failing here where cfgevalstrarr has already logged why data->desktopnames = cfgevalstrarr(s, "desktop_names"); - success = data->desktopnames != NULL; + if(!data->desktopnames) { + terminate(s, EXIT_FAILURE); + } cfgreadfloat(s, &data->layoutmasterarea, "layout_master_area"); cfgreadfloat(s, &data->layoutmasterarea_min, "layout_master_area_min"); diff --git a/src/ragnar.c b/src/ragnar.c index 85b3fcf..c5c3402 100644 --- a/src/ragnar.c +++ b/src/ragnar.c @@ -566,8 +566,8 @@ makeclient(state_t* s, xcb_window_t win) { // Adding the mapped client to our linked list client_t* cl = addclient(s, &clmon->clients, win); - // Setting border - xcb_atom_t motif_hints = getatom(s, "_MOTIF_WM_HINTS"); + // Setting border + xcb_atom_t motif_hints = s->motifhints_atom; xcb_get_property_cookie_t prop_cookie = xcb_get_property( s->con, 0, cl->win, motif_hints, motif_hints, 0, 5 ); @@ -1021,9 +1021,9 @@ clienthasdeleteatom(state_t* s, client_t* cl) { */ bool clientshouldtile(state_t* s, client_t* cl) { - // Get atoms - xcb_atom_t wintype_atom = getatom(s, "_NET_WM_WINDOW_TYPE"); - xcb_atom_t wintypenormal_atom = getatom(s, "_NET_WM_WINDOW_TYPE_NORMAL"); + // Get atoms, interned once in setupatoms + xcb_atom_t wintype_atom = s->ewmh_atoms[EWMHwindowType]; + xcb_atom_t wintypenormal_atom = s->ewmh_atoms[EWMHwindowTypeNormal]; // Get window property for type xcb_get_property_cookie_t cookie = xcb_get_property(s->con, 0, cl->win, wintype_atom, XCB_ATOM_ATOM, 0, 1); @@ -2067,7 +2067,7 @@ uploaddesktopnames(state_t* s, monitor_t* mon) { XCB_PROP_MODE_REPLACE, root_window, s->ewmh_atoms[EWMHdesktopNames], - getatom(s, "UTF8_STRING"), + s->utf8str_atom, 8, total_length, data); @@ -2217,6 +2217,7 @@ setupatoms(state_t* s) { s->ewmh_atoms[EWMHcheck] = getatom(s, "_NET_SUPPORTING_WM_CHECK"); s->ewmh_atoms[EWMHfullscreen] = getatom(s, "_NET_WM_STATE_FULLSCREEN"); s->ewmh_atoms[EWMHwindowType] = getatom(s, "_NET_WM_WINDOW_TYPE"); + s->ewmh_atoms[EWMHwindowTypeNormal] = getatom(s, "_NET_WM_WINDOW_TYPE_NORMAL"); s->ewmh_atoms[EWMHwindowTypeDialog] = getatom(s, "_NET_WM_WINDOW_TYPE_DIALOG"); s->ewmh_atoms[EWMHwindowTypePopup] = getatom(s, "_NET_WM_WINDOW_TYPE_POPUP_MENU"); s->ewmh_atoms[EWMHwindowTypeDock] = getatom(s, "_NET_WM_WINDOW_TYPE_DOCK"); @@ -2225,7 +2226,10 @@ setupatoms(state_t* s) { s->ewmh_atoms[EWMHnumberOfDesktops] = getatom(s, "_NET_NUMBER_OF_DESKTOPS"); s->ewmh_atoms[EWMHdesktopNames] = getatom(s, "_NET_DESKTOP_NAMES"); - xcb_atom_t utf8str = getatom(s, "UTF8_STRING"); + // Interned here so the per-client and per-desktop-change paths do not + // pay an intern round-trip each time they need them + s->motifhints_atom = getatom(s, "_MOTIF_WM_HINTS"); + s->utf8str_atom = getatom(s, "UTF8_STRING"); xcb_window_t wmcheckwin = xcb_generate_id(s->con); xcb_create_window(s->con, XCB_COPY_FROM_PARENT, wmcheckwin, s->root, @@ -2238,7 +2242,7 @@ setupatoms(state_t* s) { // Set _NET_WM_NAME property on the wmcheckwin xcb_change_property(s->con, XCB_PROP_MODE_REPLACE, wmcheckwin, s->ewmh_atoms[EWMHname], - utf8str, 8, strlen("ragnar"), "ragnar"); + s->utf8str_atom, 8, strlen("ragnar"), "ragnar"); // Set _NET_WM_CHECK property on the root window xcb_change_property(s->con, XCB_PROP_MODE_REPLACE, s->root, s->ewmh_atoms[EWMHcheck], diff --git a/src/structs.h b/src/structs.h index 73ea247..f3e89a7 100644 --- a/src/structs.h +++ b/src/structs.h @@ -143,6 +143,7 @@ typedef enum { EWMHfullscreen, EWMHactiveWindow, EWMHwindowType, + EWMHwindowTypeNormal, EWMHwindowTypeDialog, EWMHwindowTypePopup, EWMHwindowTypeDock, @@ -553,8 +554,12 @@ struct state_t { monitor_t* monitors; monitor_t* monfocus; - xcb_atom_t wm_atoms[WMcount]; + xcb_atom_t wm_atoms[WMcount]; xcb_atom_t ewmh_atoms[EWMHcount]; + // Interned once in setupatoms like the arrays above, but kept out of + // ewmh_atoms: that whole array is published verbatim as _NET_SUPPORTED + // and neither of these is an EWMH hint ragnar supports + xcb_atom_t motifhints_atom, utf8str_atom; desktop_t* curdesktop; From 5864872a5140b255ac0e53d73edc38196720e745 Mon Sep 17 00:00:00 2001 From: h8d13 Date: Tue, 4 Aug 2026 14:20:43 +0200 Subject: [PATCH 24/25] chore(cfg): simplify loading/logging + spacing in default --- cfg/ragnar.cfg | 52 ++++++++++++-------------------------------------- src/config.c | 18 +++++++++-------- 2 files changed, 22 insertions(+), 48 deletions(-) diff --git a/cfg/ragnar.cfg b/cfg/ragnar.cfg index 1d447c7..9d2509e 100644 --- a/cfg/ragnar.cfg +++ b/cfg/ragnar.cfg @@ -79,126 +79,105 @@ # ---------------------- # NOTE: For all key options, see the functions at: # https://github.com/cococry/ragnar/blob/main/src/structs.h#L17 -# The same list is the dispatch table 'keycbmappings' in src/config.c; a -# name that is in one and not the other is a bind that silently does -# nothing. -# ---------------------- -# -# cfg -> src resolution -# ---------------------- -# Every key below carries an (N) marker naming the config_data_t field -# (src/structs.h) it lands in. N is the order readconfig() (src/config.c) -# reads the key, not the order it appears here, so the numbers run out of -# sequence going down the file. # ---------------------- # Specifies the width of the border around client # windows -# (4) win_border_width = 2; + # Specifies the color of the border around client # windows -# (5) win_border_color = 0x3B3B3B; + # Specifies the color of the border around # selected/focused client windows -# (6) win_border_color_selected = 0xE6DFDC; # Specifies the main modifier key that is # used to execute window manager shortcuts -# (7) mod_key = "Super"; + # Specifies the modifier key that is used # to interact with clients windows -# (8) win_mod = "Super"; # Specfies the mouse button that needs to be # held in order to move client windows -# (9) move_button = "LeftMouse"; + # Specfies the mouse button that needs to be # held in order to resize client windows -# (10) resize_button = "RightMouse"; # Specifies the keyboard layout that is applied # on startup (setxkbmap syntax, e.g "be" or # "be nodeadkeys"). Uses the session's layout # when unset. -# (27) # keyboard_layout = "us"; # Specifies the desktop index that is initially # selected on every monitor -# (11) initial_desktop = 0; + # Specfies the number of allocated virtual # desktops -# (2) num_desktops = 9; + # Specifies the name of every virtual desktop # in order. -# (12) desktop_names = ["1", "2", "3", "4", "5", "6", "7", "8", "9"]; # Specifies the area that the master window takes # up in master-slave layouts initially. # (in 0.0-1.0 %) -# (13) layout_master_area = 0.5; + # Specifies the minimum area that master windows # need to take up in master-slave layouts # (in 0.0-1.0 %) -# (14) layout_master_area_min = 0.1; + # Specifies the maximum a2rea that master windows # can take up in master-slave layouts # (in 0.0-1.0 %) -# (15) layout_master_area_max = 0.9; + # Specifies the amount that the master area changes/steps # when it is decreased/increased. # (in 0.0-1.0 %) -# (16) layout_master_area_step = 0.1; # Specifies the amount that areas of windows # within layouts change when they are increased/decreased # (in px) -# (17) + layout_size_step = 100.0; # Specifies the minimum area that windows within # layouts need to take up # (in px) -# (18) layout_size_min = 150.0; # Specifies the amount that windows are moved # by when using 'move' shortcuts for floating windows # (in px) -# (19) key_win_move_step = 100.0; # Specifies the initial gap between windows # within layouts (in px) -# (20) win_layout_gap = 5; + # Specifies the maximum gap that windows within # layouts can have around each other (in px) -# (21) win_layout_gap_max = 150; + # Specifies the amount that the gap between # non-floating windows changes when it's # decreased/increased (in px) # (in px) -# (22) win_layout_gap_step = 5; # Specifies the layout that is initially used # for every virtual desktop -# (23) initial_layout = "LayoutTiledMaster"; # Advanced Configuration @@ -209,39 +188,32 @@ initial_layout = "LayoutTiledMaster"; # performance. Especially on high polling rate mouses, # lag can be very noticable when not throtteling motion # notify events. -# (24) motion_notify_debounce_fps = 60; # Specifies the maximum number of 'strut'-window that # the window manager can capture. Struts are information # about window positions and sizes that are used to correctly # establish window layouts with bars or other status windows. -# (1) max_struts = 8; # Specifies the cursor image to use for the root window -# (25) cursor_image = "arrow"; # Specifies the colour the root window is painted with. # Optional: when unset does nothing. (Let ex: `feh` take-over) -# (26) # bg_color = 0x1A1A1A; # Specifies whether or not to log messages # to the console -# (28) log_messages = false; # Specifies whether or not to log messages # to the log file -# (29) # builds it as $HOME/.ragnarwm.log should_log_to_file = false; # Specifies the maximum number of scratchpads # that can be allocated -# (3) max_scratchpads = 10; # -------------------------------- diff --git a/src/config.c b/src/config.c index 9c0c78e..f4b54f6 100644 --- a/src/config.c +++ b/src/config.c @@ -633,7 +633,11 @@ void readconfig(state_t* s, config_data_t* data) { if(!data) return; - bool success = false; + // First: logmsg drops everything while logmessages is false, so any + // key read before this point fails silently. logfile is still NULL + // here, which logtofile checks, so only the console gets these + cfgreadbool(s, &data->logmessages, "log_messages"); + cfgreadbool(s, &data->shouldlogtofile, "should_log_to_file"); cfgreadint(s, (int32_t*)&data->maxstruts, "max_struts"); cfgreadint(s, (int32_t*)&data->maxdesktops, "num_desktops"); @@ -656,6 +660,9 @@ readconfig(state_t* s, config_data_t* data) { // failing here where cfgevalstrarr has already logged why data->desktopnames = cfgevalstrarr(s, "desktop_names"); if(!data->desktopnames) { + // straight to stderr, not logmsg: log_messages defaults to false, so + // a fatal config error would otherwise exit 1 with no reason given + fprintf(stderr, "ragnar: config: desktop_names is not set.\n"); terminate(s, EXIT_FAILURE); } @@ -707,15 +714,10 @@ readconfig(state_t* s, config_data_t* data) { // NULL on allocation failure; every use site checks before opening data->logfile = logpath; - cfgreadbool(s, &data->logmessages, "log_messages"); - cfgreadbool(s, &data->shouldlogtofile, "should_log_to_file"); - - data->keybinds = cfgevalkeybinds(s, (uint32_t*)&data->numkeybinds, "keybinds"); - success = data->keybinds != NULL; - - if(!success) { + if(!data->keybinds) { + fprintf(stderr, "ragnar: config: keybinds are not set.\n"); terminate(s, EXIT_FAILURE); } else { printf("ragnar: successfully read config file.\n"); From 1ac0df51f0545a5497978724b7b780e9f9c36347 Mon Sep 17 00:00:00 2001 From: h8d13 Date: Tue, 4 Aug 2026 14:21:58 +0200 Subject: [PATCH 25/25] rem --- cfg/ragnar.cfg | 1 - 1 file changed, 1 deletion(-) diff --git a/cfg/ragnar.cfg b/cfg/ragnar.cfg index 9d2509e..405c558 100644 --- a/cfg/ragnar.cfg +++ b/cfg/ragnar.cfg @@ -220,7 +220,6 @@ max_scratchpads = 10; # =========== Key bindings =========== -# (30) # four sub-keys per entry: mod (optional), key, do, and then cmd / i as the # passthrough payload. Any other sub-key here is ignored silently. keybinds = (