diff --git a/eww/eww.scss b/eww/eww.scss index bb11da9..7dad207 100644 --- a/eww/eww.scss +++ b/eww/eww.scss @@ -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); +} \ No newline at end of file diff --git a/eww/eww.yuck b/eww/eww.yuck index ebd7a1d..fa7acf3 100644 --- a/eww/eww.yuck +++ b/eww/eww.yuck @@ -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" ? "󰅖" : "󰒓"})) + + )) \ No newline at end of file diff --git a/eww/scripts/get-active-window.sh b/eww/scripts/get-active-window.sh new file mode 100755 index 0000000..8c4be3b --- /dev/null +++ b/eww/scripts/get-active-window.sh @@ -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 \ No newline at end of file diff --git a/eww/scripts/get-active-workspace.sh b/eww/scripts/get-active-workspace.sh deleted file mode 100644 index 8b2375a..0000000 --- a/eww/scripts/get-active-workspace.sh +++ /dev/null @@ -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 \ No newline at end of file diff --git a/eww/scripts/get-workspace-icons.sh b/eww/scripts/get-workspace-icons.sh new file mode 100644 index 0000000..375b79c --- /dev/null +++ b/eww/scripts/get-workspace-icons.sh @@ -0,0 +1,32 @@ +#!/usr/bin/env bash +# get-workspace-icons.sh + +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 \ No newline at end of file diff --git a/eww/scripts/get-workspaces.py b/eww/scripts/get-workspaces.py new file mode 100644 index 0000000..8d926f9 --- /dev/null +++ b/eww/scripts/get-workspaces.py @@ -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() \ No newline at end of file diff --git a/eww/scripts/get-workspaces.sh b/eww/scripts/get-workspaces.sh index e156774..0d2dc4f 100755 --- a/eww/scripts/get-workspaces.sh +++ b/eww/scripts/get-workspaces.sh @@ -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 diff --git a/hypr/depencies-check.sh b/hypr/depencies-check.sh index dc34559..aaa311e 100644 --- a/hypr/depencies-check.sh +++ b/hypr/depencies-check.sh @@ -16,6 +16,7 @@ DEPS=( fastfetch waybar jq + python-gobject # arch ) PASS=0 diff --git a/hypr/hyprland.conf b/hypr/hyprland.conf index eb6fe9b..8b3aba7 100644 --- a/hypr/hyprland.conf +++ b/hypr/hyprland.conf @@ -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