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)
// ─────────────────────────────────────────────
.eww-bar {
margin: 8px 10px 0 10px;
margin: 0 0 0 0;
}
// ─────────────────────────────────────────────
// Bar root
// ─────────────────────────────────────────────
.bar-left{
background: $bg;
border-radius: 8px;
background: transparent;
padding: 0 10px;
min-height: 24px;
}
@@ -52,18 +51,15 @@ $red: #f38ba8;
// ─────────────────────────────────────────────
// Workspaces
// ─────────────────────────────────────────────
.workspaces {
padding: 0 2px;
}
.workspace-btn {
background: transparent;
border-radius: 5px;
min-width: 32px;
padding: 2px 8px;
background: rgba(0, 0, 0, 0.35);
border-radius: 8px;
color: $subtext;
font-weight: bold;
font-size: 11px;
padding: 2px 7px;
margin: 2px 0px;
transition: all 200ms ease;
&:hover {
@@ -72,7 +68,7 @@ $red: #f38ba8;
}
&.active {
background: rgba(23, 142, 21, 0.778);
background: rgba(90, 148, 22, 0.85);
color: $accent;
}
@@ -82,12 +78,16 @@ $red: #f38ba8;
}
}
// ─────────────────────────────────────────────
// Window title
// ─────────────────────────────────────────────
.window-title {
padding-left: 4px;
padding: 0px 8px;
color: $subtext;
background: $bg;
border-radius: 8px;
.window-icon {
font-size: 11px;
@@ -100,6 +100,7 @@ $red: #f38ba8;
}
}
// ─────────────────────────────────────────────
// 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
:initial "[]"
"bash ~/.config/eww/scripts/get-workspaces.sh DP-2")
"python3 ~/.config/eww/scripts/get-workspaces.py DP-2")
(deflisten workspaces-hdmi
: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
:initial ""
@@ -50,43 +50,53 @@
;;; Helper Widgets
;;; ─────────────────────────────────────────────
(defwidget workspace-btn [id label active urgent output]
(button
:class {active ? "workspace-btn active" : urgent ? "workspace-btn urgent" : "workspace-btn"}
:onclick "hyprctl dispatch workspace ${id}"
:width 32
label))
(defwidget workspace-btn [id label active urgent output icons pxwidth]
(box
:orientation "h"
:width pxwidth
(button
: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]
(box
:class "workspaces"
:class "workspaces"
:orientation "h"
:spacing 2
:spacing 2
(for ws in workspaces
(workspace-btn
:id {ws.id}
:label {ws.label}
:active {ws.active}
:urgent {ws.urgent}
:output {ws.output}))))
:id {ws.id}
:label {ws.label}
:active {ws.active}
:urgent {ws.urgent}
:output {ws.output}
:icons {ws.icons}
:pxwidth {ws.pxwidth}))))
(defwidget window-widget [title]
(box
:class "window-title"
:orientation "h"
:space-evenly false
(label
:class "window-text"
:text {title}
:limit-width 20)))
;;; add this with your variables
(defvar clock-show-date false)
;;; replace your clock-widget
(defwidget clock-widget []
(box
(button
:class "clock"
:orientation "h"
:spacing 2
(label :text clock-time :class "clock-time")
))
:onclick "eww update clock-show-date=${clock-show-date == 'true' ? 'false' : 'true'}"
(label
:class "clock-time"
:text {clock-show-date == "true" ? clock-date : clock-time})))
(defwidget cpu-widget []
(box
@@ -155,17 +165,15 @@
(defwidget bar-dp2 []
(centerbox
:orientation "h"
(box :class "bar-left" :orientation "h" :spacing 2 :space-evenly false :halign "start"
(workspaces-widget :workspaces workspaces-dp2)
(window-widget :title active-window-dp2))
(box :class "bar-left" :orientation "h" :spacing 8 :space-evenly false :halign "start"
(workspaces-widget :workspaces workspaces-dp2))
(box :class "bar-center" :orientation "h" :spacing 12 :space-evenly false
(logout-btn)
(clock-widget)
(settings-btn))
(box :class "bar-right" :orientation "h" :spacing 6 :space-evenly false :halign "end"
(theme-widget)
(volume-widget :on-click "bash ~/.config/eww/scripts/toggle-mixer.sh")
(network-widget)
)
(box :class "bar-right" :orientation "h" :spacing 8 :space-evenly false :halign "end"
(settings-menu)
(logout-menu)
(cpu-widget)
(mem-widget))))
@@ -174,7 +182,7 @@
:geometry (geometry
:x "10px"
:y "8px"
:width "1900px"
:width "1920px"
:height "24px"
:anchor "top center")
:exclusive true
@@ -189,17 +197,13 @@
(defwidget bar-hdmi []
(centerbox
:orientation "h"
(box :class "bar-left" :orientation "h" :spacing 8 :space-evenly false :max-width 300
(workspaces-widget :workspaces workspaces-hdmi)
(window-widget :title active-window-hdmi))
(box :class "bar-left" :orientation "h" :spacing 8 :space-evenly false
(workspaces-widget :workspaces workspaces-hdmi))
(box :class "bar-center" :orientation "h" :spacing 12 :space-evenly false
(logout-btn)
(clock-widget)
(settings-btn))
(box :class "bar-right" :orientation "h" :spacing 6 :space-evenly false :halign "end"
(theme-widget)
(volume-widget :on-click "pavucontrol")
(network-widget)
(clock-widget))
(box :class "bar-right" :orientation "h" :spacing 2 :space-evenly false :halign "end"
(settings-btn)
(cpu-widget)
(mem-widget))))
@@ -208,10 +212,100 @@
:geometry (geometry
:x "10px"
:y "8px"
:width "1900px"
:width "1920px"
:height "24px"
:anchor "top center")
:exclusive true
:layer "top"
:namespace "eww-bar"
(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)
fi
emit() {
local active urgent result
active=$(hyprctl activeworkspace -j 2>/dev/null | jq -r '.id')
urgent=$(hyprctl clients -j 2>/dev/null \
| jq -r '[.[] | select(.urgent==true) | .workspace.id] | unique | .[]')
result="["
for id in "${IDS[@]}"; do
local is_active="false"
local is_urgent="false"
[[ "$id" == "$active" ]] && is_active="true"
grep -qx "$id" <<< "$urgent" && is_urgent="true"
result+="{\"id\":$id,\"label\":\"$id\",\"active\":$is_active,\"urgent\":$is_urgent,\"output\":\"$OUTPUT\"},"
done
printf '%s\n' "${result%,}]"
# find the socket dynamically instead of relying on env var
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 active urgent_ids json
active=$(hyprctl activeworkspace -j 2>/dev/null | jq -r '.id')
urgent_ids=$(hyprctl clients -j 2>/dev/null \
| jq -r '[.[] | select(.urgent==true) | .workspace.id] | unique | .[]')
json=$(for id in "${IDS[@]}"; do
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
# listen for events and re-emit
socat -u "UNIX-CONNECT:/tmp/hypr/${HYPRLAND_INSTANCE_SIGNATURE}/.socket2.sock" - \
SOCKET=$(get_socket)
echo "Using socket: $SOCKET" >&2
socat -u "UNIX-CONNECT:$SOCKET" - \
| stdbuf -oL grep -E "^(workspace|focusedmon|activewindow|urgent|createworkspace|destroyworkspace)>" \
| while IFS= read -r _; do
sleep 0.05

View File

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

View File

@@ -317,7 +317,7 @@ windowrule {
# env stuff
#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 = ~/.config/hypr/theme-cycle.sh