diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..66c1575 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,8 @@ +# keep CRLF out of the tree +* text=auto eol=lf + +# build plumbing, not part of the language breakdown. +# a pattern without a slash matches the basename at any depth, so this +# covers ./Makefile and ./api/Makefile both. +Makefile linguist-detectable=false +flake.nix linguist-detectable=false diff --git a/CODE_OF_CONDUCT.md b/.github/CODE_OF_CONDUCT.md similarity index 100% rename from CODE_OF_CONDUCT.md rename to .github/CODE_OF_CONDUCT.md diff --git a/.gitignore b/.gitignore index 6d1c2bb..65254e1 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,11 @@ -ragnar.o -ragnarstart +*.o +*.a *.patch .cache/ bin/ +pkg/ +ragnar/ +**.tar.zst +.gdb.log +compile_commands.json +compile_flags.txt diff --git a/Makefile b/Makefile index aa0301d..974bc2c 100644 --- a/Makefile +++ b/Makefile @@ -1,44 +1,44 @@ CC = cc -CFLAGS = -O3 -ffast-math -Wall -Wextra -pedantic -CFLAGS += -isystem api/include +# ?= so an exported CFLAGS (makepkg, distro build) replaces this wholesale. +# geometry here is ratios and pixel rects cast back to ints, so -ffast-math +# 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 -LDLIBS = -lxcb -lxcb-keysyms -lxcb-icccm -lxcb-cursor -lxcb-randr -lxcb-composite -lxcb-ewmh -lX11 -lX11-xcb -lGL -lm -lconfig -lxcb-util +LDLIBS = -lxcb -lxcb-keysyms -lxcb-icccm -lxcb-cursor -lxcb-randr -lxcb-xfixes -lX11 -lX11-xcb -lconfig SRC = ./src/*.c ./src/ipc/*.c BIN = ragnar -RAGNAR_API = api/lib/ragnar.a - PREFIX = /usr BINDIR = $(PREFIX)/bin +SYSCONFDIR = /etc .PHONY: all -all: $(RAGNAR_API) +all: mkdir -p ./bin - $(CC) -o bin/$(BIN) $(CFLAGS) $(SRC) $(LDLIBS) + $(CC) -o bin/$(BIN) $(ALL_CFLAGS) $(LDFLAGS) $(SRC) $(LDLIBS) -$(RAGNAR_API): +# 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 +api: $(MAKE) -C api +.PHONY: install-api +install-api: + $(MAKE) -C api install -DEST_DIR := $(HOME)/.config/ragnarwm -CONFIG_FILE := $(DEST_DIR)/ragnar.cfg - -.PHONY: config -config: - @if [ ! -f "$(CONFIG_FILE)" ]; then \ - echo "Config file does not exist. Copying default config..."; \ - mkdir -p "$(DEST_DIR)"; \ - cp -r "./cfg/ragnar.cfg" "$(CONFIG_FILE)"; \ - else \ - echo "Config file already exists. Skipping copy $(CONFIG_FILE)."; \ - fi - +# no user config target: ragnar reads $HOME/.config/ragnarwm/ragnar.cfg first +# and falls back to the copy installed below. an override is a plain cp, and +# writing $HOME from a root install lands in the wrong home. +# mirrors PKGBUILD logic, and is "standard practice" for config files. .PHONY: install -install: - install -Dm755 bin/$(BIN) -t $(BINDIR) - install -Dm755 ragnarstart -t $(BINDIR) - cp -f ragnar.desktop $(PREFIX)/share/xsessions/ +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 .PHONY: clean clean: @@ -46,6 +46,6 @@ clean: .PHONY: uninstall uninstall: - $(RM) $(BINDIR)/ragnar - $(RM) $(PREFIX)/share/xsessions/ragnar.desktop - $(RM) $(BINDIR)/ragnarstart + $(RM) $(DESTDIR)$(BINDIR)/$(BIN) + $(RM) $(DESTDIR)$(PREFIX)/share/xsessions/ragnar.desktop + $(RM) -r $(DESTDIR)$(SYSCONFDIR)/ragnarwm diff --git a/PKGBUILD b/PKGBUILD index 30a5ad4..704d34c 100644 --- a/PKGBUILD +++ b/PKGBUILD @@ -1,33 +1,53 @@ # Contributor: Luxzi +# Contributor: h8d13 pkgname=ragnarwm -_pkgname="Ragnar" -pkgver='1.3' +_pkgname="ragnar" +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" license=('GPL') groups=() -depends=('xcb' 'xcb-util' '') +# mirrors the NEEDED entries of bin/ragnar, nothing more: +# libxcb covers xcb + randr + xfixes, libx11 covers X11 + X11-xcb. +depends=( + 'libxcb' + 'libx11' + 'xcb-util-keysyms' + 'xcb-util-cursor' + 'xcb-util-wm' + 'libconfig' + 'xorg-server' + 'xorg-xinit' +) makedepends=('git' 'make' 'gcc') +optdepends=( + 'alacritty: default terminal keybind' + 'polybar: optional status/desktops bars' + 'ttf-dejavu: fonts for both the above' +) provides=('ragnarwm') -source=("${_pkgname}::git+https://github.com/cococry/${_pkgname}.git") +options=('!debug') +backup=('etc/ragnarwm/ragnar.cfg') +source=("${_pkgname}::git+${url}.git") sha256sums=('SKIP') pkgver() { - cd $_pkgname - echo $pkgver + cd $_pkgname || exit 1 + echo $pkgver } build() { - cd $_pkgname - make ragnar + cd $_pkgname || exit 1 + make } package() { - cd $_pkgname - install -D -m777 ./ragnar "$pkgdir/usr/bin/ragnar" - install -D -m777 ./ragnar.desktop "$pkgdir/usr/share/applications/ragnar.desktop" - install -D -m777 ./ragnarstart "$pkgdir/usr/bin/ragnarstart" + cd $_pkgname || exit 1 + # single source of truth: same install target ./install.sh drives. + # only the systemwide default cfg is installed, user cfg is a manual cp. + # this is usually convention for most WM configs, template in /etc + make DESTDIR="$pkgdir" PREFIX=/usr install } diff --git a/README.md b/README.md index a497814..15142e6 100644 --- a/README.md +++ b/README.md @@ -32,14 +32,13 @@ Installing Ragnar involves two steps. Install the following dependencies: ```console -xcb-util -xcb-proto -xcb-util-keysyms -xcb-util-cursor -xcb-util-wm -xorg-server -xorg-xinit -mesa +libxcb \ +libx11 \ +xcb-util-keysyms \ +xcb-util-cursor \ +xcb-util-wm \ +xorg-server \ +xorg-xinit \ libconfig ``` @@ -74,11 +73,23 @@ The IPC system communicates through a socket using binary data. This allows exte Ragnar uses `libconfig` to load an external configuration file: ```console -~/.config/ragnarwm.cfg +# by default, system-wide: +/etc/ragnarwm/ragnar.cfg +# or mkdir -p and cp the default per user. +~/.config/ragnarwm/ragnar.cfg ``` The configuration is loaded on startup and can be reloaded while the window manager is running, typically through a keybinding. +You can add to `.xinitrc`: + +`exec path/to/ragnar` Then simply; `startx` + +> 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` + +You can also for example add: `polybar &` before the `exec ragnar` line. + --- ## Code Structure diff --git a/api/Makefile b/api/Makefile index 7136040..ac862d4 100644 --- a/api/Makefile +++ b/api/Makefile @@ -1,9 +1,9 @@ CC=gcc -CFLAGS= -all: lib/ragnar.a -lib/ragnar.a: lib/api.o +CFLAGS ?= +all: lib/libragnar.a +lib/libragnar.a: lib/api.o ar cr lib/libragnar.a lib/*.o -lib/api.o: lib +lib/api.o: api.c include/ragnar/api.h | lib ${CC} -c api.c -o lib/api.o ${CFLAGS} lib: diff --git a/api/lib/api.o b/api/lib/api.o deleted file mode 100644 index cc54e97..0000000 Binary files a/api/lib/api.o and /dev/null differ diff --git a/api/lib/libragnar.a b/api/lib/libragnar.a deleted file mode 100644 index 93fedb6..0000000 Binary files a/api/lib/libragnar.a and /dev/null differ diff --git a/cfg/ragnar.cfg b/cfg/ragnar.cfg index ccbd392..7482ee8 100644 --- a/cfg/ragnar.cfg +++ b/cfg/ragnar.cfg @@ -1,8 +1,8 @@ -# ██████ █████ ██████ ███ ██ █████ ██████ ██████ ██████ ███ ██ ███████ ██ ██████ -# ██ ██ ██ ██ ██ ████ ██ ██ ██ ██ ██ ██ ██ ██ ████ ██ ██ ██ ██ -# ██████ ███████ ██ ███ ██ ██ ██ ███████ ██████ ██ ██ ██ ██ ██ ██ █████ ██ ██ ███ -# ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ -# ██ ██ ██ ██ ██████ ██ ████ ██ ██ ██ ██ ██████ ██████ ██ ████ ██ ██ ██████ +# ██████ █████ ██████ ███ ██ █████ ██████ ██████ ██████ ███ ██ ███████ ██ ██████ +# ██ ██ ██ ██ ██ ████ ██ ██ ██ ██ ██ ██ ██ ██ ████ ██ ██ ██ ██ +# ██████ ███████ ██ ███ ██ ██ ██ ███████ ██████ ██ ██ ██ ██ ██ ██ █████ ██ ██ ███ +# ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ +# ██ ██ ██ ██ ██████ ██ ████ ██ ██ ██ ██ ██████ ██████ ██ ████ ██ ██ ██████ # Options: # ---------------------- @@ -70,36 +70,36 @@ # - togglescratchpad # # ---------------------- -# NOTE: For all key options, see: +# NOTE: For all key options, see: # https://github.com/cococry/ragnar/blob/main/src/structs.h#L89 # ---------------------- # -# Specifies the width of the border around client +# Specifies the width of the border around client # windows -win_border_width = 0; -# Specifies the color of the border around client +win_border_width = 2; +# Specifies the color of the border around client # windows -win_border_color = 0xE6DFDC; -# Specifies the color of the border around +win_border_color = 0x3B3B3B; +# Specifies the color of the border around # selected/focused client windows win_border_color_selected = 0xE6DFDC; -# Specifies the main modifier key that is +# Specifies the main modifier key that is # used to execute window manager shortcuts mod_key = "Super"; -# Specifies the modifier key that is used +# Specifies the modifier key that is used # to interact with clients windows win_mod = "Super"; -# Specfies the mouse button that needs to be +# 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 +# Specfies the mouse button that needs to be # held in order to resize client windows resize_button = "RightMouse"; -# Specifies the desktop index that is initially +# Specifies the desktop index that is initially # selected on every monitor initial_desktop = 0; # Specfies the number of allocated virtual @@ -109,101 +109,100 @@ num_desktops = 9; # in order. desktop_names = ["1", "2", "3", "4", "5", "6", "7", "8", "9"]; -# Specifies whether or not server-side window +# 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 +# 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 +# Specifies the height (in pixels) of the # titlebar of client windows. titlebar_height = 30 -# Specifies the color of titlebars of +# Specifies the color of titlebars of # client windows titlebar_color = 0xffffff; -# Specifies the color of the font that is used +# 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 +# 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. +# 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 +# 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 +# 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 +# Specifies the amount that the master area changes/steps # when it is decreased/increased. # (in 0.0-1.0 %) layout_master_area_step = 0.1; -# Specifies the amount that areas of windows +# 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 +# Specifies the minimum area that windows within +# layouts need to take up # (in px) layout_size_min = 150.0; -# Specifies the amount that windows are moved +# Specifies the amount that windows are moved # by when using 'move' shortcuts for floating windows # (in px) key_win_move_step = 100.0; -# Specifies the initial gap between windows +# 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 +# Specifies the amount that the gap between +# non-floating windows changes when it's # decreased/increased (in px) # (in px) win_layout_gap_step = 5; -# Specifies the layout that is initially used +# Specifies the layout that is initially used # for every virtual desktop initial_layout = "LayoutTiledMaster"; # Advanced Configuration # -------------------------------- -# Specifies the framerate at which motion notify -# events are captured. This is used to streamline +# Specifies the framerate at which motion notify +# events are captured. This is used to streamline # performance. Especially on high polling rate mouses, -# lag can be very noticable when not throtteling motion +# lag can be very noticable when not throtteling motion # notify events. -motion_notify_debounce_fps = 60; +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 +# 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. max_struts = 8; -# Specifies if decoration that is rendered with OpenGL should +# Specifies if decoration that is rendered with OpenGL should # use Vsync gl_vsync = false; -# Specifies the file where to log messages to. -# (Ignored if 'log_messages' or 'should_log_to_file' +# Messages are logged to ~/.ragnarwm.log +# (Ignored if 'log_messages' or 'should_log_to_file' # are disabled.) -log_file = "/home/cococry/ragnarwm.log"; -# Specifies the cursor image to use for the root window +# Specifies the cursor image to use for the root window cursor_image = "arrow"; # Specifies whether or not to log messages @@ -214,7 +213,7 @@ log_messages = true; # to the log file should_log_to_file = true; -# Specifies the maximum number of scratchpads +# Specifies the maximum number of scratchpads # that can be allocated max_scratchpads = 10; @@ -450,7 +449,7 @@ keybinds = ( mod = "%mod_key"; key = "KeyJ"; do = "cycledownlayout"; - }, + }, { mod = "%mod_key"; key = "KeyK"; @@ -506,7 +505,7 @@ keybinds = ( mod = "%mod_key"; key = "KeyS"; do = "runcmd"; - cmd = "dmenu &"; + cmd = "dmenu_run &"; }, { mod = "%mod_key"; diff --git a/clean.sh b/clean.sh new file mode 100755 index 0000000..827101c --- /dev/null +++ b/clean.sh @@ -0,0 +1,22 @@ +#!/usr/bin/env bash +# stupid helper to clean artifacts + +# ⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⣴⣷⣦⡀⠀⠀⠀⠀⠀⠀⠀⠀⠀ +# ⠀⠀⠀⠀⠀⠀⠀⠀⠐⢿⣿⣿⣿⣿⣦⡀⠀⠀⠀⠀⠀⠀⠀ +# ⠀⠀⠀⠀⠀⢀⣴⣿⣦⡀⠙⠉⠻⣿⣿⣿⣦⡀⠀⠀⠀⠀⠀ +# ⠀⠀⠀⢀⣴⣿⣿⣿⣿⣇⠀⠀⠀⢽⣿⣿⣿⣿⣦⡀⠀⠀⠀ +# ⠀⢀⣴⣿⣿⣿⣿⣿⣿⣿⡇⠀⣦⡀⠙⢿⢿⣿⣿⣿⣦⡀⠀ +# ⣴⣿⣿⣿⣿⣿⣿⣿⣿⣿⡇⠀⣿⣿⡆⠀⠀⠈⢻⣿⣿⣿⣆ +# ⠙⢿⣿⣿⣿⣿⣿⣿⣿⣿⡇⠀⣿⣿⣷⣄⣀⣠⣿⣿⣿⠟⠁ +# ⠀⠀⠙⢿⣿⣿⣿⣿⣿⡿⠃⠀⠻⣿⣿⣿⣿⣿⣿⠟⠁⠀⠀ +# ⠀⠀⠀⠀⠙⢿⣿⣿⣿⣇⠀⠀⠀⣽⣿⣿⣿⠟⠁⠀⠀⠀⠀ +# ⠀⠀⠀⠀⠀⠀⠙⢿⣿⣿⣷⣶⣾⣿⣿⠟⠁⠀⠀⠀⠀⠀⠀ +# ⠀⠀⠀⠀⠀⠀⠀⠀⠙⢿⣿⣿⣿⠟⠁⠀⠀⠀⠀⠀⠀⠀⠀ +# ⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠙⠛⠁⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀ + +git clean -Xnd + +read -rp "Proceed? [y/N] " reply +[[ "${reply}" == [yY] ]] || { echo "aborted"; exit 0; } + +git clean -Xfd diff --git a/compile_commands.json b/compile_commands.json deleted file mode 100644 index 8d05530..0000000 --- a/compile_commands.json +++ /dev/null @@ -1,70 +0,0 @@ -[ - { - "arguments": [ - "/usr/bin/cc", - "-c", - "-O3", - "-ffast-math", - "-Wall", - "-Wextra", - "-pedantic", - "-o", - "bin/ragnar", - "./src/config.c" - ], - "directory": "/home/cococry/dev/ragnar", - "file": "/home/cococry/dev/ragnar/src/config.c", - "output": "/home/cococry/dev/ragnar/bin/ragnar" - }, - { - "arguments": [ - "/usr/bin/cc", - "-c", - "-O3", - "-ffast-math", - "-Wall", - "-Wextra", - "-pedantic", - "-o", - "bin/ragnar", - "./src/ragnar.c" - ], - "directory": "/home/cococry/dev/ragnar", - "file": "/home/cococry/dev/ragnar/src/ragnar.c", - "output": "/home/cococry/dev/ragnar/bin/ragnar" - }, - { - "arguments": [ - "/usr/bin/cc", - "-c", - "-O3", - "-ffast-math", - "-Wall", - "-Wextra", - "-pedantic", - "-o", - "bin/ragnar", - "./src/ipc/shm.c" - ], - "directory": "/home/cococry/dev/ragnar", - "file": "/home/cococry/dev/ragnar/src/ipc/shm.c", - "output": "/home/cococry/dev/ragnar/bin/ragnar" - }, - { - "arguments": [ - "/usr/bin/cc", - "-c", - "-O3", - "-ffast-math", - "-Wall", - "-Wextra", - "-pedantic", - "-o", - "bin/ragnar", - "./src/ipc/sockets.c" - ], - "directory": "/home/cococry/dev/ragnar", - "file": "/home/cococry/dev/ragnar/src/ipc/sockets.c", - "output": "/home/cococry/dev/ragnar/bin/ragnar" - } -] diff --git a/run b/debug similarity index 100% rename from run rename to debug diff --git a/install.sh b/install.sh index 1e3027c..023538e 100755 --- a/install.sh +++ b/install.sh @@ -1,7 +1,21 @@ -cd api -make && sudo make install -cd .. -make config -make && sudo make install +#!/usr/bin/env bash +# api/ only needs building when you iterate on the IPC client lib: +# make -C api && sudo make -C api install +set -e +# we also want this to work regardless if the user is root or noot +as_root() { + if [ "$(id -u)" -eq 0 ]; then + "$@" + elif esc=$(command -v sudo) || esc=$(command -v doas); then + "$esc" "$@" + else + echo "no sudo or doas, trying su" >&2 + su root -c "$*" + fi +} +make +as_root make install + +# we do not need the make config anymore; logic unified. echo "Successfully installed ragnarwm." diff --git a/ragnarstart b/ragnarstart deleted file mode 100755 index 8b13789..0000000 --- a/ragnarstart +++ /dev/null @@ -1 +0,0 @@ - diff --git a/src/config.c b/src/config.c index ae03d76..629570f 100644 --- a/src/config.c +++ b/src/config.c @@ -314,7 +314,7 @@ replaceplaceholder(const char* str, const char* placeholder, const char* value) // Replace each occurrence of placeholder with the value while (count--) { - ins = strstr(str, placeholder); + ins = (char*)strstr(str, placeholder); len_front = ins - str; tmp = strncpy(tmp, str, len_front) + len_front; tmp = strcpy(tmp, value) + strlen(value); @@ -335,16 +335,21 @@ kbmodsfromstr(state_t* s, const char* modifiers) { return bitmask; } - modifiers = replaceplaceholder(modifiers, "%mod_key", modstr); + char* expanded = replaceplaceholder(modifiers, "%mod_key", modstr); + if (!expanded) + return bitmask; - while (*modifiers) { - while (isspace(*modifiers)) modifiers++; + const char* p = expanded; + while (*p) { + while (isspace(*p)) p++; char *end = buffer; - while (*modifiers && !strchr(delim, *modifiers)) { - *end++ = *modifiers++; + while (*p && !strchr(delim, *p)) { + if (end < buffer + sizeof(buffer) - 1) + *end++ = *p; + p++; } - *end = '\0'; + *end = '\0'; if (strcmp(buffer, "Shift") == 0) { bitmask |= Shift; @@ -356,9 +361,10 @@ kbmodsfromstr(state_t* s, const char* modifiers) { bitmask |= Super; } - while (isspace(*modifiers) || *modifiers == '|') modifiers++; + while (isspace(*p) || *p == '|') p++; } + free(expanded); return bitmask; } @@ -592,14 +598,18 @@ initconfig(state_t* s) { logmsg(s, LogLevelError, "cannot read config file because HOME is not defined."); } - char *cfg_path; + char *cfg_path = NULL; char const cfg_path_global[] = "/etc/ragnarwm/ragnar.cfg"; - asprintf(&cfg_path, "%s/.config/ragnarwm/ragnar.cfg", home); + // on failure asprintf leaves cfg_path undefined, reset to NULL + if (home && asprintf(&cfg_path, "%s/.config/ragnarwm/ragnar.cfg", home) < 0) { + cfg_path = NULL; + } - printf("ragnar: attempting to read config at %s or %s\n", cfg_path, cfg_path_global); + printf("ragnar: attempting to read config at %s or %s\n", + cfg_path ? cfg_path : "(no HOME)", cfg_path_global); if ( - !config_read_file(&cfghndl, cfg_path) + !(cfg_path && config_read_file(&cfghndl, cfg_path)) && !config_read_file(&cfghndl, cfg_path_global) ) { logmsg(s, LogLevelError, "%s:%d - %s\n", config_error_file(&cfghndl), @@ -610,7 +620,7 @@ initconfig(state_t* s) { destroyconfig(); terminate(s, EXIT_FAILURE); } - + free(cfg_path); } void diff --git a/src/funcs.h b/src/funcs.h index 6ddc9c4..f99aa06 100644 --- a/src/funcs.h +++ b/src/funcs.h @@ -338,16 +338,6 @@ void setwintype(state_t* s, client_t* cl); */ void seturgent(state_t* s, client_t* cl, bool urgent); - -/** - * @brief Returns the next on-screen client after the - * given client. - * - * @param s The window manager's state - * @param skip_floating Whether or not to skip floating clients - */ -client_t* nextvisible(state_t* s, bool skip_floating); - /** * @brief Gets the value of a given property on a * window of a given client. @@ -481,6 +471,7 @@ void updateewmhdesktops(state_t* s, monitor_t* mon); * @param mon The monitor that the virtual desktop is on */ void createdesktop(state_t* s, uint32_t idx, monitor_t* mon); +void prunedesktops(state_t* s, monitor_t* mon); /** * @brief Initializes all important atoms for EWMH & @@ -504,7 +495,10 @@ void grabkeybinds(state_t* s); * */ void loaddefaultcursor(state_t* s); -bool iswindowpopup(state_t* s, xcb_window_t win); +void setcursorhidden(state_t* s, bool hidden); +bool haswindowtype(state_t* s, xcb_window_t win, xcb_atom_t type); +bool iswindowpopup(state_t* s, xcb_window_t win); +bool iswindowdock(state_t* s, xcb_window_t win); /** * @brief Takes in a size for a client window and adjusts it diff --git a/src/keycallbacks.h b/src/keycallbacks.h index e00d189..6f859a0 100644 --- a/src/keycallbacks.h +++ b/src/keycallbacks.h @@ -261,7 +261,7 @@ inline void runcmd(state_t* s, passthrough_data_t data) { inline void addfocustolayout(state_t* s, passthrough_data_t data) { (void)data; - if(s->focus->is_scratchpad) return; + if(!s->focus || s->focus->is_scratchpad) return; s->focus->floating = false; @@ -355,6 +355,9 @@ inline void setfloatingmode(state_t* s, passthrough_data_t data) { for(client_t* cl = s->monfocus->clients; cl != NULL; cl = cl->next) { if(clientonscreen(s, cl, s->monfocus)) { cl->floating = true; + // floating layout skips makelayout, so no geometry op will + // refresh the edge handles; do it here + updateedgewindows(s, cl); } } uint32_t deskidx = mondesktop(s, s->monfocus)->idx; @@ -691,9 +694,9 @@ inline void decgapsizelayout(state_t* s, passthrough_data_t data) { * @param s The window manager's state * @param data The data to use for the function (unused here) */ -inline void movefocusup(state_t* s, passthrough_data_t data) { +inline void movefocusup(state_t* s, passthrough_data_t data) { (void)data; - if(!s->focus->floating) return; + if(!s->focus || !s->focus->floating) return; v2_t pos = s->focus->area.pos; v2_t dest = (v2_t){pos.x, @@ -711,9 +714,9 @@ inline void movefocusup(state_t* s, passthrough_data_t data) { * @param s The window manager's state * @param data The data to use for the function (unused here) */ -inline void movefocusdown(state_t* s, passthrough_data_t data) { +inline void movefocusdown(state_t* s, passthrough_data_t data) { (void)data; - if(!s->focus->floating) return; + if(!s->focus || !s->focus->floating) return; v2_t pos = s->focus->area.pos; v2_t dest = (v2_t){pos.x, @@ -731,9 +734,9 @@ inline void movefocusdown(state_t* s, passthrough_data_t data) { * @param s The window manager's state * @param data The data to use for the function (unused here) */ -inline void movefocusleft(state_t* s, passthrough_data_t data) { +inline void movefocusleft(state_t* s, passthrough_data_t data) { (void)data; - if(!s->focus->floating) return; + 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), @@ -750,13 +753,13 @@ inline void movefocusleft(state_t* s, passthrough_data_t data) { * @param s The window manager's state * @param data The data to use for the function (unused here) */ -inline void movefocusright(state_t* s, passthrough_data_t data) { +inline void movefocusright(state_t* s, passthrough_data_t data) { (void)data; - if(!s->focus->floating) return; + 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){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); s->ignore_enter_layout = true; @@ -793,6 +796,14 @@ inline void cyclefocusmonitordown(state_t* s, passthrough_data_t data) { if(!prevmon) return; + // single monitor: wrap-around lands on the same monitor; move the + // client one slot towards the master instead (symmetric to the + // in-layout move, unlike the head-insert the monitor switch does) + if(prevmon == s->focus->mon) { + cycleuplayout(s, data); + return; + } + area_t afocusmon = s->focus->mon->area; bool fs = s->focus->fullscreen; @@ -872,10 +883,18 @@ inline void cyclefocusmonitorup(state_t* s, passthrough_data_t data) { monitor_t* nextmon = s->focus->mon->next; if(!nextmon) { - nextmon = s->monitors; + nextmon = s->monitors; } if(!nextmon) return; + // single monitor: wrap-around lands on the same monitor; move the + // client one slot away from the master instead (symmetric to the + // in-layout move, unlike the head-insert the monitor switch does) + if(nextmon == s->focus->mon) { + cycledownlayout(s, data); + return; + } + area_t afocusmon = s->focus->mon->area; bool fs = s->focus->fullscreen; diff --git a/src/ragnar.c b/src/ragnar.c index 0cb3ebb..5d9c101 100644 --- a/src/ragnar.c +++ b/src/ragnar.c @@ -14,15 +14,10 @@ #include #include #include -#include #include #include -#include -#include -#include -#include #include -#include +#include #include #include @@ -34,9 +29,6 @@ #include #include -#include -#include - #include "config.h" #include "ipc/sockets.h" #include "structs.h" @@ -148,9 +140,6 @@ setup(state_t* s) { XSetErrorHandler(xerror); XSync(s->dsp, False); } - // Run the startup script - runcmd(NULL, (passthrough_data_t){.cmd = "ragnarstart"}); - // Setting up xcb connection s->con = XGetXCBConnection(s->dsp); // Checking for errors @@ -247,7 +236,7 @@ loop(state_t* s) { // Poll for events without blocking while ((ev = xcb_wait_for_event(s->con))) { uint8_t evcode = ev->response_type & ~0x80; - /* If the event we receive is listened for by our + /* If the event we receive is listened for by our * event listeners, call the callback for the event. */ if (evcode < ARRLEN(evhandlers) && evhandlers[evcode]) { evhandlers[evcode](s, ev); @@ -287,10 +276,10 @@ terminate(state_t* s, int32_t exitcode) { } } + // s->con is owned by s->dsp (XGetXCBConnection); XCloseDisplay tears the + // xcb connection down with it, a second xcb_disconnect double-frees if (s->dsp != NULL) XCloseDisplay(s->dsp); - // Give up the X connection - xcb_disconnect(s->con); logmsg(s, LogLevelTrace, "terminated with exit code %i.", exitcode); @@ -383,6 +372,12 @@ void managewins(state_t* s) { if (transient_for) continue; + // docks stay unmanaged; struts are gathered after the scan + if (iswindowdock(s, wins[i])) { + free(attr_reply); + continue; + } + if (wait_for_mapped(s, wins[i]) || getstate(s, wins[i]) == 3) { client_t* cl = makeclient(s, wins[i]); xcb_flush(s->con); @@ -433,6 +428,13 @@ void managewins(state_t* s) { void updateedgewindows(state_t* s, client_t* cl) { if (!cl->edges) return; + // pointer resize handles are floating-window UX; tiled windows + // resize through layout keybinds + bool show = cl->floating && !cl->fullscreen; + if (cl->showedgewindows != show) + toggleedgewindows(s, cl, show); + if (!show) return; + int w = cl->area.size.x; int h = cl->area.size.y; int b = EDGE_WIDTH; @@ -548,7 +550,7 @@ makeclient(state_t* s, xcb_window_t win) { updateclienthints(s, cl); // Set client's monitor - cl->mon = clmon; + cl->mon = clmon; cl->desktop = mondesktop(s, s->monfocus)->idx; logmsg(s, LogLevelTrace,"Added client on desktop %i", cl->desktop); @@ -1342,40 +1344,7 @@ seturgent(state_t* s, client_t* cl, bool urgent) { } /** - * @brief Returns the next on-screen client after the - * focused client. - * - * @param s The window manager's state - * @param skip_floating Whether or not to skip floating clients - */ -client_t* -nextvisible(state_t* s, bool skip_floating) { - client_t* next = NULL; - // Find the next client on the current monitor & desktop - for(client_t* cl = s->focus->next; cl != NULL; cl = cl->next) { - bool checktiled = (skip_floating) ? !cl->floating : true; - if(checktiled && clientonscreen(s, cl, s->monfocus)) { - next = cl; - break; - } - } - - // If there is no next client, cycle back to the first client on the - // current monitor & desktop - if(!next) { - for(client_t* cl = s->monfocus->clients; cl != NULL; cl = cl->next) { - bool checktiled = (skip_floating) ? !cl->floating : true; - if(checktiled) { - next = cl; - break; - } - } - } - return next; - } - -/** - * @brief Gets the value of a given property on a + * @brief Gets the value of a given property on a * window of a given client. * * @param s The window manager's state @@ -1497,6 +1466,28 @@ switchclientdesktop(state_t* s, client_t* cl, int32_t desktop) { } hideclient(s, cl); makelayout(s, s->monfocus); + + // Sending a client somewhere makes that desktop active; only view + // switches set this otherwise, leaving the EWMH count stale + s->monfocus->activedesktops[desktop].init = true; + + // Publish desktop count/names right away (the target desktop may + // have just been created); bars otherwise lag until a view switch + updateewmhdesktops(s, s->monfocus); + + // Sent-away client took focus with it; hand it to the first client + // (master slot) still on the visible desktop + if(!s->focus) { + for(client_t* it = s->monfocus->clients; it != NULL; it = it->next) { + if(it->desktop == mondesktop(s, s->monfocus)->idx) { + focusclient(s, it, false); + break; + } + } + } + + // IPC/EWMH moves can drain a background desktop + prunedesktops(s, s->monfocus); } /** @@ -1523,9 +1514,9 @@ switchmonitordesktop(state_t* s, int32_t desktop) { } init_i++; } - // Notify EWMH for desktop change - xcb_change_property(s->con, XCB_PROP_MODE_REPLACE, s->root, s->ewmh_atoms[EWMHcurrentDesktop], - XCB_ATOM_CARDINAL, 32, 1, &desktopidx); + // Names and count go out before current: pagers resolve the + // current index against names, stale ones render the wrong label + uploaddesktopnames(s, s->monfocus); uint32_t desktopcount = 0; for(uint32_t i = 0; i < s->monfocus->desktopcount; i++) { @@ -1535,7 +1526,10 @@ switchmonitordesktop(state_t* s, int32_t desktop) { } xcb_change_property(s->con, XCB_PROP_MODE_REPLACE, s->root, s->ewmh_atoms[EWMHnumberOfDesktops], XCB_ATOM_CARDINAL, 32, 1, &desktopcount); - uploaddesktopnames(s, s->monfocus); + + // Notify EWMH for desktop change + xcb_change_property(s->con, XCB_PROP_MODE_REPLACE, s->root, s->ewmh_atoms[EWMHcurrentDesktop], + XCB_ATOM_CARDINAL, 32, 1, &desktopidx); for (client_t* cl = s->monfocus->clients; cl != NULL; cl = cl->next) { @@ -1560,21 +1554,21 @@ switchmonitordesktop(state_t* s, int32_t desktop) { logmsg(s, LogLevelTrace, "Switched virtual desktop on monitor %i to %i", s->monfocus->idx, desktop); - s->ignore_enter_layout = false; - - // Retrieving cursor position - bool cursor_success; - v2_t cursor = cursorpos(s, &cursor_success); - if(!cursor_success) return; + // Swallow the enter events generated by hiding/showing clients; + // the pointer only decides focus again once it actually moves + // (reset in evmotionnotify) + s->ignore_enter_layout = true; - // Focusing the client on the other desktop that is hovered + // Focus the first client on the switched-to desktop for(client_t* cl = s->monfocus->clients; cl != NULL; cl = cl->next) { - if(pointinarea(cursor, cl->area)) { + if(cl->desktop == mondesktop(s, s->monfocus)->idx) { focusclient(s, cl, false); break; } } + // Leaving an emptied desktop unpublishes it + prunedesktops(s, s->monfocus); } /** @@ -1587,7 +1581,7 @@ switchmonitordesktop(state_t* s, int32_t desktop) { uint32_t numinlayout(state_t* s, monitor_t* mon) { uint32_t nlayout = 0; - for(client_t* cl = s->monfocus->clients; cl != NULL; cl = cl->next) { + for(client_t* cl = mon->clients; cl != NULL; cl = cl->next) { if(clientonscreen(s, cl, mon) && !cl->floating) { nlayout++; } @@ -1617,7 +1611,7 @@ makelayout(state_t* s, monitor_t* mon) { if(curlayout == LayoutFloating) return; /* Make sure that there is always at least one slave window */ - uint32_t nlayout = numinlayout(s, s->monfocus); + uint32_t nlayout = numinlayout(s, mon); uint32_t deskidx = mondesktop(s, mon)->idx; while(nlayout - mon->layouts[deskidx].nmaster == 0 && nlayout != 1) { mon->layouts[deskidx].nmaster--; @@ -1652,7 +1646,7 @@ makelayout(state_t* s, monitor_t* mon) { */ void resetlayoutsizes(state_t* s, monitor_t* mon) { - for(client_t* cl = s->monfocus->clients; cl != NULL; cl = cl->next) { + for(client_t* cl = mon->clients; cl != NULL; cl = cl->next) { if(cl->floating || !clientonscreen(s, cl, mon)) continue; @@ -1713,9 +1707,9 @@ tiledmaster(state_t* s, monitor_t* mon) { mon->layouts[deskidx].mastermaxed = false; uint32_t i = 0; - for(client_t* cl = s->monfocus->clients; cl != NULL; cl = cl->next) { - if(cl->floating || - cl->desktop != mondesktop(s, cl->mon)->idx || + for(client_t* cl = mon->clients; cl != NULL; cl = cl->next) { + if(cl->floating || + cl->desktop != mondesktop(s, cl->mon)->idx || cl->mon != mon) continue; if(i >= nmaster) break; @@ -1729,9 +1723,9 @@ tiledmaster(state_t* s, monitor_t* mon) { i = 0; float lastadd = 0.0f; - for(client_t* cl = s->monfocus->clients; cl != NULL; cl = cl->next) { + for(client_t* cl = mon->clients; cl != NULL; cl = cl->next) { if(cl->floating || - cl->desktop != mondesktop(s, cl->mon)->idx || + cl->desktop != mondesktop(s, cl->mon)->idx || cl->mon != mon) continue; bool ismaster = (i < nmaster); @@ -1788,7 +1782,7 @@ verticalstripes(state_t* s, monitor_t* mon) { int32_t gapsize = mon->layouts[deskidx].gapsize; { - for(client_t* cl = s->monfocus->clients; cl != NULL; cl = cl->next) { + for(client_t* cl = mon->clients; cl != NULL; cl = cl->next) { if(cl->floating || cl->desktop != mondesktop(s, cl->mon)->idx || cl->mon != mon) continue; nwins++; @@ -1802,8 +1796,8 @@ verticalstripes(state_t* s, monitor_t* mon) { // Apply strut information to the layout for(uint32_t i = 0; i < s->nwinstruts; i++) { - bool onmonitor = - s->winstruts[i].startx >= mon->area.pos.x + bool onmonitor = + s->winstruts[i].startx >= mon->area.pos.x && s->winstruts[i].endx <= mon->area.pos.x + mon->area.size.x; if(!onmonitor) continue; @@ -1823,10 +1817,10 @@ verticalstripes(state_t* s, monitor_t* mon) { h -= s->winstruts[i].bottom; } } - + float lastadd = 0.0f; - for(client_t* cl = s->monfocus->clients; cl != NULL; cl = cl->next) { + for(client_t* cl = mon->clients; cl != NULL; cl = cl->next) { if(cl->floating || cl->desktop != mondesktop(s, cl->mon)->idx || cl->mon != mon) continue; float winw = (float)w / nwins + cl->layoutsizeadd - lastadd; @@ -1859,7 +1853,7 @@ horizontalstripes(state_t* s, monitor_t* mon) { int32_t gapsize = mon->layouts[deskidx].gapsize; { - for(client_t* cl = s->monfocus->clients; cl != NULL; cl = cl->next) { + for(client_t* cl = mon->clients; cl != NULL; cl = cl->next) { if(cl->floating || cl->desktop != mondesktop(s, cl->mon)->idx || cl->mon != mon) continue; nwins++; @@ -1897,7 +1891,7 @@ horizontalstripes(state_t* s, monitor_t* mon) { float lastadd = 0.0f; - for(client_t* cl = s->monfocus->clients; cl != NULL; cl = cl->next) { + for(client_t* cl = mon->clients; cl != NULL; cl = cl->next) { if(cl->floating || cl->desktop != mondesktop(s, cl->mon)->idx || cl->mon != mon) continue; float winh = (float)h / nwins + cl->layoutsizeadd - lastadd; @@ -1993,12 +1987,13 @@ uploaddesktopnames(state_t* s, monitor_t* mon) { xcb_screen_t *screen = xcb_setup_roots_iterator(xcb_get_setup(s->con)).data; xcb_window_t root_window = screen->root; - // Set the _NET_DESKTOP_NAMES property + // Set the _NET_DESKTOP_NAMES property; EWMH mandates UTF8_STRING, + // pagers reject a STRING-typed value and fall back to numbering xcb_change_property(s->con, XCB_PROP_MODE_REPLACE, root_window, s->ewmh_atoms[EWMHdesktopNames], - XCB_ATOM_STRING, + getatom(s, "UTF8_STRING"), 8, total_length, data); @@ -2008,6 +2003,9 @@ uploaddesktopnames(state_t* s, monitor_t* mon) { void updateewmhdesktops(state_t* s, monitor_t* mon) { s->monfocus = mon; + // Names and count go out before current: pagers resolve the + // current index against names, stale ones render the wrong label + uploaddesktopnames(s, s->monfocus); uint32_t desktopcount = 0; for(uint32_t i = 0; i < s->monfocus->desktopcount; i++) { if(s->monfocus->activedesktops[i].init) { @@ -2016,17 +2014,59 @@ updateewmhdesktops(state_t* s, monitor_t* mon) { } xcb_change_property(s->con, XCB_PROP_MODE_REPLACE, s->root, s->ewmh_atoms[EWMHnumberOfDesktops], XCB_ATOM_CARDINAL, 32, 1, &desktopcount); - uploaddesktopnames(s, s->monfocus); desktop_t* desk = mondesktop(s, s->monfocus); if(desk) { + // _NET_DESKTOP_NAMES only lists published desktops, so the + // current desktop must be its position in that list; the raw + // idx drifts as soon as pruning leaves a gap + uint32_t ewmhidx = 0; + for(uint32_t i = 0; i < desk->idx && i < s->monfocus->desktopcount; i++) { + if(s->monfocus->activedesktops[i].init) { + ewmhidx++; + } + } xcb_change_property(s->con, XCB_PROP_MODE_REPLACE, s->root, s->ewmh_atoms[EWMHcurrentDesktop], - XCB_ATOM_CARDINAL, 32, 1, &desk->idx); + XCB_ATOM_CARDINAL, 32, 1, &ewmhidx); + } +} + +/** + * @brief Unpublishes active desktops that hold no client anymore and + * notifies EWMH if any were removed. The currently viewed desktop and + * the default desktop stay published even when empty. + * + * @param s The window manager's state + * @param mon The monitor whose desktops are pruned + */ +void +prunedesktops(state_t* s, monitor_t* mon) { + desktop_t* curdesk = mondesktop(s, mon); + bool changed = false; + for(uint32_t i = 0; i < mon->desktopcount; i++) { + if(!mon->activedesktops[i].init) continue; + if(i == s->config.desktopinit) continue; + if(curdesk && i == curdesk->idx) continue; + + bool occupied = false; + for(client_t* cl = mon->clients; cl != NULL; cl = cl->next) { + if(cl->desktop == i) { + occupied = true; + break; + } + } + if(!occupied) { + mon->activedesktops[i].init = false; + changed = true; + } + } + if(changed) { + updateewmhdesktops(s, mon); } } /** * @brief Creates a new virtual desktop and notifies EWMH about it. - * @param s The window manager's state + * @param s The window manager's state * @param idx The index of the virtual desktop * @param mon The monitor that the virtual desktop is on */ @@ -2068,6 +2108,7 @@ setupatoms(state_t* s) { s->ewmh_atoms[EWMHwindowType] = getatom(s, "_NET_WM_WINDOW_TYPE"); 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"); s->ewmh_atoms[EWMHclientList] = getatom(s, "_NET_CLIENT_LIST"); s->ewmh_atoms[EWMHcurrentDesktop] = getatom(s, "_NET_CURRENT_DESKTOP"); s->ewmh_atoms[EWMHnumberOfDesktops] = getatom(s, "_NET_NUMBER_OF_DESKTOPS"); @@ -2174,14 +2215,41 @@ loaddefaultcursor(state_t* s) { xcb_cursor_context_free(context); logmsg(s, LogLevelTrace, "loaded cursor image '%s'.", s->config.cursorimage); + + // XFixes lets us hide the cursor until the pointer actually moves; + // the version handshake is required before any other xfixes request + xcb_xfixes_query_version_reply_t* xfixes_reply = xcb_xfixes_query_version_reply( + s->con, xcb_xfixes_query_version(s->con, XCB_XFIXES_MAJOR_VERSION, + XCB_XFIXES_MINOR_VERSION), NULL); + s->xfixes_ok = xfixes_reply != NULL; + free(xfixes_reply); + if(!s->xfixes_ok) { + logmsg(s, LogLevelError, "XFixes unavailable, cursor auto-hide disabled."); + } + setcursorhidden(s, true); } -bool -iswindowpopup(state_t* s, xcb_window_t win) { - xcb_atom_t typeatom = s->ewmh_atoms[EWMHwindowType]; - xcb_atom_t popupatom = s->ewmh_atoms[EWMHwindowTypePopup]; +/** + * @brief Hides or shows the cursor (no-op without XFixes). The cursor + * is hidden on keybind use and shown again on real pointer motion. + * */ +void +setcursorhidden(state_t* s, bool hidden) { + if(!s->xfixes_ok || s->cursorhidden == hidden) return; + if(hidden) { + xcb_xfixes_hide_cursor(s->con, s->root); + } else { + xcb_xfixes_show_cursor(s->con, s->root); + } + s->cursorhidden = hidden; + xcb_flush(s->con); +} + +bool +haswindowtype(state_t* s, xcb_window_t win, xcb_atom_t type) { + xcb_atom_t typeatom = s->ewmh_atoms[EWMHwindowType]; - if (typeatom == XCB_ATOM_NONE || popupatom == XCB_ATOM_NONE) + if (typeatom == XCB_ATOM_NONE || type == XCB_ATOM_NONE) return false; xcb_get_property_cookie_t prop_cookie = xcb_get_property( @@ -2190,20 +2258,30 @@ iswindowpopup(state_t* s, xcb_window_t win) { if (!prop_reply) return false; - bool ispopup = false; + bool hastype = false; if (xcb_get_property_value_length(prop_reply) > 0) { xcb_atom_t* atoms = (xcb_atom_t*) xcb_get_property_value(prop_reply); int len = xcb_get_property_value_length(prop_reply) / sizeof(xcb_atom_t); for (int i = 0; i < len; ++i) { - if (atoms[i] == popupatom) { - ispopup = true; + if (atoms[i] == type) { + hastype = true; break; } } } free(prop_reply); - return ispopup; + return hastype; +} + +bool +iswindowpopup(state_t* s, xcb_window_t win) { + return haswindowtype(s, win, s->ewmh_atoms[EWMHwindowTypePopup]); +} + +bool +iswindowdock(state_t* s, xcb_window_t win) { + return haswindowtype(s, win, s->ewmh_atoms[EWMHwindowTypeDock]); } /** @@ -2333,7 +2411,7 @@ enumartelayout(state_t* s, monitor_t* mon, uint32_t* nmaster, uint32_t* nslaves) } uint32_t i = 0; - for(client_t* cl = s->monfocus->clients; cl != NULL; cl = cl->next) { + for(client_t* cl = mon->clients; cl != NULL; cl = cl->next) { if(cl->floating || cl->desktop != mondesktop(s, cl->mon)->idx || cl->mon != mon) continue; if(i >= *nmaster) { @@ -2363,7 +2441,7 @@ isclientmaster(state_t* s, client_t* cl, monitor_t* mon) { uint32_t deskidx = mondesktop(s, mon)->idx; uint32_t nmaster = mon->layouts[deskidx].nmaster; - for(client_t* iter = s->monfocus->clients; iter != NULL; iter = iter->next) { + for(client_t* iter = mon->clients; iter != NULL; iter = iter->next) { if(iter->floating || iter->desktop != mondesktop(s, iter->mon)->idx || iter->mon != mon) continue; @@ -2418,18 +2496,28 @@ evmaprequest(state_t* s, xcb_generic_event_t* ev) { return; } - if (wa_reply->override_redirect) { + bool override_redirect = wa_reply->override_redirect; + free(wa_reply); + if (override_redirect) { return; } - // Free the reply after checking - free(wa_reply); - // Don't handle already managed clients if (clientfromwin(s, map_ev->window) != NULL) { return; } + // Docks (status bars etc.) stay unmanaged: map them as-is and + // reserve the space their struts request + if (iswindowdock(s, map_ev->window)) { + xcb_map_window(s->con, map_ev->window); + s->nwinstruts = 0; + getwinstruts(s, s->root); + makelayout(s, s->monfocus); + xcb_flush(s->con); + return; + } + // Handle new client client_t* cl = makeclient(s, map_ev->window); @@ -2462,6 +2550,13 @@ evmaprequest(state_t* s, xcb_generic_event_t* ev) { s->mapping_scratchpad_index = -1; } + // Newly spawned windows take focus (and the active border) right + // away instead of waiting for the pointer to wander into them. + // The relayout shifts windows under the stationary pointer; swallow + // the resulting enter events so they can't steal the focus back. + s->ignore_enter_layout = true; + focusclient(s, cl, true); + xcb_flush(s->con); } void @@ -2554,6 +2649,13 @@ void evunmapnotify(state_t* s, xcb_generic_event_t* ev) { // Retrieve the event xcb_unmap_notify_event_t* unmap_ev = (xcb_unmap_notify_event_t*)ev; + // A dock going away frees the space its struts reserved + if(iswindowdock(s, unmap_ev->window)) { + s->nwinstruts = 0; + getwinstruts(s, s->root); + makelayout(s, s->monfocus); + return; + } if(iswindowpopup(s, unmap_ev->window)) { logmsg(s, LogLevelTrace, "remoed popup window. ", unmap_ev->window); int32_t idx = -1; @@ -2573,6 +2675,8 @@ evunmapnotify(state_t* s, xcb_generic_event_t* ev) { cl->ignoreunmap = false; return; } + // cl is freed by releaseclient below; monitors outlive it + monitor_t* clmon = cl ? cl->mon : NULL; if(cl) { if(cl->is_scratchpad) { @@ -2593,6 +2697,11 @@ evunmapnotify(state_t* s, xcb_generic_event_t* ev) { // Re-establish the window layout makelayout(s, s->monfocus); + // Client may have been the last one on a background desktop + if(clmon) { + prunedesktops(s, clmon); + } + xcb_flush(s->con); } @@ -2614,8 +2723,10 @@ evdestroynotify(state_t* s, xcb_generic_event_t* ev) { if(cl->is_scratchpad) { removescratchpad(s, cl->scratchpad_index); } + monitor_t* clmon = cl->mon; unframeclient(s, cl); releaseclient(s, destroy_ev->window); + prunedesktops(s, clmon); } /** @@ -2684,6 +2795,7 @@ evkeypress(state_t* s, xcb_generic_event_t* ev) { // If it was pressed, call the callback of the keybind if ((keysym == s->config.keybinds[i].key) && (e->state == s->config.keybinds[i].modmask)) { if(s->config.keybinds[i].cb) { + setcursorhidden(s, true); s->config.keybinds[i].cb(s, s->config.keybinds[i].data); } } @@ -2802,6 +2914,7 @@ evmotionnotify(state_t* s, xcb_generic_event_t* ev) { } s->lastmotiontime = curtime; s->ignore_enter_layout = false; + setcursorhidden(s, false); if (motion_ev->event == s->root) { monitor_t* mon = cursormon(s); @@ -3100,6 +3213,25 @@ evpropertynotify(state_t* s, xcb_generic_event_t* ev) { void evclientmessage(state_t* s, xcb_generic_event_t* ev) { xcb_client_message_event_t* msg_ev = (xcb_client_message_event_t*)ev; + + // Pagers request desktop switches with an index into the published + // (compressed) desktop list; map it back to the raw desktop index + if(msg_ev->type == s->ewmh_atoms[EWMHcurrentDesktop]) { + if(!s->monfocus) return; + uint32_t target = msg_ev->data.data32[0]; + uint32_t pos = 0; + for(uint32_t i = 0; i < s->monfocus->desktopcount; i++) { + if(!s->monfocus->activedesktops[i].init) continue; + if(pos == target) { + switchmonitordesktop(s, i); + xcb_flush(s->con); + break; + } + pos++; + } + return; + } + client_t* cl = clientfromwin(s, msg_ev->window); if(!cl) { @@ -3229,12 +3361,26 @@ releaseclient(state_t* s, xcb_window_t win) { * associated with the window */ while(cl) { if(cl->win == win) { - /* Setting the pointer to previous client to the next client + /* Setting the pointer to previous client to the next client * after the client we want to release, effectivly removing it - * from our list of clients*/ + * from our list of clients*/ *prev = cl->next; + bool wasfocus = (cl == s->focus); + if(wasfocus) { + s->focus = NULL; + } // Freeing memory allocated for client free(cl); + // Released client held focus; hand it to the first client + // (master slot) still on the visible desktop + if(wasfocus && s->monfocus) { + for(client_t* it = s->monfocus->clients; it != NULL; it = it->next) { + if(it->desktop == mondesktop(s, s->monfocus)->idx) { + focusclient(s, it, false); + break; + } + } + } return; } // Advancing the client diff --git a/src/structs.h b/src/structs.h index 858e0a8..fe5ca6a 100644 --- a/src/structs.h +++ b/src/structs.h @@ -5,11 +5,8 @@ #include #include +#include #include -#include - -#include -#include #include #define EDGE_WIDTH 5 @@ -142,8 +139,9 @@ typedef enum { EWMHfullscreen, EWMHactiveWindow, EWMHwindowType, - EWMHwindowTypeDialog, - EWMHwindowTypePopup, + EWMHwindowTypeDialog, + EWMHwindowTypePopup, + EWMHwindowTypeDock, EWMHclientList, EWMHcurrentDesktop, EWMHnumberOfDesktops, @@ -510,7 +508,6 @@ typedef struct { struct state_t { window_edge_t grabedge; xcb_connection_t* con; - xcb_ewmh_connection_t ewmh; xcb_window_t root; xcb_screen_t* screen; @@ -520,6 +517,9 @@ struct state_t { bool ignore_enter_layout; + bool xfixes_ok; + bool cursorhidden; + client_t* focus; popup_list_t popups;