diff --git a/.gitignore b/.gitignore index 01de4a4..e6efd45 100644 --- a/.gitignore +++ b/.gitignore @@ -1,12 +1,14 @@ *.o *.a *.patch +*.data +.*.md +**.tar.zst .cache/ bin/ pkg/ /ragnar/ -**.tar.zst +src/ragnar/ .gdb.log compile_commands.json compile_flags.txt -.*.md diff --git a/Makefile b/Makefile index 974bc2c..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,9 +53,26 @@ 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 +# 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 + +# 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/* @@ -47,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/PKGBUILD b/PKGBUILD index 7a120e3..d1e3960 100644 --- a/PKGBUILD +++ b/PKGBUILD @@ -26,14 +26,18 @@ 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' + 'feh: optional set wallpaper for X displays' 'ttf-dejavu: fonts for both the above' 'wireplumber: default volume keybinds' 'brightnessctl: default brightness keybinds' 'playerctl: default media player keybinds' + 'xorg-xset: for screenblanking idle/DPMS' ) + provides=('ragnarwm') options=('!debug') backup=('etc/ragnarwm/ragnar.cfg') @@ -43,6 +47,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 +58,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/README.md b/README.md index 95d877c..e22385f 100644 --- a/README.md +++ b/README.md @@ -79,16 +79,18 @@ 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`: ```console +feh --bg-fill ~/Downloads/nerd.jpg; +xset s off -dpms; 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` 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/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 822a84c..405c558 100644 --- a/cfg/ragnar.cfg +++ b/cfg/ragnar.cfg @@ -58,6 +58,7 @@ # - setfloatingmode # - updatebarslayout # - cycledownlayout +# - cycleuplayout # - addmasterlayout # - removemasterlayout # - incmasterarealayout @@ -73,19 +74,21 @@ # - cyclefocusmonitordown # - cyclefocusmonitorup # - togglescratchpad +# - reloadconfigfile # # ---------------------- # NOTE: For all key options, see the functions at: # https://github.com/cococry/ragnar/blob/main/src/structs.h#L17 # ---------------------- -# # Specifies the width of the border around client # windows win_border_width = 2; + # Specifies the color of the border around client # windows win_border_color = 0x3B3B3B; + # Specifies the color of the border around # selected/focused client windows win_border_color_selected = 0xE6DFDC; @@ -93,6 +96,7 @@ win_border_color_selected = 0xE6DFDC; # Specifies the main modifier key that is # used to execute window manager shortcuts mod_key = "Super"; + # Specifies the modifier key that is used # to interact with clients windows win_mod = "Super"; @@ -100,6 +104,7 @@ win_mod = "Super"; # Specfies the mouse button that needs to be # held in order to move client windows move_button = "LeftMouse"; + # Specfies the mouse button that needs to be # held in order to resize client windows resize_button = "RightMouse"; @@ -113,47 +118,30 @@ resize_button = "RightMouse"; # Specifies the desktop index that is initially # selected on every monitor initial_desktop = 0; + # Specfies the number of allocated virtual # desktops num_desktops = 9; + # Specifies the name of every virtual desktop # 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 %) 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 %) 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 %) 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 %) @@ -162,6 +150,7 @@ layout_master_area_step = 0.1; # Specifies the amount that areas of windows # within layouts change when they are increased/decreased # (in px) + layout_size_step = 100.0; # Specifies the minimum area that windows within # layouts need to take up @@ -176,9 +165,11 @@ key_win_move_step = 100.0; # Specifies the initial gap between windows # within layouts (in px) win_layout_gap = 5; + # Specifies the maximum gap that windows within # layouts can have around each other (in px) win_layout_gap_max = 150; + # Specifies the amount that the gap between # non-floating windows changes when it's # decreased/increased (in px) @@ -208,15 +199,18 @@ max_struts = 8; # Specifies the cursor image to use for the root window cursor_image = "arrow"; -# Logging (log file is ~/.ragnarwm.log) +# Specifies the colour the root window is painted with. +# Optional: when unset does nothing. (Let ex: `feh` take-over) +# bg_color = 0x1A1A1A; # Specifies whether or not to log messages # to the console -log_messages = true; +log_messages = false; # Specifies whether or not to log messages # to the log file -should_log_to_file = true; +# builds it as $HOME/.ragnarwm.log +should_log_to_file = false; # Specifies the maximum number of scratchpads # that can be allocated @@ -226,6 +220,8 @@ max_scratchpads = 10; # =========== Key bindings =========== +# 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"; @@ -550,6 +546,7 @@ keybinds = ( key = "KeyC"; do = "reloadconfigfile"; }, + { key = "KeyAudioLowerVolume"; do = "runcmd"; cmd = "wpctl set-volume -l 1.0 @DEFAULT_AUDIO_SINK@ 5%-"; 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 diff --git a/src/config.c b/src/config.c index 69387e3..f4b54f6 100644 --- a/src/config.c +++ b/src/config.c @@ -633,49 +633,64 @@ 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"); - 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"); + // 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; - - success = cfgreadbool(s, &data->usedecoration, "use_decoration"); + 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); + } - 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; + 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; @@ -684,21 +699,25 @@ 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"); - - 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"); @@ -709,22 +728,19 @@ void reloadconfig(state_t* s, config_data_t* data) { destroyconfig(); - bool using_decoration = s->config.usedecoration; - initconfig(s); readconfig(s, 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); - if(!using_decoration) { - s->config.usedecoration = false; - } - - // Reload struts + // Reload struts s->nwinstruts = 0; getwinstruts(s, s->root); diff --git a/src/funcs.h b/src/funcs.h index 27bb178..0a7b809 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 @@ -283,6 +287,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 @@ -531,6 +544,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); /** @@ -958,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/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/keycallbacks.h b/src/keycallbacks.h index 8db43d6..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 @@ -147,14 +149,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}); } @@ -192,7 +190,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); } @@ -699,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); @@ -720,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); @@ -739,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); @@ -758,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); diff --git a/src/ragnar.c b/src/ragnar.c index 1f32ab8..c5c3402 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; @@ -181,6 +190,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); @@ -257,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) { @@ -308,11 +320,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 @@ -389,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); } } @@ -479,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 | @@ -523,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 ); @@ -564,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); @@ -971,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); @@ -1114,8 +1164,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 @@ -1250,6 +1301,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); } @@ -1262,6 +1314,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); } @@ -1976,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++) { @@ -1983,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; @@ -2004,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); @@ -2154,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"); @@ -2162,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, @@ -2175,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], @@ -2367,6 +2434,68 @@ loaddefaultcursor(state_t* s) { setcursorhidden(s, true); } +/** + * @brief Paints the root window with the configured background colour. + * 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) { + // 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; + + // 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. @@ -2805,6 +2934,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); @@ -3077,7 +3209,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; @@ -3364,13 +3500,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); } @@ -3446,7 +3575,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; @@ -3470,13 +3602,23 @@ 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 *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"); @@ -3541,7 +3683,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 @@ -4058,7 +4204,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"); @@ -4068,8 +4214,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) { @@ -4095,53 +4248,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. diff --git a/src/structs.h b/src/structs.h index 2cdcc97..f3e89a7 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) @@ -139,6 +143,7 @@ typedef enum { EWMHfullscreen, EWMHactiveWindow, EWMHwindowType, + EWMHwindowTypeNormal, EWMHwindowTypeDialog, EWMHwindowTypePopup, EWMHwindowTypeDock, @@ -465,8 +470,6 @@ typedef struct { char** desktopnames; - bool usedecoration; - double layoutmasterarea; double layoutmasterarea_min; double layoutmasterarea_max; @@ -496,6 +499,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 { @@ -515,7 +523,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. @@ -531,6 +541,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; @@ -540,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;