#!/usr/bin/env bash set -euo pipefail # Usage: # Waybar exec: hyprscroll-overflow.sh # Waybar on-click: hyprscroll-overflow.sh --pick # # Env: # COLUMN_WIDTH=0.5 # ICON="󰓒" # DMENU_CMD="walker --dmenu" (or: "wofi --dmenu", "rofi -dmenu", etc.) COLUMN_WIDTH="${COLUMN_WIDTH:-0.5}" ICON="${ICON:-󰓒}" DMENU_CMD="${DMENU_CMD:-walker --dmenu}" json_escape() { # minimal JSON string escape (quotes, backslashes, newlines, tabs, CR) local s="${1:-}" s="${s//\\/\\\\}" s="${s//\"/\\\"}" s="${s//$'\n'/\\n}" s="${s//$'\r'/\\r}" s="${s//$'\t'/\\t}" printf '%s' "$s" } fail_json() { # NOTE: errors SHOULD still show (so you notice), hence JSON output here. local msg="hyprscroll-overflow: ${1:-unknown error}" printf '{"text":"%s !","tooltip":"%s","class":"error"}\n' \ "$(json_escape "$ICON")" "$(json_escape "$msg")" exit 0 } need() { command -v "$1" >/dev/null 2>&1 || fail_json "$1 not in PATH"; } need hyprctl need jq need awk read -r focused_mon_id focused_ws_id < <( hyprctl -j monitors 2>/dev/null | jq -r ' .[] | select(.focused==true) | "\(.id) \(.activeWorkspace.id)" ' ) || fail_json "failed to read focused monitor/workspace" [[ -n "${focused_mon_id:-}" && -n "${focused_ws_id:-}" ]] || fail_json "no focused monitor/workspace" # Current layout (needed for both normal + --pick paths) layout="$(hyprctl getoption general:layout 2>/dev/null | awk '/str:/ {print $2; exit}' || true)" layout="${layout:-}" # Collect windows (current ws + current monitor, mapped only) clients_json="$(hyprctl -j clients 2>/dev/null)" || fail_json "failed to read clients" # Click action: pick a window and focus it if [[ "${1:-}" == "--pick" ]]; then # Build menu lines: address at end so we can parse it back reliably. menu="$( jq -r --argjson ws "$focused_ws_id" --argjson mid "$focused_mon_id" ' [ .[] | select(.mapped == true) | select(.workspace.id == $ws) | select(.monitor == $mid) | {address, class, title} ] | map("[\(.class)] \(.title) \(.address)") | .[] ' <<<"$clients_json" )" || exit 0 [[ -n "${menu:-}" ]] || exit 0 # shellcheck disable=SC2086 choice="$(printf '%s\n' "$menu" | eval "$DMENU_CMD" || true)" [[ -n "${choice:-}" ]] || exit 0 addr="$(awk '{print $NF}' <<<"$choice")" [[ "$addr" =~ ^0x[0-9a-fA-F]+$ ]] || exit 0 hyprctl dispatch focuswindow "address:${addr}" >/dev/null 2>&1 || exit 0 exit 0 fi # Tooltip list (multiline) # Include a stable selector: address (hex), plus class + title for humans. tooltip_list="$( jq -r --argjson ws "$focused_ws_id" --argjson mid "$focused_mon_id" ' [ .[] | select(.mapped == true) | select(.workspace.id == $ws) | select(.monitor == $mid) | {address, class, title} ] | to_entries | map("\(.key+1). [\(.value.class)] \(.value.title) (\(.value.address))") | .[] ' <<<"$clients_json" )" || fail_json "failed to build tooltip list" win_count="$( jq -r --argjson ws "$focused_ws_id" --argjson mid "$focused_mon_id" ' [ .[] | select(.mapped == true) | select(.workspace.id == $ws) | select(.monitor == $mid) ] | length ' <<<"$clients_json" )" || fail_json "failed to count clients" max_visible="$(awk -v w="$COLUMN_WIDTH" 'BEGIN{ if (w<=0) {print 1} else {print int(1.0/w)} }')" \ || fail_json "awk failed" (( max_visible < 1 )) && max_visible=1 overflow=$(( win_count - max_visible )) # IMPORTANT: hide module by outputting NOTHING (no JSON) when not relevant if (( overflow <= 0 )) || [[ "$layout" != "scrolling" ]]; then exit 0 fi text="$ICON +$overflow" cls="overflow" tooltip="WS ${focused_ws_id} • ${win_count} window(s)\n" tooltip+="Approx ${max_visible} fit (column_width=${COLUMN_WIDTH})\n" tooltip+="------------------------------\n" tooltip+="${tooltip_list:-"(no windows)"}" printf '{"text":"%s","tooltip":"%s","class":"%s"}\n' \ "$(json_escape "$text")" \ "$(json_escape "$tooltip")" \ "$(json_escape "$cls")"