Skip to content

Instantly share code, notes, and snippets.

@svandragt
Last active January 18, 2026 12:15
Show Gist options
  • Select an option

  • Save svandragt/76f3844988e1cb02779093903f59fa49 to your computer and use it in GitHub Desktop.

Select an option

Save svandragt/76f3844988e1cb02779093903f59fa49 to your computer and use it in GitHub Desktop.
Lower Active Window and Cycle Focus on the Same Monitor (X11)
#!/usr/bin/env bash
set -euo pipefail
DEBUG="${DEBUG:-0}"
LOG_FILE="${LOG_FILE:-$HOME/.cache/lower-cycle.log}"
mkdir -p "$(dirname "$LOG_FILE")"
ts() { date +'%H:%M:%S'; }
log() { (( DEBUG >= 1 )) && printf '[%s] %s\n' "$(ts)" "$*" | tee -a "$LOG_FILE" >&2; }
log2() { (( DEBUG >= 2 )) && printf '[%s] %s\n' "$(ts)" "$*" | tee -a "$LOG_FILE" >&2; }
on_err() {
local ec=$?
printf '[%s] ERROR line=%s cmd=%q exit=%s\n' "$(ts)" "${BASH_LINENO[0]}" "$BASH_COMMAND" "$ec" | tee -a "$LOG_FILE" >&2
exit "$ec"
}
trap on_err ERR
# ---------------------------
# Monitor model
# ---------------------------
declare -a MON_NAME MON_X MON_Y MON_W MON_H
load_monitors() {
MON_NAME=(); MON_X=(); MON_Y=(); MON_W=(); MON_H=()
while IFS= read -r line; do
[[ -n "$line" ]] || continue
local name geom w h x y
name="$(awk '{print $2}' <<<"$line" | sed 's/^[+*]*//')"
geom="$(awk '{print $3}' <<<"$line")"
w="$(sed -n 's#^\([0-9]\+\)/.*#\1#p' <<<"$geom")"
h="$(sed -n 's#^[0-9]\+/.*x\([0-9]\+\)/.*#\1#p' <<<"$geom")"
x="$(sed -n 's#^.*+\([0-9]\+\)+\([0-9]\+\)$#\1#p' <<<"$geom")"
y="$(sed -n 's#^.*+\([0-9]\+\)+\([0-9]\+\)$#\2#p' <<<"$geom")"
if [[ -n "$name" && -n "$w" && -n "$h" && -n "$x" && -n "$y" ]]; then
MON_NAME+=("$name"); MON_W+=("$w"); MON_H+=("$h"); MON_X+=("$x"); MON_Y+=("$y")
fi
done < <(xrandr --listmonitors 2>/dev/null | tail -n +2)
if (( ${#MON_NAME[@]} == 0 )); then
log "No monitors found via xrandr --listmonitors (monitor filtering disabled)."
return 1
fi
log "Monitors:"
for i in "${!MON_NAME[@]}"; do
log " [$i] ${MON_NAME[$i]}: ${MON_W[$i]}x${MON_H[$i]}+${MON_X[$i]}+${MON_Y[$i]}"
done
}
monitor_for_point() {
local px="$1" py="$2"
for i in "${!MON_NAME[@]}"; do
local x="${MON_X[$i]}" y="${MON_Y[$i]}" w="${MON_W[$i]}" h="${MON_H[$i]}"
if (( px >= x && px < x + w && py >= y && py < y + h )); then
printf '%s' "$i"
return 0
fi
done
return 1
}
# ---------------------------
# Window helpers
# ---------------------------
window_geom() {
local wid_dec="$1"
local out
out="$(xdotool getwindowgeometry --shell "$wid_dec" 2>/dev/null || true)"
[[ -n "$out" ]] || return 1
local X="" Y="" WIDTH="" HEIGHT=""
# shellcheck disable=SC1090
source <(printf '%s\n' "$out")
[[ -n "${X:-}" && -n "${Y:-}" && -n "${WIDTH:-}" && -n "${HEIGHT:-}" ]] || return 1
printf '%s %s %s %s\n' "$X" "$Y" "$WIDTH" "$HEIGHT"
}
monitor_for_window_center() {
local wid_dec="$1"
local gx gy gw gh
read -r gx gy gw gh < <(window_geom "$wid_dec")
local cx=$((gx + gw / 2))
local cy=$((gy + gh / 2))
monitor_for_point "$cx" "$cy"
}
is_skippable_window() {
local wid_hex="$1"
local t
t="$(xprop -id "$wid_hex" _NET_WM_WINDOW_TYPE 2>/dev/null || true)"
grep -qE '_NET_WM_WINDOW_TYPE_(DOCK|DESKTOP)' <<<"$t"
}
window_title() {
local wid_hex="$1"
xprop -id "$wid_hex" _NET_WM_NAME 2>/dev/null \
| sed -n 's/^.*= \(.*\)$/\1/p' \
| sed 's/^"//; s/"$//'
}
window_class() {
local wid_hex="$1"
xprop -id "$wid_hex" WM_CLASS 2>/dev/null \
| sed -n 's/^.*= \(.*\)$/\1/p'
}
# ---------------------------
# Stacking helpers
# ---------------------------
stacking_list_hex() {
local raw
raw="$(xprop -root _NET_CLIENT_LIST_STACKING 2>/dev/null || true)"
[[ -n "$raw" ]] || return 1
sed -n 's/^.*# //p' <<<"$raw" \
| tr -d ',' \
| tr ' ' '\n' \
| sed '/^$/d'
}
find_index_in_array() {
local needle="$1"; shift
local i=0
for item in "$@"; do
[[ "$item" == "$needle" ]] && { printf '%s\n' "$i"; return 0; }
((i++))
done
return 1
}
pick_target_under_active_same_monitor() {
local active_hex="$1"
local active_mon_idx="${2:-}"
mapfile -t stack_hex < <(stacking_list_hex)
local idx
idx="$(find_index_in_array "$active_hex" "${stack_hex[@]}")"
log "Considering windows under active (nearest-under to bottom):"
for ((j=idx-1; j>=0; j--)); do
local cand_hex="${stack_hex[$j]}"
local cand_dec="$((cand_hex))"
local prefix="$cand_hex ($cand_dec)"
if is_skippable_window "$cand_hex"; then
log " $prefix -> skip: DOCK/DESKTOP"
continue
fi
if is_hidden_window "$cand_hex"; then
log " $prefix -> skip: hidden/minimized"
continue
fi
# Workspace filter: only consider windows on current workspace, plus sticky
if [[ -n "${curdesk:-}" ]]; then
if ! same_workspace_or_sticky "$cand_hex" "$curdesk"; then
log " $prefix -> skip: other workspace"
continue
fi
fi
if (( ${#MON_NAME[@]} > 0 )) && [[ -n "$active_mon_idx" ]]; then
local cand_mon_idx=""
cand_mon_idx="$(monitor_for_window_center "$cand_dec" 2>/dev/null || true)"
if [[ -z "$cand_mon_idx" ]]; then
log " $prefix -> skip: monitor unknown"
continue
fi
if [[ "$cand_mon_idx" != "$active_mon_idx" ]]; then
log " $prefix -> skip: other monitor (${MON_NAME[$cand_mon_idx]})"
continue
fi
fi
if (( DEBUG >= 2 )); then
local d; d="$(window_desktop "$cand_hex" || true)"
log2 " $prefix -> hidden state: $(window_net_wm_state "$cand_hex" | tr '\n' ' ') | $(window_wm_state "$cand_hex" | tr '\n' ' ')"
log2 " $prefix -> ACCEPT desk=${d:-?} class=$(window_class "$cand_hex") title=$(window_title "$cand_hex")"
else
log " $prefix -> ACCEPT"
fi
printf '%s\n' "$cand_dec"
return 0
done
}
window_net_wm_state() {
# arg: window id in hex (0x...)
xprop -id "$1" _NET_WM_STATE 2>/dev/null || true
}
window_wm_state() {
# arg: window id in hex (0x...)
xprop -id "$1" WM_STATE 2>/dev/null || true
}
is_hidden_window() {
# arg: window id in hex (0x...)
local s
# EWMH: hidden/minimized
s="$(window_net_wm_state "$1")"
if grep -q '_NET_WM_STATE_HIDDEN' <<<"$s"; then
return 0
fi
# ICCCM fallback: IconicState == minimized
s="$(window_wm_state "$1")"
if grep -q 'IconicState' <<<"$s"; then
return 0
fi
return 1
}
# ---------------------------
# Workspace helpers
# ---------------------------
current_desktop() {
# prints an integer like 0,1,2...
xprop -root _NET_CURRENT_DESKTOP 2>/dev/null \
| awk -F' = ' '{print $2}' \
| tr -d '\n'
}
window_desktop() {
# arg: window id in hex (0x...)
# prints: integer desktop index, or -1 for sticky, or empty if missing
xprop -id "$1" _NET_WM_DESKTOP 2>/dev/null \
| awk -F' = ' '{print $2}' \
| tr -d '\n'
}
same_workspace_or_sticky() {
# args: cand_hex, curdesk
local cand_hex="$1" curdesk="$2"
local desk
desk="$(window_desktop "$cand_hex" || true)"
[[ -n "$desk" ]] || return 1
# Sticky windows often show as 0xFFFFFFFF or 4294967295; normalize to -1.
if [[ "$desk" == "0xFFFFFFFF" || "$desk" == "4294967295" ]]; then
return 0
fi
[[ "$desk" == "$curdesk" ]]
}
main() {
: > "$LOG_FILE" # truncate each run so it’s easy to inspect
log "Starting (DEBUG=$DEBUG) PID=$$"
local curdesk=""
curdesk="$(current_desktop || true)"
log "Current desktop: ${curdesk:-UNKNOWN}"
local active_dec active_hex
active_dec="$(xdotool getactivewindow)"
active_hex="$(printf '0x%x' "$active_dec")"
load_monitors || true
local active_mon_idx=""
if (( ${#MON_NAME[@]} > 0 )); then
active_mon_idx="$(monitor_for_window_center "$active_dec" 2>/dev/null || true)"
fi
if [[ -n "$active_mon_idx" ]]; then
log "Active window: $active_hex monitor=[${active_mon_idx}] ${MON_NAME[$active_mon_idx]}"
else
log "Active window: $active_hex monitor=UNKNOWN"
fi
local target_dec=""
if xprop -root _NET_CLIENT_LIST_STACKING >/dev/null 2>&1; then
target_dec="$(pick_target_under_active_same_monitor "$active_hex" "$active_mon_idx" || true)"
else
log "No _NET_CLIENT_LIST_STACKING available; will only lower."
fi
log "Lowering active window: $active_hex"
xdo lower "$active_dec"
if [[ -n "$target_dec" ]]; then
log "Activating target window: $target_dec"
xdotool windowactivate --sync "$target_dec"
else
log "No eligible target found; focus unchanged."
fi
}
main "$@"
@svandragt
Copy link
Author

svandragt commented Jan 18, 2026

A robust X11 utility script that lowers the currently active window and automatically transfers focus to the next eligible window underneath it on the same physical monitor. The script uses the real X11 restack operation (xdo lower) to ensure correct stacking order, derives monitor boundaries from xrandr, and determines window-to-monitor assignment via window geometry rather than unreliable screen indices. It filters out non-interactive windows (docks, panels, desktops), guarantees repeatable cycling behavior, and includes optional debug logging to make all window-selection decisions transparent. Designed for multi-monitor setups and keyboard-driven workflows on X11-based Linux desktops. It's also workspace aware. It avoids switching to hidden windows.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment