foxygit / dotfiles Log in
commit ed28fa2f1a024805b4345e98cadbe4033626efee
Author:     mrfox <jens.se@icloud.com>
AuthorDate: Tue Sep 22 19:40:23 2026 +0200
Commit:     mrfox <jens.se@icloud.com>
CommitDate: Tue Sep 22 19:40:23 2026 +0200

    sxwm: add focus-follows-mouse (hover to focus)

    sxwm already selects EnterWindowMask on every client but never had a
    handler for it -- hovering a window only changed the cursor, not focus;
    only clicking did. Added an EnterNotify handler behind a new opt-in
    focus_follows_mouse config key (defaults to off upstream; this repo's
    sxwmrc turns it on).

    Ignores crossings from an active pointer grab (mode != NotifyNormal, so
    mid-drag/resize doesn't fight focus) and crossings into a child of the
    already-focused window (detail == NotifyInferior, the standard sloppy-
    focus guard against flicker). Only changes input focus -- doesn't raise
    the window or warp the cursor (the mouse is already there).

    Verified: applies cleanly on top of the existing 6 local sxwm patches
    and builds clean with -Wall -Wextra. Not yet installed live -- swapping
    the running window manager needs the user to do it (sudo, and it's
    disruptive to test blind).

    Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
---
 README.md                 |  3 +-
 config/sxwmrc             |  1 +
 focus-follows-mouse.patch | 81 +++++++++++++++++++++++++++++++++++++++++++++++
 install.sh                |  7 ++++
 4 files changed, 91 insertions(+), 1 deletion(-)

diff --git a/README.md b/README.md
index 107c24e..ad25260 100644
--- a/README.md
+++ b/README.md
@@ -61,7 +61,7 @@ and you have a working environment.

 | | |
 |---|---|
-| 🪟 | **sxwm** — tiling window manager, patched with 5 local fixes (DPI, taskbar focus, multi-monitor struts, hotplug remap, fullscreen targeting) |
+| 🪟 | **sxwm** — tiling window manager, patched with 6 local fixes/additions (DPI, taskbar focus, multi-monitor struts, hotplug remap, fullscreen targeting, focus-follows-mouse) |
 | 📊 | **sxbar** — status bar with clock, date, battery, volume, CPU, brightness, Bluetooth, network, media player, taskbar and start menu popups |
 | 🚀 | **dmenu** — fast application launcher (`mod + p`), centered on screen with results listed vertically |
 | 📶 | **bluetui** — Bluetooth management right in the terminal |
@@ -198,6 +198,7 @@ order:
 | [`monitor-hotplug-remap.patch`](monitor-hotplug-remap.patch) | Windows lost their correct monitor association on hotplug (e.g. `lid-display-watch` toggling eDP); monitor ownership is now recomputed from each window's actual position |
 | [`fullscreen-monitor-fix.patch`](fullscreen-monitor-fix.patch) | Fullscreening a window that had been moved to another monitor by an external tool (`xdotool`, `screensaver-launch`) fullscreened it on the wrong screen; the target monitor is now recomputed from the window's real geometry |
 | [`strut-fresh-screen-size.patch`](strut-fresh-screen-size.patch) | A bottom/right-edge panel (e.g. sxbar's `secondary_bar`) could end up hidden behind normal windows — `reserve_bottom`/`reserve_right` are measured from the *opposite* screen edge and need the full screen size, but read it from `scr_width`/`scr_height`, cached globals only refreshed by `update_mons()`; `reserve_top`/`reserve_left` never depend on screen size so stayed correct. Reads the screen size fresh from the X server instead |
+| [`focus-follows-mouse.patch`](focus-follows-mouse.patch) | sxwm selects `EnterWindowMask` on every client already but never had a handler for it — hovering a window never focused it, only clicking did. Adds an `EnterNotify` handler behind a new `focus_follows_mouse` config key (opt-in, off by default upstream; this repo's `sxwmrc` turns it on). Only changes input focus — doesn't raise the window or warp the cursor, matching classic sloppy-focus behavior |

 ## dmenu patches

diff --git a/config/sxwmrc b/config/sxwmrc
index ec94c76..36b5357 100644
--- a/config/sxwmrc
+++ b/config/sxwmrc
@@ -23,6 +23,7 @@ motion_throttle         : 60 # Set to screen refresh rate for smoothest motions
 should_float            : "pcmanfm", "obs", "screensaver"
 new_win_focus           : true
 warp_cursor             : true
+focus_follows_mouse     : true
 floating_on_top         : true
 new_win_master          : false
 can_swallow             : "st"
diff --git a/focus-follows-mouse.patch b/focus-follows-mouse.patch
new file mode 100644
index 0000000..16a0cc2
--- /dev/null
+++ b/focus-follows-mouse.patch
@@ -0,0 +1,81 @@
+diff --git a/src/defs.h b/src/defs.h
+index ac8f74f..a18731f 100644
+--- a/src/defs.h
++++ b/src/defs.h
+@@ -96,6 +96,7 @@ typedef struct {
+ 	int resize_window_amt;
+ 	Bool new_win_focus;
+ 	Bool warp_cursor;
++	Bool focus_follows_mouse;
+ 	Bool floating_on_top;
+ 	Bool new_win_master;
+ 	Binding binds[MAX_ITEMS];
+diff --git a/src/parser.c b/src/parser.c
+index e50db00..ed09899 100644
+--- a/src/parser.c
++++ b/src/parser.c
+@@ -372,6 +372,8 @@ int parser(Config *cfg)
+ 		}
+ 		else if (!strcmp(key, "floating_on_top"))
+ 			cfg->floating_on_top = !strcmp(rest, "true");
++		else if (!strcmp(key, "focus_follows_mouse"))
++			cfg->focus_follows_mouse = !strcmp(rest, "true");
+ 		else if (!strcmp(key, "focused_border_colour"))
+ 			cfg->border_foc_col = parse_col(rest);
+ 		else if (!strcmp(key, "gaps"))
+diff --git a/src/sxwm.c b/src/sxwm.c
+index c30db92..d30d023 100644
+--- a/src/sxwm.c
++++ b/src/sxwm.c
+@@ -64,6 +64,7 @@ void hdl_config_ntf(XEvent *xev);
+ void hdl_config_req(XEvent *xev);
+ void hdl_dummy(XEvent *xev);
+ void hdl_destroy_ntf(XEvent *xev);
++void hdl_enter_ntf(XEvent *xev);
+ void hdl_keypress(XEvent *xev);
+ void hdl_mapping_ntf(XEvent *xev);
+ void hdl_map_req(XEvent *xev);
+@@ -1174,6 +1175,27 @@ void hdl_destroy_ntf(XEvent *xev)
+ 	}
+ }
+
++void hdl_enter_ntf(XEvent *xev)
++{
++	XCrossingEvent *crossing_ev = &xev->xcrossing;
++
++	if (!user_config.focus_follows_mouse)
++		return;
++
++	/* Ignore crossings from a pointer grab (drag/resize in progress) and
++	 * crossings into a child of the window we're already in -- both fire
++	 * EnterNotify without the pointer actually landing on a new client,
++	 * and would otherwise flicker focus or fight an active drag. */
++	if (crossing_ev->mode != NotifyNormal || crossing_ev->detail == NotifyInferior)
++		return;
++
++	Client *c = find_client(find_toplevel(crossing_ev->window));
++	if (!c || c == focused || c->ws != current_ws)
++		return;
++
++	set_input_focus(c, False, False);
++}
++
+ void hdl_keypress(XEvent *xev)
+ {
+ 	KeyCode code = xev->xkey.keycode;
+@@ -1627,6 +1649,7 @@ void init_defaults(void)
+ 	user_config.n_binds = 0;
+ 	user_config.new_win_focus = True;
+ 	user_config.warp_cursor = True;
++	user_config.focus_follows_mouse = False;
+ 	user_config.new_win_master = False;
+ 	user_config.floating_on_top = True;
+ }
+@@ -2301,6 +2324,7 @@ void setup(void)
+ 	evtable[ConfigureNotify] = hdl_config_ntf;
+ 	evtable[ConfigureRequest] = hdl_config_req;
+ 	evtable[DestroyNotify] = hdl_destroy_ntf;
++	evtable[EnterNotify] = hdl_enter_ntf;
+ 	evtable[KeyPress] = hdl_keypress;
+ 	evtable[MappingNotify] = hdl_mapping_ntf;
+ 	evtable[MapRequest] = hdl_map_req;
diff --git a/install.sh b/install.sh
index cceb5e2..99c72eb 100755
--- a/install.sh
+++ b/install.sh
@@ -471,6 +471,13 @@ step_sxwm() {
     # monitorns egen x/y, och påverkades inte). Läser bredd/höjd direkt från
     # X-servern i update_struts() istället för att lita på cachen.
     patch -p1 < "$SCRIPT_DIR/strut-fresh-screen-size.patch"
+    # sxwm never had focus-follows-mouse -- it selects EnterWindowMask on
+    # every client already but has no handler for it (defaults to a no-op),
+    # so hovering a window never focused it, only clicking did. Adds an
+    # EnterNotify handler behind a new opt-in "focus_follows_mouse" config
+    # key (default off); doesn't raise or warp the cursor, just changes
+    # input focus, matching classic sloppy-focus behavior.
+    patch -p1 < "$SCRIPT_DIR/focus-follows-mouse.patch"

     make
     sudo make install