Merge pull request 'feat: add initial eww config' (#1) from add_eww_config into main
Reviewed-on: https://gitea.oyacoi.ru/pyrschtjag/dotfiles-hypr/pulls/1
This commit was merged in pull request #1.
This commit is contained in:
+248
@@ -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; }
|
||||
}
|
||||
+171
@@ -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;"))
|
||||
Executable
+60
@@ -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()
|
||||
Executable
+46
@@ -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()
|
||||
Executable
+42
@@ -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()
|
||||
Executable
+213
@@ -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()
|
||||
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"rustdesk": "",
|
||||
"org.gajim.Gajim": "",
|
||||
"firefox": "",
|
||||
"code-oss": "",
|
||||
"org.telegram.desktop": "",
|
||||
"kitty": "",
|
||||
"steam": "",
|
||||
"spotify": "",
|
||||
"discord": "",
|
||||
"no-icon": "",
|
||||
"default": ""
|
||||
}
|
||||
Reference in New Issue
Block a user