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:
2026-04-03 12:32:53 +04:00
7 changed files with 793 additions and 0 deletions
+248
View File
@@ -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
View File
@@ -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;"))
+60
View File
@@ -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()
+46
View File
@@ -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()
+42
View File
@@ -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()
+213
View File
@@ -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()
+13
View File
@@ -0,0 +1,13 @@
{
"rustdesk": "󰢹",
"org.gajim.Gajim": "󰷕",
"firefox": "",
"code-oss": "󰨞",
"org.telegram.desktop": "",
"kitty": "",
"steam": "",
"spotify": "",
"discord": "󰙯",
"no-icon": "",
"default": ""
}