From 3169f5a670035b7a24bb010451feaef83bdcb08d Mon Sep 17 00:00:00 2001 From: pyrschtjag Date: Fri, 3 Apr 2026 12:31:56 +0400 Subject: [PATCH] feat: add initial eww config --- eww/eww.scss | 248 ++++++++++++++++++++++++++++++++ eww/eww.yuck | 171 ++++++++++++++++++++++ eww/scripts/kb_leds.py | 60 ++++++++ eww/scripts/tray_monitor.py | 46 ++++++ eww/scripts/volume.py | 42 ++++++ eww/scripts/workspaces.py | 213 +++++++++++++++++++++++++++ eww/scripts/workspaces_map.json | 13 ++ 7 files changed, 793 insertions(+) create mode 100644 eww/eww.scss create mode 100644 eww/eww.yuck create mode 100755 eww/scripts/kb_leds.py create mode 100755 eww/scripts/tray_monitor.py create mode 100755 eww/scripts/volume.py create mode 100755 eww/scripts/workspaces.py create mode 100644 eww/scripts/workspaces_map.json diff --git a/eww/eww.scss b/eww/eww.scss new file mode 100644 index 0000000..8fe5ce3 --- /dev/null +++ b/eww/eww.scss @@ -0,0 +1,248 @@ +// ============== +// переменные +// ============== + +// цветовая палитра +$surface0: #363a4f; +$text: #cad3f5; +$lavender: #a8afe1; +$red: #ed8796; +$yellow: #f9e2af; +$blue: #89b4fa; +$pink: #e3a1d2; +$muted-gray: #666666; +$bg-dark: #1e2030; + +// геометрия +$r: 12px; // радиус скругления +$h: 30px; // высота элементов +$win-margin: 6px; // отступ бара от краев экрана + +// капсулы +$capsule-margin-x: 8px; +$capsule-padding-x: 12px; +$border-idle: 0.3px; +$border-active: 1px; + +// тайминги +$t-fast: 300ms; +$t-norm: 400ms; +$t-slow: 600ms; +$ease-oil: cubic-bezier(0.16, 1, 0.3, 1); + +// ================= +// базовые стили +// ================= + +* { + font-family: "Inter", "FiraCode Nerd Font"; + font-size: 14px; + all: unset; // сбрасываем дефолтные стили gtk +} + +window { + background-color: transparent; +} + +.bar-inner { + margin: $win-margin; + padding: 0; + + // сброс отступов для крайних элементов + > box:first-child .capsule:first-child { margin-left: 0; } + > box:last-child .capsule:last-child { margin-right: 0; } +} + +// общий стиль для всех таблеток на баре +.capsule { + background-color: rgba($surface0, 0.7); + color: $text; + margin: 0 $capsule-margin-x; + padding: 0 $capsule-padding-x; + border-radius: $r; + min-height: $h; + border: $border-idle solid $lavender; + transition: all $t-fast ease-in-out; + + &.active { + background-color: rgba($surface0, 0.9); + border: $border-active solid $lavender; + transition: all $t-slow ease-in-out; + } +} + +// ========================= +// меню/поповеры/тутлипы +// ========================= + +menu, +.menu, +popover, +tooltip { + background-color: rgba($bg-dark, 0.9); + border: 1px solid rgba($lavender, 0.5); + border-radius: $r; + padding: 8px; + box-shadow: 0 4px 15px rgba(0, 0, 0, 0.4); + color: $text; +} + +menuitem, +.menu-item { + padding: 6px 14px; + border-radius: $r - 4px; + color: $text; + background-color: transparent; + transition: all 0.2s ease; + + &:hover, + &:focus { + background-color: rgba($lavender, 0.2); + color: $lavender; + } +} + +separator { + background-color: rgba($lavender, 0.15); + margin: 4px 8px; + min-height: 1px; +} + +// =========== +// виджеты +// =========== + +// воркспейсы +.workspaces-rail { + @extend .capsule; + padding: 0; + transition: all $t-norm $ease-oil; +} + +.ws-pill { + color: $text; + background-color: transparent; + border-radius: $r - 2px; + min-height: $h; + padding: 0 4px; + transition: background-color $t-norm $ease-oil, color 0.3s ease; + + &.active { background-color: rgba($lavender, 0.2); } + &.focused { background-color: rgba($lavender, 0.8); color: $surface0; } + &.urgent { animation: pulse-lavender 4s infinite ease-in-out; } +} + +.ws-id { margin-left: 4px; } +.ws-icons-box { margin: 0 8px 0 4px; } + +// раскладка +.kb-pill { + @extend .capsule; + margin-right: 0; + + .kb-label { + transition: color $t-norm ease-in-out; + } + + &.caps-on { + border-color: $yellow; + box-shadow: inset 0 0 0 1px rgba($yellow, 0.3); + .kb-label { color: $yellow; } + } + + &.num-on { + border-color: $blue; + box-shadow: inset 0 0 0 1px rgba($blue, 0.3); + .kb-label { color: $blue; } + } + + &.caps-on.num-on { + border-color: $pink; + .kb-label { color: $pink; } + } +} + +// статистика +.stats-pill { + padding: 0 6px; + &.alert { + border-color: $red; + border-width: 0.75px; + .slide-wrapper { border-color: $red; } + } +} + +.stat-group { + .icon.alert, + .stat-label.alert { color: $red; } +} + +// управление звуком +.volume-pill { + @extend .capsule; + + .icon { margin-right: 14px; } + + &.muted { + border-color: #444; + .icon, .stat-label { color: $muted-gray; } + scale slider { background-color: $muted-gray; } + scale trough highlight { background-color: #444; } + } +} + +.vol-bar { min-width: 120px; margin: 0 6px; } + +// трей +.tray-capsule { + @extend .capsule; + padding: 0 10px; + margin-right: 0; + image { margin: 0 2px; opacity: 0.8; } +} + +// часы +.time-pill { + .icon { margin-right: 8px; } +} + +// ================ +// элементы gtk +// ================ + +// ползунки +scale trough { + background-color: $bg-dark; + border-radius: $r; + min-height: 6px; + + highlight { + background-color: $lavender; + border-radius: $r; + } +} + +scale slider { + margin: -6px 0; + background-color: $text; + min-height: 12px; + min-width: 12px; + border-radius: 6px; +} + +// разделитель внутри пилюль +.slide-wrapper { + margin-left: 10px; + padding-left: 10px; + border-left: 0.7px solid $lavender; +} + +// ============ +// анимации +// ============ + +@keyframes pulse-lavender { + 0% { background-color: rgba($lavender, 0); color: $text; } + 50% { background-color: rgba($lavender, 0.2); color: lighten($text, 10%); } + 100% { background-color: rgba($lavender, 0); color: $text; } +} \ No newline at end of file diff --git a/eww/eww.yuck b/eww/eww.yuck new file mode 100644 index 0000000..06c65c8 --- /dev/null +++ b/eww/eww.yuck @@ -0,0 +1,171 @@ +;; ============== +;; переменные +;; ============== + +;; состояния раскрытия для каждого монитора +(defvar stats_reveal_0 false) +(defvar vol_reveal_0 false) +(defvar time_reveal_0 false) + +(defvar stats_reveal_1 false) +(defvar vol_reveal_1 false) +(defvar time_reveal_1 false) + +;; слушатели +(deflisten ws_data :initial "{}" "python3 $HOME/.config/eww/scripts/workspaces.py") +(deflisten volume :initial "0" "python3 $HOME/.config/eww/scripts/volume.py") +(deflisten layout :initial "EN" "socat -u UNIX-CONNECT:$XDG_RUNTIME_DIR/hypr/$HYPRLAND_INSTANCE_SIGNATURE/.socket2.sock - | stdbuf -oL awk -F'>>' '/activelayout/ {split(\$2, a, \",\"); print toupper(substr(a[2], 1, 2))}'") +(deflisten tray_count :initial "0" "python3 $HOME/.config/eww/scripts/tray_monitor.py") +(deflisten kb_leds :initial '{"caps":"0","num":"0"}' "python3 $HOME/.config/eww/scripts/kb_leds.py") + +;; пуллы +(defpoll timestr :interval "1s" "date '+%H:%M'") +(defpoll datestr :interval "1m" "date '+%A, %B %d'") +(defpoll temp :interval "30s" :initial "0" "awk '{print int($1/1000)}' /sys/class/hwmon/hwmon2/temp1_input") +(defpoll gpu_temp :interval "30s" :initial "0" "nvidia-smi --query-gpu=temperature.gpu --format=csv,noheader,nounits") + +;; ============== +;; воркспейсы +;; ============== + +;; слот под воркспейс +(defwidget workspace_slot [data id] + (revealer :reveal {data[id] != "null" && data[id].exists} + :transition "slideleft" + :duration "300ms" + (box :class "ws-pill ${data[id].urgent ? 'urgent' : data[id].focused ? 'focused' : data[id].active ? 'active' : ''}" + :space-evenly false + :valign "center" + (eventbox :onclick "hyprctl dispatch workspace ${id}" + (box :space-evenly false :valign "center" + (label :class "ws-id" :text "${id}") + (revealer :reveal {arraylength(data[id].icons) > 0} + :transition "slideright" + :duration "350ms" + (box :class "ws-icons-box" :space-evenly false :spacing 8 + (for icon in {data[id].icons} + (label :class "ws-icon-item" :text "${icon}"))))))))) + +;; контейнер для 10 воркспейсов +(defwidget workspaces [ws_data] + (box :class "workspaces-rail" :space-evenly false + (for i in "[1, 2, 3, 4, 5, 6, 7, 8, 9, 10]" + (workspace_slot :data ws_data :id i)))) + +;; =========== +;; виджеты +;; =========== + +;; трей +(defwidget tray_dynamic [] + (revealer :reveal {tray_count > 0} + :transition "crossfade" + :duration "400ms" + (box :class "tray-capsule" + (systray :icon-size 16 :spacing 8 :prepend-new true)))) + +;; раскладка +(defwidget kb_widget [] + (box :class "capsule kb-pill ${kb_leds.caps == '1' ? 'caps-on' : ''} ${kb_leds.num == '1' ? 'num-on' : ''}" + :space-evenly false + (label :text {layout ?: "EN"} :class "kb-label"))) + +;; звук +(defwidget volumewidget [reveal_state reveal_var] + (eventbox :cursor "pointer" :onclick "eww update ${reveal_var}=${!reveal_state}" + (box :class "capsule volume-pill ${reveal_state ? 'active' : ''} ${volume == 0 || volume == '' ? 'muted' : ''}" + :space-evenly false + (label :class "icon" :text {volume == 0 || volume == "" ? "" : (volume < 50 ? "" : "")}) + (label :class "stat-label" :text "${volume}%" :width 38) + (revealer :reveal reveal_state :transition "slideleft" + (box :class "slide-wrapper" + (scale :class "vol-bar" :min 0 :max 101 :value {volume ?: 0} + :onchange "pactl set-sink-volume @DEFAULT_SINK@ {}%")))))) + +;; часы +(defwidget timewidget [reveal_state reveal_var] + (eventbox :cursor "pointer" :onclick "eww update ${reveal_var}=${!reveal_state}" + (box :class "capsule time-pill ${reveal_state ? 'active' : ''}" + :space-evenly false + (label :text timestr) + (revealer :reveal reveal_state :transition "slideleft" + (box :class "slide-wrapper" :space-evenly false + (label :class "icon" :text "󰃭") + (label :text datestr)))))) + +;; cтатистика +(defwidget statswidget [reveal_state reveal_var] + (eventbox :cursor "pointer" :onclick "eww update ${reveal_var}=${!reveal_state}" + (box :class "capsule stats-pill ${reveal_state ? 'active' : ''} + ${(EWW_CPU.avg > 90 || EWW_RAM.used_mem_perc > 90 || (temp ?: 0) > 75 || (gpu_temp ?: 0) > 75) ? 'alert' : ''}" + :space-evenly false + (group :icon "" :value "${round(EWW_CPU.avg, 0)}%" :alert {EWW_CPU.avg > 90}) + (group :icon "󰍛" :value "${round(EWW_RAM.used_mem_perc, 0)}%" :alert {EWW_RAM.used_mem_perc > 90}) + (revealer :reveal reveal_state :transition "slideleft" + (box :space-evenly false :class "slide-wrapper" + (group :icon "󱛟" :value "${round(EWW_DISK['/'].free / 1073741824, 1)}GB" :alert {EWW_DISK['/'].free / 1073741824 < 10}) + (group :icon "" :value "${temp}°" :alert {(temp ?: 0) > 75}) + (group :icon "󰢮" :value "${gpu_temp}°" :alert {(gpu_temp ?: 0) > 75})))))) + +;; шаблон для иконки с текстом +(defwidget group [icon value alert] + (box :space-evenly false :class "stat-group" + (label :class "icon ${alert ? 'alert' : ''}" :text icon :width 24) + (label :class "stat-label ${alert ? 'alert' : ''}" + :text value + :width {icon == "󱛟" ? 65 : 40} + :halign "start"))) + +;; =============== +;; сборка бара +;; =============== + +;; основной бар (с треем) +(defwidget main_bar [mon_key t_rev t_var s_rev s_var v_rev v_var] + (centerbox :class "bar-inner" + (box :halign "start" (workspaces :ws_data {ws_data[mon_key]})) + (box :halign "center" (timewidget :reveal_state t_rev :reveal_var t_var)) + (box :halign "end" :space-evenly false + (tray_dynamic) + (kb_widget) + (statswidget :reveal_state s_rev :reveal_var s_var) + (volumewidget :reveal_state v_rev :reveal_var v_var)))) + +;; дополнительный бар (без трея) +(defwidget side_bar [mon_key t_rev t_var s_rev s_var v_rev v_var] + (centerbox :class "bar-inner" + (box :halign "start" (workspaces :ws_data {ws_data[mon_key]})) + (box :halign "center" (timewidget :reveal_state t_rev :reveal_var t_var)) + (box :halign "end" :space-evenly false + (kb_widget) + (statswidget :reveal_state s_rev :reveal_var s_var) + (volumewidget :reveal_state v_rev :reveal_var v_var)))) + +;; ======== +;; окна +;; ======== + +;; окно бара для dvi +(defwindow bar_0 :monitor 0 :exclusive false :stacking "fg" + :geometry (geometry :width "100%" :anchor "top center") + (side_bar :mon_key "DVI_I_1" + :t_rev time_reveal_0 :t_var "time_reveal_0" + :s_rev stats_reveal_0 :s_var "stats_reveal_0" + :v_rev vol_reveal_0 :v_var "vol_reveal_0")) + +;; окно бара для hdmi +(defwindow bar_1 :monitor 1 :exclusive false :stacking "fg" + :geometry (geometry :width "100%" :anchor "top center") + (main_bar :mon_key "HDMI_A_1" + :t_rev time_reveal_1 :t_var "time_reveal_1" + :s_rev stats_reveal_1 :s_var "stats_reveal_1" + :v_rev vol_reveal_1 :v_var "vol_reveal_1")) + +;; обои +(defwindow wallpaper_0 :monitor 0 :stacking "bg" :focusable false + :geometry (geometry :width "100%" :height "100%") + (box :style "background-image: url('/home/pyrschtjag/Pictures/wallhaven-x1lr3l.jpg'); background-size: cover;")) + +(defwindow wallpaper_1 :monitor 1 :stacking "bg" :focusable false + :geometry (geometry :width "100%" :height "100%") + (box :style "background-image: url('/home/pyrschtjag/Pictures/wallhaven-7jj86e.jpg'); background-size: cover;")) \ No newline at end of file diff --git a/eww/scripts/kb_leds.py b/eww/scripts/kb_leds.py new file mode 100755 index 0000000..8253cc4 --- /dev/null +++ b/eww/scripts/kb_leds.py @@ -0,0 +1,60 @@ +import evdev +import json +import sys +import select +import os + +class KeyboardLedMonitor: + def __init__(self): + self.last_state = {"caps": "0", "num": "0"} + self.kbd_devices = [] + + # ищем все устройства, которые умеют в CapsLock + devices = [evdev.InputDevice(path) for path in evdev.list_devices()] + for dev in devices: + capabilities = dev.capabilities().get(evdev.ecodes.EV_KEY, []) + if evdev.ecodes.KEY_CAPSLOCK in capabilities: + self.kbd_devices.append(dev) + + if not self.kbd_devices: + sys.exit(1) + + # выводим начальное состояние + self.push_update(self.kbd_devices[0]) + + def get_led_state(self, dev): + # проверяем коды + leds = dev.leds() + return { + "caps": "1" if 1 in leds else "0", + "num": "1" if 0 in leds else "0" + } + + def push_update(self, dev): + # отдаем json в stdout при изменении + current_state = self.get_led_state(dev) + if current_state != self.last_state: + sys.stdout.write(json.dumps(current_state) + "\n") + sys.stdout.flush() + self.last_state = current_state + + def run(self): + # создаем мапу файловых дескрипторов для селектора + devices_dict = {d.fd: d for d in self.kbd_devices} + + try: + while True: + # ждем событий от любого из устройств + r, _, _ = select.select(devices_dict, [], []) + for fd in r: + dev = devices_dict[fd] + for event in dev.read(): + # если прилетело событие изменения светодиода + if event.type == evdev.ecodes.EV_LED: + self.push_update(dev) + + except (PermissionError, KeyboardInterrupt): + sys.exit(0) + +if __name__ == "__main__": + KeyboardLedMonitor().run() \ No newline at end of file diff --git a/eww/scripts/tray_monitor.py b/eww/scripts/tray_monitor.py new file mode 100755 index 0000000..3d8d56e --- /dev/null +++ b/eww/scripts/tray_monitor.py @@ -0,0 +1,46 @@ +import dbus +from dbus.mainloop.glib import DBusGMainLoop +from gi.repository import GLib +import sys + +class TrayMonitor: + def __init__(self): + # настраиваем dbus и вешаем слушателей + DBusGMainLoop(set_as_default=True) + self.bus = dbus.SessionBus() + self.interface = "org.kde.StatusNotifierWatcher" + self.path = "/StatusNotifierWatcher" + + # подписываемся на регистрацию и удаление иконок + self.bus.add_signal_receiver(self.push_update, "StatusNotifierItemRegistered", self.interface) + self.bus.add_signal_receiver(self.push_update, "StatusNotifierItemUnregistered", self.interface) + + # выводим начальное состояние + self.push_update() + + def get_count(self): + # опрашиваем шину на количество активных элементов + try: + obj = self.bus.get_object(self.interface, self.path) + props = dbus.Interface(obj, "org.freedesktop.DBus.Properties") + items = props.Get(self.interface, "RegisteredStatusNotifierItems") + return len(items) + except: + return 0 + + def push_update(self, *args): + # шлем цифру в stdout + count = self.get_count() + sys.stdout.write(f"{count}\n") + sys.stdout.flush() + + def run(self): + # цикл glib, чтобы слушал сигналы + self.loop = GLib.MainLoop() + try: + self.loop.run() + except KeyboardInterrupt: + sys.exit(0) + +if __name__ == "__main__": + TrayMonitor().run() \ No newline at end of file diff --git a/eww/scripts/volume.py b/eww/scripts/volume.py new file mode 100755 index 0000000..2ab2004 --- /dev/null +++ b/eww/scripts/volume.py @@ -0,0 +1,42 @@ +import subprocess +import sys +import os +import signal + +class VolumeMonitor: + def __init__(self): + # отдаём текущую громкость при старте + self.push_update() + + def get_vol(self): + # достаём цифры из pactl + try: + raw = subprocess.check_output(["pactl", "get-sink-volume", "@DEFAULT_SINK@"], stderr=subprocess.DEVNULL).decode() + if "/" in raw: + # забираем канал и чистим от процентов + return raw.split("/")[1].strip().replace("%", "") + except: + return "0" + return "0" + + def push_update(self): + # отдаём данные в stdout для eww + vol = self.get_vol() + sys.stdout.write(f"{vol}\n") + sys.stdout.flush() + + def run(self): + # подписываемся на события аудио-сервера + proc = subprocess.Popen(["pactl", "subscribe"], stdout=subprocess.PIPE, text=True) + + try: + for line in proc.stdout: + # если изменился основной выход + if "sink" in line: + self.push_update() + except KeyboardInterrupt: + proc.terminate() + sys.exit(0) + +if __name__ == "__main__": + VolumeMonitor().run() \ No newline at end of file diff --git a/eww/scripts/workspaces.py b/eww/scripts/workspaces.py new file mode 100755 index 0000000..5919e45 --- /dev/null +++ b/eww/scripts/workspaces.py @@ -0,0 +1,213 @@ +import json +import subprocess +import os +import sys +import socket +import threading +import time + +class UnifiedWorkspaceManager: + def __init__(self): + self.last_sent_json = "" + self.urgent_window_addresses = set() + self.script_directory = os.path.dirname(os.path.realpath(__file__)) + + self.clients_cache = {} + self.monitors_cache = {} + + self.load_icon_configuration() + self.clients_refresh() + self.monitors_refresh() + + def load_icon_configuration(self): + # тянем иконки из конфига + config_path = os.path.join(self.script_directory, "workspaces_map.json") + try: + with open(config_path, 'r') as file: + self.icon_map = json.load(file) + except: + self.icon_map = {"default": "", "no-icon": ""} + + def monitors_refresh(self): + # обновляем инфу по мониторам + try: + raw = subprocess.check_output(["hyprctl", "-j", "monitors"], stderr=subprocess.DEVNULL) + monitors = json.loads(raw) + self.monitors_cache = { + m['name']: { + "id": m['id'], + "active_workspace_id": m['activeWorkspace']['id'], + "focused": m.get('focused', False) + } for m in monitors + } + except: + self.monitors_cache = {} + + def clients_refresh(self): + # полный сброс и пересборка кэша окон + try: + raw = subprocess.check_output(["hyprctl", "-j", "clients"], stderr=subprocess.DEVNULL) + clients = json.loads(raw) + self.clients_cache = { + c['address'].strip().replace('0x', ''): { + "class": c['class'], + "workspace_id": c['workspace']['id'], + "monitor_id": c['monitor'] + } for c in clients + } + except: + self.clients_cache = {} + + def get_current_state_json(self): + # собираем финальный json + try: + output = {} + + for mon_name, mon_data in self.monitors_cache.items(): + m_id = mon_data["id"] + active_id = mon_data["active_workspace_id"] + focused = mon_data["focused"] + + # готовим 10 слотов + workspaces = { + str(i): {"exists": False, "active": False, "focused": False, "urgent": False, "icons": []} + for i in range(1, 11) + } + + # раскидываем окна по воркспейсам + for addr, client in self.clients_cache.items(): + if client['monitor_id'] == m_id: + ws_id = str(client['workspace_id']) + if ws_id in workspaces: + slot = workspaces[ws_id] + slot["exists"] = True + if addr in self.urgent_window_addresses: + slot["urgent"] = True + + icon = self.icon_map.get(client['class'], self.icon_map.get("default", "")) + if icon not in slot["icons"]: + slot["icons"].append(icon) + + # помечаем текущий активный воркспейс + curr_ws = str(active_id) + if curr_ws in workspaces: + workspaces[curr_ws].update({"active": True, "focused": focused, "exists": True}) + + # рисуем no-icon + for ws in workspaces.values(): + if ws["exists"] and not ws["icons"]: + ws["icons"] = [self.icon_map.get("no-icon", "")] + + output[mon_name.replace("-", "_")] = workspaces + + return json.dumps(output) + except: + return "{}" + + def push_update(self): + # отдаём json в stdout при изменении + current = self.get_current_state_json() + if current != self.last_sent_json: + self.last_sent_json = current + sys.stdout.write(current + '\n') + sys.stdout.flush() + + def update_client(self, window_address): + # обновляем окно + try: + clean_addr = window_address.replace('0x', '') + search_addr = f"0x{clean_addr}" + + raw = subprocess.check_output(["hyprctl", "-j", "clients"], stderr=subprocess.DEVNULL) + clients = json.loads(raw) + + target = next((c for c in clients if c['address'] == search_addr), None) + + if target: + self.clients_cache[clean_addr] = { + "class": target.get('class', 'unknown'), + "workspace_id": target['workspace']['id'], + "monitor_id": target['monitor'] + } + else: + self.clients_cache.pop(clean_addr, None) + + except: + # если что-то отвалилось, то просто пересобираем всё + self.clients_refresh() + + def run(self): + self.push_update() + + def event_socket_listener(): + # слушаем сокет хайпера + signature = os.environ.get('HYPRLAND_INSTANCE_SIGNATURE') + socket_path = f"/run/user/{os.getuid()}/hypr/{signature}/.socket2.sock" + + while True: + try: + with socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) as sock: + sock.connect(socket_path) + buf = "" + while True: + data = sock.recv(4096).decode('utf-8', errors='ignore') + if not data: break + buf += data + + if '\n' in buf: + lines = buf.split('\n') + buf = lines.pop() + + trigger = False + for line in lines: + if ">>" not in line: continue + event, args = line.split('>>') + + # ивенты с окнами + if event in ["openwindow", "movewindow"]: + addr = args.split(',')[0].replace('0x', '') + self.update_client(addr) + trigger = True + + elif event == "closewindow": + addr = args.replace('0x', '') + self.clients_cache.pop(addr, None) + self.urgent_window_addresses.discard(addr) + trigger = True + + # ивенты с мониторами + elif event in ["workspace", "workspacev2", "monitoradded", "monitorremoved"]: + self.monitors_refresh() + trigger = True + + elif event in ["focusedmon", "focusedmonv2"]: + mon_name = args.split(',')[0] + for name in self.monitors_cache: + self.monitors_cache[name]["focused"] = (name == mon_name) + trigger = True + + # алерты + elif event == "urgent": + self.urgent_window_addresses.add(args.replace('0x', '')) + trigger = True + + elif event in ["activewindow", "activewindowv2"]: + addr = args.split(',')[0].replace('0x', '') + if addr in self.urgent_window_addresses: + self.urgent_window_addresses.remove(addr) + trigger = True + + if trigger: + self.push_update() + except: + time.sleep(2) # реконнект, если хайпер отвалился + + # слушаем поток в фоне + threading.Thread(target=event_socket_listener, daemon=True).start() + + try: + while True: time.sleep(100) + except KeyboardInterrupt: sys.exit(0) + +if __name__ == "__main__": + UnifiedWorkspaceManager().run() \ No newline at end of file diff --git a/eww/scripts/workspaces_map.json b/eww/scripts/workspaces_map.json new file mode 100644 index 0000000..e784446 --- /dev/null +++ b/eww/scripts/workspaces_map.json @@ -0,0 +1,13 @@ +{ + "rustdesk": "󰢹", + "org.gajim.Gajim": "󰷕", + "firefox": "", + "code-oss": "󰨞", + "org.telegram.desktop": "", + "kitty": "", + "steam": "", + "spotify": "", + "discord": "󰙯", + "no-icon": "", + "default": "" +}