From 87b03b51f948fbe127b6cdd49a92454b26f47ecd Mon Sep 17 00:00:00 2001 From: Ivan Malison Date: Sat, 7 Feb 2026 17:08:12 -0800 Subject: [PATCH] hyprland: add rofi icons to window go/bring/replace pickers MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add desktop-entry icon lookup to the rofi window picker scripts, matching the XMonad setup's icon support. A shared helper script (window-icon-map.sh) builds a class→icon mapping from .desktop files and each picker uses rofi's dmenu icon protocol (\0icon\x1f). Also replaces the X11-only "rofi -show window" with a native Hyprland window picker using hyprctl clients. Co-Authored-By: Claude Opus 4.6 --- dotfiles/config/hypr/hyprland.conf | 10 +-- dotfiles/config/hypr/scripts/bring-window.sh | 36 ++++++---- dotfiles/config/hypr/scripts/go-to-window.sh | 33 ++++++++++ .../config/hypr/scripts/replace-window.sh | 34 ++++++---- .../config/hypr/scripts/window-icon-map.sh | 66 +++++++++++++++++++ 5 files changed, 149 insertions(+), 30 deletions(-) create mode 100755 dotfiles/config/hypr/scripts/go-to-window.sh create mode 100755 dotfiles/config/hypr/scripts/window-icon-map.sh diff --git a/dotfiles/config/hypr/hyprland.conf b/dotfiles/config/hypr/hyprland.conf index 4f78ac77..5ae4cf8e 100644 --- a/dotfiles/config/hypr/hyprland.conf +++ b/dotfiles/config/hypr/hyprland.conf @@ -17,7 +17,7 @@ monitor=,preferred,auto,1 # ============================================================================= # PROGRAMS # ============================================================================= -$terminal = alacritty +$terminal = ghostty $fileManager = dolphin $menu = rofi -show drun -show-icons $runMenu = rofi -show run @@ -284,7 +284,7 @@ bind = $modAlt, C, exec, ~/.config/hypr/scripts/raise-or-run.sh google-chrome go # Toggle scratchpads bind = $modAlt, E, exec, ~/.config/hypr/scripts/toggle-scratchpad.sh element '^Element$' - element-desktop bind = $modAlt, G, exec, ~/.config/hypr/scripts/toggle-scratchpad.sh gmail '^google-chrome$' '.*@gmail.com.*Gmail.*' google-chrome-stable --new-window https://mail.google.com/mail/u/0/#inbox -bind = $modAlt, H, exec, ~/.config/hypr/scripts/toggle-scratchpad.sh htop '^htop-scratch$' - alacritty --class htop-scratch --title htop -e htop +bind = $modAlt, H, exec, ~/.config/hypr/scripts/toggle-scratchpad.sh htop '^htop-scratch$' - ghostty --class=htop-scratch --title=htop -e htop bind = $modAlt, M, exec, ~/.config/hypr/scripts/toggle-scratchpad.sh messages '^google-chrome$' '^Messages.*' google-chrome-stable --new-window https://messages.google.com/web/conversations bind = $modAlt, K, exec, ~/.config/hypr/scripts/toggle-scratchpad.sh slack '^Slack$' - slack bind = $modAlt, S, exec, ~/.config/hypr/scripts/toggle-scratchpad.sh spotify '^spotify$' - spotify @@ -489,8 +489,8 @@ bind = $mainMod SHIFT, H, exec, ~/.config/hypr/scripts/workspace-move-to-empty.s # WINDOW MANAGEMENT # ----------------------------------------------------------------------------- -# Go to window (rofi window switcher) -bind = $mainMod, G, exec, rofi -show window -show-icons +# Go to window (rofi window switcher with icons) +bind = $mainMod, G, exec, ~/.config/hypr/scripts/go-to-window.sh # Bring window (move to current workspace) bind = $mainMod, B, exec, ~/.config/hypr/scripts/bring-window.sh @@ -576,7 +576,7 @@ exec-once = ~/.config/hypr/scripts/workspace-history.sh # Scratchpad applications (spawn on demand via keybinds) # exec-once = [workspace special:element silent] element-desktop # exec-once = [workspace special:gmail silent] google-chrome-stable --new-window https://mail.google.com/mail/u/0/#inbox -# exec-once = [workspace special:htop silent] alacritty --class htop-scratch --title htop -e htop +# exec-once = [workspace special:htop silent] ghostty --class=htop-scratch --title=htop -e htop # exec-once = [workspace special:messages silent] google-chrome-stable --new-window https://messages.google.com/web/conversations # exec-once = [workspace special:slack silent] slack # exec-once = [workspace special:spotify silent] spotify diff --git a/dotfiles/config/hypr/scripts/bring-window.sh b/dotfiles/config/hypr/scripts/bring-window.sh index 94a3131a..cbaec2cc 100755 --- a/dotfiles/config/hypr/scripts/bring-window.sh +++ b/dotfiles/config/hypr/scripts/bring-window.sh @@ -1,30 +1,40 @@ #!/usr/bin/env bash # Bring window to current workspace (like XMonad's bringWindow) -# Uses rofi to select a window and moves it to the current workspace +# Uses rofi with icons to select a window, then moves it here. set -euo pipefail -# Get current workspace +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +source "$SCRIPT_DIR/window-icon-map.sh" + CURRENT_WS=$(hyprctl activeworkspace -j | jq -r '.id') -# Get all windows and format for rofi -WINDOWS=$(hyprctl clients -j | jq -r '.[] | select(.workspace.id >= 0 and .workspace.id != '"$CURRENT_WS"') | "\(.title) [\(.class)] - WS:\(.workspace.id) |\(.address)"') +# Get windows on OTHER workspaces as TSV +WINDOW_DATA=$(hyprctl clients -j | jq -r --argjson cws "$CURRENT_WS" ' + .[] | select(.workspace.id >= 0 and .workspace.id != $cws) + | [.address, .class, (.title | gsub("\t"; " ")), (.workspace.id | tostring)] + | @tsv') -if [ -z "$WINDOWS" ]; then +if [ -z "$WINDOW_DATA" ]; then notify-send "Bring Window" "No windows on other workspaces" exit 0 fi -# Show rofi menu -SELECTION=$(echo "$WINDOWS" | rofi -dmenu -i -p "Bring window" -format 's') +addresses=() +TMPFILE=$(mktemp) +trap 'rm -f "$TMPFILE"' EXIT -if [ -n "$SELECTION" ]; then - # Extract the window address (after the last |) - ADDRESS=$(echo "$SELECTION" | sed 's/.*|//') +while IFS=$'\t' read -r address class title ws_id; do + icon=$(icon_for_class "$class") + addresses+=("$address") + printf '%-20s %-40s WS:%s\0icon\x1f%s\n' \ + "$class" "${title:0:40}" "$ws_id" "$icon" +done <<< "$WINDOW_DATA" > "$TMPFILE" - # Move window to current workspace +INDEX=$(rofi -dmenu -i -show-icons -p "Bring window" -format i < "$TMPFILE") || exit 0 + +if [ -n "$INDEX" ] && [ -n "${addresses[$INDEX]:-}" ]; then + ADDRESS="${addresses[$INDEX]}" hyprctl dispatch movetoworkspace "$CURRENT_WS,address:$ADDRESS" - - # Focus the window hyprctl dispatch focuswindow "address:$ADDRESS" fi diff --git a/dotfiles/config/hypr/scripts/go-to-window.sh b/dotfiles/config/hypr/scripts/go-to-window.sh new file mode 100755 index 00000000..6e404dd0 --- /dev/null +++ b/dotfiles/config/hypr/scripts/go-to-window.sh @@ -0,0 +1,33 @@ +#!/usr/bin/env bash +# Go to a window selected via rofi (with icons from desktop entries). +# Replaces "rofi -show window" which doesn't work well on Wayland. + +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +source "$SCRIPT_DIR/window-icon-map.sh" + +# Get all windows on regular workspaces as TSV +WINDOW_DATA=$(hyprctl clients -j | jq -r ' + .[] | select(.workspace.id >= 0) + | [.address, .class, (.title | gsub("\t"; " ")), (.workspace.id | tostring)] + | @tsv') + +[ -n "$WINDOW_DATA" ] || exit 0 + +addresses=() +TMPFILE=$(mktemp) +trap 'rm -f "$TMPFILE"' EXIT + +while IFS=$'\t' read -r address class title ws_id; do + icon=$(icon_for_class "$class") + addresses+=("$address") + printf '%-20s %-40s WS:%s\0icon\x1f%s\n' \ + "$class" "${title:0:40}" "$ws_id" "$icon" +done <<< "$WINDOW_DATA" > "$TMPFILE" + +INDEX=$(rofi -dmenu -i -show-icons -p "Go to window" -format i < "$TMPFILE") || exit 0 + +if [ -n "$INDEX" ] && [ -n "${addresses[$INDEX]:-}" ]; then + hyprctl dispatch focuswindow "address:${addresses[$INDEX]}" +fi diff --git a/dotfiles/config/hypr/scripts/replace-window.sh b/dotfiles/config/hypr/scripts/replace-window.sh index 5f16c916..e9a062fa 100755 --- a/dotfiles/config/hypr/scripts/replace-window.sh +++ b/dotfiles/config/hypr/scripts/replace-window.sh @@ -4,8 +4,9 @@ set -euo pipefail -# Get current workspace and focused window -CURRENT_WS=$(hyprctl activeworkspace -j | jq -r '.id') +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +source "$SCRIPT_DIR/window-icon-map.sh" + FOCUSED=$(hyprctl activewindow -j | jq -r '.address') if [ "$FOCUSED" = "null" ] || [ -z "$FOCUSED" ]; then @@ -13,21 +14,30 @@ if [ "$FOCUSED" = "null" ] || [ -z "$FOCUSED" ]; then exit 0 fi -# Get all windows except focused -WINDOWS=$(hyprctl clients -j | jq -r ".[] | select(.workspace.id >= 0 and .address != \"$FOCUSED\") | \"\(.title) [\(.class)] - WS:\(.workspace.id) |\(.address)\"") +# Get all windows except focused as TSV +WINDOW_DATA=$(hyprctl clients -j | jq -r --arg focused "$FOCUSED" ' + .[] | select(.workspace.id >= 0 and .address != $focused) + | [.address, .class, (.title | gsub("\t"; " ")), (.workspace.id | tostring)] + | @tsv') -if [ -z "$WINDOWS" ]; then +if [ -z "$WINDOW_DATA" ]; then notify-send "Replace Window" "No other windows available" exit 0 fi -# Show rofi menu -SELECTION=$(echo "$WINDOWS" | rofi -dmenu -i -p "Replace with" -format 's') +addresses=() +TMPFILE=$(mktemp) +trap 'rm -f "$TMPFILE"' EXIT -if [ -n "$SELECTION" ]; then - # Extract the window address - ADDRESS=$(echo "$SELECTION" | sed 's/.*|//') +while IFS=$'\t' read -r address class title ws_id; do + icon=$(icon_for_class "$class") + addresses+=("$address") + printf '%-20s %-40s WS:%s\0icon\x1f%s\n' \ + "$class" "${title:0:40}" "$ws_id" "$icon" +done <<< "$WINDOW_DATA" > "$TMPFILE" - # Swap windows using hy3 - hyprctl dispatch hy3:movewindow "address:$ADDRESS" +INDEX=$(rofi -dmenu -i -show-icons -p "Replace with" -format i < "$TMPFILE") || exit 0 + +if [ -n "$INDEX" ] && [ -n "${addresses[$INDEX]:-}" ]; then + hyprctl dispatch hy3:movewindow "address:${addresses[$INDEX]}" fi diff --git a/dotfiles/config/hypr/scripts/window-icon-map.sh b/dotfiles/config/hypr/scripts/window-icon-map.sh new file mode 100755 index 00000000..8c8714e1 --- /dev/null +++ b/dotfiles/config/hypr/scripts/window-icon-map.sh @@ -0,0 +1,66 @@ +#!/usr/bin/env bash +# Source this file to get icon_for_class function. +# Builds a mapping from window class → freedesktop icon name +# by scanning .desktop files for StartupWMClass and Icon fields. +# +# Usage: +# source "$(dirname "$0")/window-icon-map.sh" +# icon=$(icon_for_class "google-chrome") + +declare -A _WINDOW_ICON_MAP + +_build_window_icon_map() { + local IFS=':' + local -a search_dirs=() + local dir + + for dir in ${XDG_DATA_DIRS:-/run/current-system/sw/share:/usr/share:/usr/local/share}; do + [ -d "$dir/applications" ] && search_dirs+=("$dir/applications") + done + [ -d "$HOME/.local/share/applications" ] && search_dirs+=("$HOME/.local/share/applications") + [ ${#search_dirs[@]} -eq 0 ] && return + + # Expand globs per-directory so the pattern works correctly + local -a desktop_files=() + for dir in "${search_dirs[@]}"; do + desktop_files+=("$dir"/*.desktop) + done + [ ${#desktop_files[@]} -eq 0 ] && return + + # Single grep pass across all desktop files + local -A file_icons file_wmclass + local filepath line + while IFS=: read -r filepath line; do + case "$line" in + Icon=*) + [ -z "${file_icons[$filepath]:-}" ] && file_icons["$filepath"]="${line#Icon=}" + ;; + StartupWMClass=*) + [ -z "${file_wmclass[$filepath]:-}" ] && file_wmclass["$filepath"]="${line#StartupWMClass=}" + ;; + esac + done < <(grep -H '^Icon=\|^StartupWMClass=' "${desktop_files[@]}" 2>/dev/null) + + # Build class → icon map + local icon wm_class bn name + for filepath in "${!file_icons[@]}"; do + icon="${file_icons[$filepath]}" + [ -n "$icon" ] || continue + + wm_class="${file_wmclass[$filepath]:-}" + if [ -n "$wm_class" ]; then + _WINDOW_ICON_MAP["${wm_class,,}"]="$icon" + fi + + bn="${filepath##*/}" + name="${bn%.desktop}" + _WINDOW_ICON_MAP["${name,,}"]="$icon" + done +} + +_build_window_icon_map + +icon_for_class() { + local class_lower="${1,,}" + echo "${_WINDOW_ICON_MAP[$class_lower]:-$class_lower}" +}