get app icons for each workspace

This commit is contained in:
samantha42
2026-04-23 05:43:05 +02:00
parent 52fa42698f
commit f7d6f07e3a
9 changed files with 334 additions and 106 deletions

View File

@@ -26,15 +26,14 @@ $red: #f38ba8;
// Window margin (replaces calc() in geometry) // Window margin (replaces calc() in geometry)
// ───────────────────────────────────────────── // ─────────────────────────────────────────────
.eww-bar { .eww-bar {
margin: 8px 10px 0 10px; margin: 0 0 0 0;
} }
// ───────────────────────────────────────────── // ─────────────────────────────────────────────
// Bar root // Bar root
// ───────────────────────────────────────────── // ─────────────────────────────────────────────
.bar-left{ .bar-left{
background: $bg; background: transparent;
border-radius: 8px;
padding: 0 10px; padding: 0 10px;
min-height: 24px; min-height: 24px;
} }
@@ -52,18 +51,15 @@ $red: #f38ba8;
// ───────────────────────────────────────────── // ─────────────────────────────────────────────
// Workspaces // Workspaces
// ───────────────────────────────────────────── // ─────────────────────────────────────────────
.workspaces {
padding: 0 2px;
}
.workspace-btn { .workspace-btn {
background: transparent; min-width: 32px;
border-radius: 5px; padding: 2px 8px;
background: rgba(0, 0, 0, 0.35);
border-radius: 8px;
color: $subtext; color: $subtext;
font-weight: bold; font-weight: bold;
font-size: 11px; font-size: 11px;
padding: 2px 7px;
margin: 2px 0px;
transition: all 200ms ease; transition: all 200ms ease;
&:hover { &:hover {
@@ -72,7 +68,7 @@ $red: #f38ba8;
} }
&.active { &.active {
background: rgba(23, 142, 21, 0.778); background: rgba(90, 148, 22, 0.85);
color: $accent; color: $accent;
} }
@@ -82,12 +78,16 @@ $red: #f38ba8;
} }
} }
// ───────────────────────────────────────────── // ─────────────────────────────────────────────
// Window title // Window title
// ───────────────────────────────────────────── // ─────────────────────────────────────────────
.window-title { .window-title {
padding-left: 4px; padding: 0px 8px;
color: $subtext; color: $subtext;
background: $bg;
border-radius: 8px;
.window-icon { .window-icon {
font-size: 11px; font-size: 11px;
@@ -100,6 +100,7 @@ $red: #f38ba8;
} }
} }
// ───────────────────────────────────────────── // ─────────────────────────────────────────────
// Clock // Clock
// ───────────────────────────────────────────── // ─────────────────────────────────────────────
@@ -184,3 +185,6 @@ $red: #f38ba8;
} }
} }
.center-btn.active {
background: rgba(90, 148, 22, 0.85);
}

View File

@@ -4,11 +4,11 @@
(deflisten workspaces-dp2 (deflisten workspaces-dp2
:initial "[]" :initial "[]"
"bash ~/.config/eww/scripts/get-workspaces.sh DP-2") "python3 ~/.config/eww/scripts/get-workspaces.py DP-2")
(deflisten workspaces-hdmi (deflisten workspaces-hdmi
:initial "[]" :initial "[]"
"bash ~/.config/eww/scripts/get-workspaces.sh HDMI-A-1") "python3 ~/.config/eww/scripts/get-workspaces.py HDMI-A-1")
(deflisten active-window-dp2 (deflisten active-window-dp2
:initial "" :initial ""
@@ -50,43 +50,53 @@
;;; Helper Widgets ;;; Helper Widgets
;;; ───────────────────────────────────────────── ;;; ─────────────────────────────────────────────
(defwidget workspace-btn [id label active urgent output] (defwidget workspace-btn [id label active urgent output icons pxwidth]
(button (box
:class {active ? "workspace-btn active" : urgent ? "workspace-btn urgent" : "workspace-btn"} :orientation "h"
:onclick "hyprctl dispatch workspace ${id}" :width pxwidth
:width 32 (button
label)) :class {active ? "workspace-btn active" : urgent ? "workspace-btn urgent" : "workspace-btn"}
:onclick "hyprctl dispatch workspace ${id}"
:hexpand true
(box
:orientation "h"
:spacing 3
:halign "center"
:valign "center"
(label :text label :class "ws-label")
(for icon in icons
(image
:path icon
:image-width 16
:image-height 16))))))
(defwidget workspaces-widget [workspaces] (defwidget workspaces-widget [workspaces]
(box (box
:class "workspaces" :class "workspaces"
:orientation "h" :orientation "h"
:spacing 2 :spacing 2
(for ws in workspaces (for ws in workspaces
(workspace-btn (workspace-btn
:id {ws.id} :id {ws.id}
:label {ws.label} :label {ws.label}
:active {ws.active} :active {ws.active}
:urgent {ws.urgent} :urgent {ws.urgent}
:output {ws.output})))) :output {ws.output}
:icons {ws.icons}
:pxwidth {ws.pxwidth}))))
(defwidget window-widget [title] ;;; add this with your variables
(box (defvar clock-show-date false)
:class "window-title"
:orientation "h"
:space-evenly false
(label
:class "window-text"
:text {title}
:limit-width 20)))
;;; replace your clock-widget
(defwidget clock-widget [] (defwidget clock-widget []
(box (button
:class "clock" :class "clock"
:orientation "h" :onclick "eww update clock-show-date=${clock-show-date == 'true' ? 'false' : 'true'}"
:spacing 2 (label
(label :text clock-time :class "clock-time") :class "clock-time"
)) :text {clock-show-date == "true" ? clock-date : clock-time})))
(defwidget cpu-widget [] (defwidget cpu-widget []
(box (box
@@ -155,17 +165,15 @@
(defwidget bar-dp2 [] (defwidget bar-dp2 []
(centerbox (centerbox
:orientation "h" :orientation "h"
(box :class "bar-left" :orientation "h" :spacing 2 :space-evenly false :halign "start" (box :class "bar-left" :orientation "h" :spacing 8 :space-evenly false :halign "start"
(workspaces-widget :workspaces workspaces-dp2) (workspaces-widget :workspaces workspaces-dp2))
(window-widget :title active-window-dp2))
(box :class "bar-center" :orientation "h" :spacing 12 :space-evenly false (box :class "bar-center" :orientation "h" :spacing 12 :space-evenly false
(logout-btn)
(clock-widget) (clock-widget)
(settings-btn)) )
(box :class "bar-right" :orientation "h" :spacing 6 :space-evenly false :halign "end" (box :class "bar-right" :orientation "h" :spacing 8 :space-evenly false :halign "end"
(theme-widget) (settings-menu)
(volume-widget :on-click "bash ~/.config/eww/scripts/toggle-mixer.sh") (logout-menu)
(network-widget)
(cpu-widget) (cpu-widget)
(mem-widget)))) (mem-widget))))
@@ -174,7 +182,7 @@
:geometry (geometry :geometry (geometry
:x "10px" :x "10px"
:y "8px" :y "8px"
:width "1900px" :width "1920px"
:height "24px" :height "24px"
:anchor "top center") :anchor "top center")
:exclusive true :exclusive true
@@ -189,17 +197,13 @@
(defwidget bar-hdmi [] (defwidget bar-hdmi []
(centerbox (centerbox
:orientation "h" :orientation "h"
(box :class "bar-left" :orientation "h" :spacing 8 :space-evenly false :max-width 300 (box :class "bar-left" :orientation "h" :spacing 8 :space-evenly false
(workspaces-widget :workspaces workspaces-hdmi) (workspaces-widget :workspaces workspaces-hdmi))
(window-widget :title active-window-hdmi))
(box :class "bar-center" :orientation "h" :spacing 12 :space-evenly false (box :class "bar-center" :orientation "h" :spacing 12 :space-evenly false
(logout-btn) (logout-btn)
(clock-widget) (clock-widget))
(settings-btn)) (box :class "bar-right" :orientation "h" :spacing 2 :space-evenly false :halign "end"
(box :class "bar-right" :orientation "h" :spacing 6 :space-evenly false :halign "end" (settings-btn)
(theme-widget)
(volume-widget :on-click "pavucontrol")
(network-widget)
(cpu-widget) (cpu-widget)
(mem-widget)))) (mem-widget))))
@@ -208,10 +212,100 @@
:geometry (geometry :geometry (geometry
:x "10px" :x "10px"
:y "8px" :y "8px"
:width "1900px" :width "1920px"
:height "24px" :height "24px"
:anchor "top center") :anchor "top center")
:exclusive true :exclusive true
:layer "top" :layer "top"
:namespace "eww-bar" :namespace "eww-bar"
(bar-hdmi)) (bar-hdmi))
(defvar logout-menu-open false)
(defvar settings-menu-open false)
(defwidget logout-menu []
(box
:class "center-menu-wrap"
:orientation "h"
:spacing 4
:space-evenly false
(revealer
:reveal {logout-menu-open == "true"}
:transition "slideright"
:duration "200ms"
(box
:class "center-menu"
:orientation "h"
:spacing 4
:space-evenly false
(button
:class "center-btn"
:tooltip "Lock"
:onclick "eww update logout-menu-open=false && hyprlock"
(label :text "󰌾"))
(button
:class "center-btn"
:tooltip "Reboot"
:onclick "eww update logout-menu-open=false && systemctl reboot"
(label :text "󰜉"))
(button
:class "center-btn"
:tooltip "Shutdown"
:onclick "eww update logout-menu-open=false && systemctl poweroff"
(label :text "󰐥"))
(button
:class "center-btn"
:tooltip "Logout"
:onclick "eww update logout-menu-open=false && hyprctl dispatch exit"
(label :text "󰍃"))))
(button
:class "center-btn"
:onclick "eww update logout-menu-open=${logout-menu-open == 'true' ? 'false' : 'true'} && eww update settings-menu-open=false"
(label :text {logout-menu-open == "true" ? "󰅖" : "󰍃"}))
))
(defwidget settings-menu []
(box
:class "center-menu-wrap"
:orientation "h"
:spacing 4
:space-evenly false
(revealer
:reveal {settings-menu-open == "true"}
:transition "slideright"
:duration "200ms"
(box
:class "center-menu"
:orientation "h"
:spacing 4
:space-evenly false
(theme-widget)
(volume-widget :on-click "bash ~/.config/eww/scripts/toggle-mixer.sh")
(network-widget)
(button
:class "center-btn"
:tooltip "Displays"
:onclick "eww update settings-menu-open=false && wdisplays"
(label :text "󰍹"))
))
(button
:class {settings-menu-open == "true" ? "center-btn active" : "center-btn"}
:onclick "eww update settings-menu-open=${settings-menu-open == 'true' ? 'false' : 'true'} && eww update logout-menu-open=false"
(label :text {settings-menu-open == "true" ? "󰅖" : "󰒓"}))
))

View File

@@ -0,0 +1,33 @@
#!/usr/bin/env bash
OUTPUT="${1:-DP-2}"
get_socket() {
local runtime="${XDG_RUNTIME_DIR:-/run/user/$(id -u)}"
local sig
sig=$(ls "$runtime/hypr/" 2>/dev/null | head -n1)
echo "$runtime/hypr/$sig/.socket2.sock"
}
emit() {
local monitor_ws title
monitor_ws=$(hyprctl monitors -j 2>/dev/null \
| jq -r ".[] | select(.name==\"$OUTPUT\") | .activeWorkspace.id")
title=$(hyprctl clients -j 2>/dev/null \
| jq -r ".[] | select(.workspace.id==$monitor_ws and .focusHistoryID==0) | .title" \
| head -n1)
printf '%s\n' "${title:-}"
}
emit
SOCKET=$(get_socket)
socat -u "UNIX-CONNECT:$SOCKET" - \
| stdbuf -oL grep -E "^(activewindow|focusedmon|workspace|closewindow)>" \
| while IFS= read -r _; do
sleep 0.05
emit
done

View File

@@ -1,24 +0,0 @@
#!/usr/bin/env bash
OUTPUT="${1:-DP-2}"
emit() {
local monitor title
monitor=$(hyprctl monitors -j 2>/dev/null \
| jq -r ".[] | select(.name==\"$OUTPUT\") | .activeWorkspace.id")
title=$(hyprctl clients -j 2>/dev/null \
| jq -r ".[] | select(.workspace.id==$monitor and .focusHistoryID==0) | .title" \
| head -n1)
printf '%s\n' "${title:-}"
}
emit
socat -u "UNIX-CONNECT:/tmp/hypr/${HYPRLAND_INSTANCE_SIGNATURE}/.socket2.sock" - \
| stdbuf -oL grep -E "^(activewindow|focusedmon|workspace|closewindow)>" \
| while IFS= read -r _; do
sleep 0.05
emit
done

View File

@@ -0,0 +1,32 @@
#!/usr/bin/env bash
# get-workspace-icons.sh <workspace_id>
WORKSPACE=${1:-1}
# Get app classes on the workspace
CLASSES=$(hyprctl clients -j | jq -r \
"[.[] | select(.workspace.id == $WORKSPACE) | .class] | unique[]")
for class in $CLASSES; do
echo "=== $class ==="
# Find matching .desktop file (case-insensitive)
DESKTOP=$(find /usr/share/applications ~/.local/share/applications \
-name "*.desktop" 2>/dev/null | \
xargs grep -li "^Name.*=$class\|^Exec.*$class\|^\[Desktop Entry\]" 2>/dev/null | \
head -1)
if [[ -n "$DESKTOP" ]]; then
ICON_NAME=$(grep "^Icon=" "$DESKTOP" | cut -d= -f2)
echo " Icon name: $ICON_NAME"
# Find actual icon file
ICON_FILE=$(find /usr/share/icons ~/.local/share/icons /usr/share/pixmaps \
-name "${ICON_NAME}.*" \( -name "*.png" -o -name "*.svg" \) \
2>/dev/null | sort | tail -1)
echo " Icon file: ${ICON_FILE:-not found}"
else
echo " .desktop file not found"
fi
done

View File

@@ -0,0 +1,79 @@
#!/usr/bin/env python3
import subprocess, json, os, socket
import gi
gi.require_version('Gtk', '3.0')
from gi.repository import Gtk
OUTPUT = None # set via argv, e.g. "DP-2"
def get_icon(cls: str) -> str | None:
theme = Gtk.IconTheme.get_default()
for name in [cls, cls.lower(), cls.lower().rstrip("0123456789")]:
info = theme.lookup_icon(name, 16, 0)
if info:
return info.get_filename()
return None
# Define which workspace IDs belong to each monitor
MONITOR_WORKSPACES = {
"DP-2": [1, 2, 3, 4, 5],
"HDMI-A-1": [6, 7, 8, 9, 10],
}
def build():
clients = json.loads(subprocess.check_output(["hyprctl", "clients", "-j"]))
workspaces = json.loads(subprocess.check_output(["hyprctl", "workspaces", "-j"]))
active = json.loads(subprocess.check_output(["hyprctl", "activeworkspace", "-j"]))
# existing workspace IDs on this monitor (Hyprland only lists non-empty ones)
existing = {w["id"] for w in workspaces if w["monitor"] == OUTPUT}
# all IDs we want to show, including empty ones
all_ids = sorted(set(MONITOR_WORKSPACES.get(OUTPUT, [])) | existing)
# build icon map
amount = {}
icons: dict[int, list[str]] = {}
for c in clients:
wid = c["workspace"]["id"]
cls = c.get("class") or c.get("initialClass", "")
path = get_icon(cls)
if path and path not in icons.get(wid, []):
icons.setdefault(wid, []).append(path)
amount[c["workspace"]["id"]] =+ 1
result = []
for wid in all_ids:
result.append({
"id": wid,
"label": str(wid),
"active": wid == active["id"],
"urgent": any(c.get("urgent") and c["workspace"]["id"] == wid for c in clients),
"output": OUTPUT,
"icons": icons.get(wid, []),
"pxwidth": 32 + len(icons.get(wid, [])) * 20,
})
print(json.dumps(result), flush=True)
def watch():
xdg = os.environ.get("XDG_RUNTIME_DIR", f"/run/user/{os.getuid()}")
sig = os.environ.get("HYPRLAND_INSTANCE_SIGNATURE", "")
sock = f"{xdg}/hypr/{sig}/.socket2.sock"
with socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) as s:
s.connect(sock)
buf = ""
while True:
buf += s.recv(4096).decode()
while "\n" in buf:
line, buf = buf.split("\n", 1)
ev = line.split(">>")[0]
if ev in {"workspace", "openwindow", "closewindow",
"movewindow", "urgent", "focusedmon", "activelayout"}:
build()
if __name__ == "__main__":
import sys
OUTPUT = sys.argv[1] if len(sys.argv) > 1 else None
build()
watch()

View File

@@ -7,30 +7,39 @@ else
IDS=(6 7 8 9 10) IDS=(6 7 8 9 10)
fi fi
emit() { # find the socket dynamically instead of relying on env var
local active urgent result get_socket() {
local runtime="${XDG_RUNTIME_DIR:-/run/user/$(id -u)}"
active=$(hyprctl activeworkspace -j 2>/dev/null | jq -r '.id') local sig
urgent=$(hyprctl clients -j 2>/dev/null \ sig=$(ls "$runtime/hypr/" 2>/dev/null | head -n1)
| jq -r '[.[] | select(.urgent==true) | .workspace.id] | unique | .[]') echo "$runtime/hypr/$sig/.socket2.sock"
}
result="["
for id in "${IDS[@]}"; do emit() {
local is_active="false" local active urgent_ids json
local is_urgent="false"
[[ "$id" == "$active" ]] && is_active="true" active=$(hyprctl activeworkspace -j 2>/dev/null | jq -r '.id')
grep -qx "$id" <<< "$urgent" && is_urgent="true" urgent_ids=$(hyprctl clients -j 2>/dev/null \
result+="{\"id\":$id,\"label\":\"$id\",\"active\":$is_active,\"urgent\":$is_urgent,\"output\":\"$OUTPUT\"}," | jq -r '[.[] | select(.urgent==true) | .workspace.id] | unique | .[]')
done
json=$(for id in "${IDS[@]}"; do
printf '%s\n' "${result%,}]" is_active=false
is_urgent=false
[[ "$id" == "$active" ]] && is_active=true
grep -qx "$id" <<< "$urgent_ids" && is_urgent=true
printf '{"id":%s,"label":"%s","active":%s,"urgent":%s,"output":"%s"}\n' \
"$id" "$id" "$is_active" "$is_urgent" "$OUTPUT"
done | jq -sc '.')
printf '%s\n' "$json"
} }
# emit once on start
emit emit
# listen for events and re-emit SOCKET=$(get_socket)
socat -u "UNIX-CONNECT:/tmp/hypr/${HYPRLAND_INSTANCE_SIGNATURE}/.socket2.sock" - \ echo "Using socket: $SOCKET" >&2
socat -u "UNIX-CONNECT:$SOCKET" - \
| stdbuf -oL grep -E "^(workspace|focusedmon|activewindow|urgent|createworkspace|destroyworkspace)>" \ | stdbuf -oL grep -E "^(workspace|focusedmon|activewindow|urgent|createworkspace|destroyworkspace)>" \
| while IFS= read -r _; do | while IFS= read -r _; do
sleep 0.05 sleep 0.05

View File

@@ -16,6 +16,7 @@ DEPS=(
fastfetch fastfetch
waybar waybar
jq jq
python-gobject # arch
) )
PASS=0 PASS=0

View File

@@ -317,7 +317,7 @@ windowrule {
# env stuff # env stuff
#exec-once = waybar --style ~/.config/waybar/style.css #exec-once = waybar --style ~/.config/waybar/style.css
exec-once = ~/.config/eww/scipts/launch.sh exec-once = bash ~/.config/eww/scripts/launch.sh
exec-once = hyprpaper exec-once = hyprpaper
exec-once = ~/.config/hypr/theme-cycle.sh exec-once = ~/.config/hypr/theme-cycle.sh