From e5cc36a64799345626b224a9d12047fe5cbfa6c0 Mon Sep 17 00:00:00 2001 From: Ezri Date: Thu, 7 Mar 2024 13:10:01 -0700 Subject: [PATCH] Added rocinante eww config --- .../eww##hostname.rocinante/.python-version | 1 + .config/eww##hostname.rocinante/colors.scss | 13 ++ .config/eww##hostname.rocinante/eww.scss | 90 ++++++++ .config/eww##hostname.rocinante/eww.yuck | 183 +++++++++++++++ .../modules/angles/angles.yuck | 11 + .../modules/angles/left.png | Bin 0 -> 1061 bytes .../modules/angles/right.png | Bin 0 -> 1177 bytes .../modules/clock/clock.yuck | 10 + .../modules/network/network.py | 81 +++++++ .../modules/network/network.yuck | 76 +++++++ .../modules/system/system.py | 56 +++++ .../modules/system/system.yuck | 20 ++ .../modules/volume/volume.py | 50 ++++ .../modules/volume/volume.yuck | 28 +++ .../modules/workspaces/hyprland.py | 213 ++++++++++++++++++ .../modules/workspaces/sway.py | 184 +++++++++++++++ .../modules/workspaces/workspaces.yuck | 89 ++++++++ .../modules/workspaces/workspaces.yuck~ | 89 ++++++++ .config/sway/swayidle | 1 - 19 files changed, 1194 insertions(+), 1 deletion(-) create mode 100644 .config/eww##hostname.rocinante/.python-version create mode 100644 .config/eww##hostname.rocinante/colors.scss create mode 100644 .config/eww##hostname.rocinante/eww.scss create mode 100644 .config/eww##hostname.rocinante/eww.yuck create mode 100644 .config/eww##hostname.rocinante/modules/angles/angles.yuck create mode 100644 .config/eww##hostname.rocinante/modules/angles/left.png create mode 100644 .config/eww##hostname.rocinante/modules/angles/right.png create mode 100644 .config/eww##hostname.rocinante/modules/clock/clock.yuck create mode 100755 .config/eww##hostname.rocinante/modules/network/network.py create mode 100644 .config/eww##hostname.rocinante/modules/network/network.yuck create mode 100755 .config/eww##hostname.rocinante/modules/system/system.py create mode 100644 .config/eww##hostname.rocinante/modules/system/system.yuck create mode 100755 .config/eww##hostname.rocinante/modules/volume/volume.py create mode 100644 .config/eww##hostname.rocinante/modules/volume/volume.yuck create mode 100755 .config/eww##hostname.rocinante/modules/workspaces/hyprland.py create mode 100755 .config/eww##hostname.rocinante/modules/workspaces/sway.py create mode 100644 .config/eww##hostname.rocinante/modules/workspaces/workspaces.yuck create mode 100644 .config/eww##hostname.rocinante/modules/workspaces/workspaces.yuck~ delete mode 120000 .config/sway/swayidle diff --git a/.config/eww##hostname.rocinante/.python-version b/.config/eww##hostname.rocinante/.python-version new file mode 100644 index 0000000..bba70ea --- /dev/null +++ b/.config/eww##hostname.rocinante/.python-version @@ -0,0 +1 @@ +eww-modules diff --git a/.config/eww##hostname.rocinante/colors.scss b/.config/eww##hostname.rocinante/colors.scss new file mode 100644 index 0000000..a62e408 --- /dev/null +++ b/.config/eww##hostname.rocinante/colors.scss @@ -0,0 +1,13 @@ +$wallpaper: #1e1e1e; +$foreground: #9a7c9d; + +$bg0: #2d272f; +$bg1: #3f3242; + +$red: #cf6a4c; +$green: #8f9d6a; +$yellow: #f9ee98; +$blue: #7587a6; +$magenta: #9b859d; +$cyan: #afc4db; +$white: #a7a7a7; diff --git a/.config/eww##hostname.rocinante/eww.scss b/.config/eww##hostname.rocinante/eww.scss new file mode 100644 index 0000000..4612533 --- /dev/null +++ b/.config/eww##hostname.rocinante/eww.scss @@ -0,0 +1,90 @@ +@import 'colors'; + +* { + all: unset; +} + +.reservepower.reservepower { + color: $red; +} + +.root { + color: $foreground; + font-family: "Source Code Pro"; + font-size: 9pt; + background-color: rgba(0,0,0,0); + // Probably needs to change + &.bar { + margin: 10px; + margin-bottom: 0px; + } + + &.side { + margin: 10px; + margin-right: 0px; + } +} + +.icon { + font-family: "Font Awesome 5 Free Solid"; +} + +.highlight { + color: $red; +} + +.offline { + color: $bg1; +} + +.special { + color: $blue; +} + +.green { + color: $green; +} + +.big { + font-size: 1.8rem; + font-family: "Nebula"; +} + +// sway module + +.sway--ws { + + .fill { + background-color: $foreground; + } + color: $bg1; + + &.sway--active { + color: $foreground; + } + + &.sway--visible { + color: $blue; + } + + &.sway--focused { + } +} + +.reservepower .sway--ws.sway--active { + color: $red; +} + +.reservepower .sway--ws .fill { + background-color: $red; +} + +// clock module + +.clock--time { + font-size: 20pt; +} + +.clock--date { + font-size: 9pt; +} diff --git a/.config/eww##hostname.rocinante/eww.yuck b/.config/eww##hostname.rocinante/eww.yuck new file mode 100644 index 0000000..aba401f --- /dev/null +++ b/.config/eww##hostname.rocinante/eww.yuck @@ -0,0 +1,183 @@ +;; Include modules +(include "./modules/workspaces/workspaces.yuck") +(include "./modules/clock/clock.yuck") +(include "./modules/system/system.yuck") +(include "./modules/network/network.yuck") +(include "./modules/volume/volume.yuck") + +(defvar power-state "normal") + +;; Windows +(defwidget leftbar--left [] + (box :orientation "h" + :halign "start" + :space-evenly false + :spacing 10 + :class "leftbox" + (vpn-network) + (network))) + +(defwidget leftbar--center [] + (box :orientation "h" + :halign "center" + :space-evenly false + :spacing 0 + :class "centerbox" + (system-name))) + +(defwidget leftbar--right [] + (box :orientation "h" + :halign "end" + :space-evenly false + :spacing 10 + :class "rightbox" + (sway-workspace :group "left") + (sway-workspaces :group "left"))) + +(defwidget rightbar--left [] + (box :orientation "h" + :halign "start" + :space-evenly false + :spacing 20 + :class "leftbox" + (sway-workspaces :group "right") + (sway-workspace :group "right"))) + +(defwidget rightbar--hypr-left [] + (box :orientation "h" + :halign "start" + :space-evenly false + :spacing 20 + :class "leftbox" + (hypr-workspaces :group "main") + (hypr-workspace :group "main"))) + +(defwidget rightbar--center [] + (box :orientation "h" + :halign "center" + :space-evenly false + :spacing 0 + :class "centerbox" + (system-name))) + +(defwidget rightbar--right [] + (box :orientation "h" + :halign "end" + :space-evenly false + :spacing 20 + :class "leftbox" + (system-battery :battery "BAT1") + (audio) + (clock))) + +(defwidget builtinbar--left [] + (box :orientation "h" + :halign "start" + :space-evenly false + :spacing 20 + :class "leftbox" + (sway-workspaces :group "builtin") + (sway-workspace :group "builtin"))) + +(defwidget builtinbar--center [] + (box :orientation "h" + :halign "center" + :space-evenly false + :spacing 0 + :class "centerbox" + (system-name))) + +(defwidget builtinbar--right [] + (box :orientation "h" + :halign "end" + :space-evenly false + :spacing 20 + :class "rightbox" + (vpn-network) + (network) + (system-battery :battery "BAT1") + (audio) + (clock))) + +(defwindow builtinbar + :monitor 0 + :geometry (geometry :width "100%" + :height "36px" + :anchor "top center") + :exclusive true + :focusable false + :stacking "fg" + (centerbox :orientation "h" :class "bar root ${power-state == "critical" ? 'reservepower' : ''}" + (builtinbar--left) + (builtinbar--center) + (builtinbar--right))) + +(defwindow side + :monitor 0 + :geometry (geometry :width "36px" + :height "100%" + :anchor "center left") + :exclusive true + :focusable false + :stacking "fg" + (centerbox :orientation "v" :class "bar root" :width 36 + (sway-workspaces-v :group "main") + "" + "")) + +(defwidget sidebar [] + (box :orientation "v" + :valign "start" + :space-evenly false + :spacing 0 + :class "bar root" + (clock))) + +(defwindow leftbar + :monitor 0 + :geometry (geometry :width "100%" + :height "36px" + :anchor "top center") + :exclusive true + :focusable false + :stacking "fg" + (centerbox :orientation "h" :class "bar root" + (leftbar--left) + (leftbar--center) + (leftbar--right))) + +(defwindow rightbar + :monitor 1 + :geometry (geometry :width "100%" + :height "55px" + :anchor "top center") + :exclusive true + :focusable false + :stacking "fg" + (centerbox :orientation "h" :class "bar root" + (rightbar--left) + (rightbar--center) + (rightbar--right))) + +(defwindow hypr-mainbar + :monitor 0 + :geometry (geometry :width "100%" + :height "36px" + :anchor "top center") + :exclusive true + :focusable false + :stacking "overlay" + (centerbox :orientation "h" :class "bar root" + (rightbar--hypr-left) + (rightbar--center) + (rightbar--right))) + +(defwindow sidebar + :monitor 1 + :geometry (geometry :width "210px" + :height "1044px" + :anchor "left bottom") + :exclusive true + :focusable false + :stacking "fg" + (sidebar)) diff --git a/.config/eww##hostname.rocinante/modules/angles/angles.yuck b/.config/eww##hostname.rocinante/modules/angles/angles.yuck new file mode 100644 index 0000000..c424577 --- /dev/null +++ b/.config/eww##hostname.rocinante/modules/angles/angles.yuck @@ -0,0 +1,11 @@ +(defwidget left-angle [] + (image :path "/home/ezri/.config/eww.new/modules/angles/left.png" + :width 150 + :height 36)) + +(defwidget right-angle [] + (image :path "/home/ezri/.config/eww.new/modules/angles/right.png" + :width 150 + :height 36)) + + diff --git a/.config/eww##hostname.rocinante/modules/angles/left.png b/.config/eww##hostname.rocinante/modules/angles/left.png new file mode 100644 index 0000000000000000000000000000000000000000..f79354dfd326326ae78d3a452ea6dc0a4a7deddb GIT binary patch literal 1061 zcmV+=1ls$FP)pF8FWQhbW?9;ba!ELWdL_~cP?peYja~^aAhuUa%Y?FJQ@H11F}g( zK~#90?b}Ol8+8-~@N>s@8s||Ui$D+(q7p9&!4xTdXeQxNv*^y{v56E=#0TILbi>DB zMF4pLNQhMzn6`>%B$AXeAW^!as9jXF1Uup3Cbs9WNQ#E3O+v8!_e}0@YstQgBTGj! zN8Wn()6Ynp0`Mb{Tykz0&xhu1h63x4}vm4 zfAQ>VSBhuO(DcD78KhV&w*6qjo5y2}0j!!0|K%5ZZdPsPdMV}!rZUm+4 zUHLIJ@VXYPxO)o$Una0FfG;AR7J_u^S+LsKE(9o$bOFo)I0V5Zke&$3kA+vh34$Q( z+d-;l`WmdbHih7E2v)QZB%je>#pS0L9?Uiy#{}X;3ju&KK|jMxr4}sKOy8+?H&DTf z%h_{dd1OvNLKh-Vv=Bg8&gOWrx>CAr`c8GPfeuz&n?i7kKrLDb)-Qkbz3Dqu!h#i- zo14pwT(zD8#MvnXF9661#3Sg7gq{`xGZG!Fb|QtKUeG~c0jq~qqn!Ypb zQWvba`|Y`hT4GIa7XlOjJ_+Dk?EI_c4~0v&zACMozB6r7AFQ}Gh2SJ0SMNTGYO-;m z9(??Z={u9|zy&LA^7QH4=Fbm2OG*JEE+`Jc>fJ&RO6!Z?zAjxeeP_}f_+Z7gDFiP9 z$VLmn;@LMY>!YZDr@`8T=(}mLLQn?j*MXHrP}(wm=OEEw?Zepg{FF39_aE=30p9`Q zfhgAxxBSLZsc!mClQdXeFmZBWyjgD^?^FmRmpQ+&^1+(vJNrq4)dQ3E+aq^Se}3oP zG#A7Sfi)0i%c3t~cBvLD{$%>jUeaLoL9`HnC`899X)OdAEJNaB6#%0l&;BY`68gW#ukZFTHpF8FWQhbW?9;ba!ELWdL_~cP?peYja~^aAhuUa%Y?FJQ@H11SUyD zK~#90?VCYp9Ay}W-|wH@*iDmEgH|g6v4>Eh;8aN3Fk`8@-cw0EEVLRu^r+y;Lm?h> z%`qpF%3kzR5b9wOO=rQZrCEx$JyqKrgq9@RlqQ?Z?EiX5wjFl65@L2|XY)Vzf95;9 z3=i|apNx)Ajb>C6P0C7O137!eSK&U(s<{^4|_R_fU+N-e5gA3DGV@oH92?!lErn>(j@D*`=uGY=j9` zoA9EYd95O9NFWY?bWrvOA-D-BkK|P91wHoQx9p>+=iDhKSP@Yx+8P)TK>^e`kaeQ{ zILcvnfeBU%2#YpJ7r>bSOoT=IsJ^vOTqx?OXWc7Iu$sWLs}V^<#KmoKG`J#QUPL@g zV`8~@`EK}3gPW1qVuG~?B<)O33f8HrX!mhx1XTA(cmQV;N&VSMA^R}uIa`Sd)-Fg5 zjlY@*{NXLprVHRWz(1;@ZP7YcD$bOmo-;~Juy#PGgOuL-qwNo1OSJQ#nmE*-y0-Z9 z^hVTkwi6~;HQ2VT{!+)^3W@gV0DhN(^4LJMS0kLaUD@}+s_h`%1wSBBs){zi$wxQ7 z`!jr|(?q)&?7Lv~Iv*XBCkIY!UyY>uxO56s_epr7^pBQGezkJ_+r_Boj5{44tX})V zVTeBYRJ7M{0wT8oNz`&jtf?8*&on@5X@IaTVd5J6|a<|p0m@5 zeXzE!Mv&eX?RP8KOjXPNBKqQ+9ri~%F2pKW^<{s)AAp-e@kAsCaI&`SZ$vv9+PaTb zBieOke`@}+TFl;$dQL+fE&F2)Hbk0I_BS+_NP|@`+FM(-AoHMVYT4h=Tv{5e za0dy3bO66cLS_F(TeXJx(X?RIm;Ik@)rv4LqNitS%KnYEY7OyY&*VLBWq(6++4UDI z?qz>NbJ=mQYPV{`Wq)W)8qqd{X&9{MY}F2IZ`I~ny;W<79|>S*W&bF*Y7Oy2E`0OD rPXI;$EDCWALQh%AIa3`m{2%@W0Qq2h)60p900000NkvXXu0mjfb9pXb literal 0 HcmV?d00001 diff --git a/.config/eww##hostname.rocinante/modules/clock/clock.yuck b/.config/eww##hostname.rocinante/modules/clock/clock.yuck new file mode 100644 index 0000000..6e9adb1 --- /dev/null +++ b/.config/eww##hostname.rocinante/modules/clock/clock.yuck @@ -0,0 +1,10 @@ +(defpoll clock--data :interval "500ms" + `date +'{"hour": "%H", "minute": "%M", "second": "%S", "year": "%Y", "day": "%d", "month": "%m", "dow": "%A", "month_name": "%B"}'`) + +(defwidget clock [] + (box :class "module text" + :spacing 0 + :orientation "v" + (label :class "special" + :text "${clock--data['hour']}:${clock--data['minute']}") + (label :text "${clock--data['year']}-${clock--data['month']}-${clock--data['day']}"))) diff --git a/.config/eww##hostname.rocinante/modules/network/network.py b/.config/eww##hostname.rocinante/modules/network/network.py new file mode 100755 index 0000000..742aa39 --- /dev/null +++ b/.config/eww##hostname.rocinante/modules/network/network.py @@ -0,0 +1,81 @@ +#!/usr/bin/env python3 + +import subprocess +import json +from time import sleep + +TRUSTED_NETWORKS = ['gayer people'] + +def wifi(): + ssid_cmd = subprocess.run(['iwgetid', '-r'], capture_output = True) + if ssid_cmd.returncode != 0: + return {"connected": False, "ssid": None} + ssid = ssid_cmd.stdout.decode('utf-8').strip() + ssid = ssid if len(ssid) <= 15 else f"{ssid[:14]}…" + return {"connected": True, "ssid": ssid} + +def netdev(device: str): + ip_cmd = subprocess.run(['ip', '-j', 'addr', 'show', device], capture_output = True) + if ip_cmd.returncode != 0: + return {"exists": False} + ip_data = json.loads(ip_cmd.stdout.decode('utf-8'))[0] + online = ip_data["operstate"] == "UP" + ip4_addr = "" + ip4_prefix_length = 24 + addr4_info = list(filter(lambda addr: addr.get("family") == "inet", ip_data["addr_info"])) + if len(addr4_info) >= 1: + ip4_addr = addr4_info[0]["local"] + ip4_prefix_length = addr4_info[0]["prefixlen"] + connecting = ip_data["operstate"] != "DOWN" and (ip4_addr == "" or not online) + return { + "exists": True, + "online": online, + "connecting": connecting, + "offline": not online and not connecting, + "ip4_addr": ip4_addr, + "ip4_prefix": ip4_prefix_length + } + +def trusted(ssid: str | None): + # Don't throw up an "INSECURE" alert when offline + if not ssid: + return True + return ssid in TRUSTED_NETWORKS + +def ping(host: str): + ping_cmd = subprocess.Popen(['/usr/bin/ping', '-c1', '-W2', host], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) + return ping_cmd + +counter = 0 +ezrinet_last = False +internet_last = False +ezrinet_ping = None +internet_ping = None + +while True: + if counter == 0: + ezrinet_ping = ping('10.242.3.1') + internet_ping = ping('1.1.1.1') + if ezrinet_ping.poll() is not None: + ezrinet_last = ezrinet_ping.returncode == 0 + if internet_ping.poll() is not None: + internet_last = internet_ping.returncode == 0 + wifi_data = wifi() + result = { + "wifi": wifi_data, + "network": { + "wlan0": netdev("wlan0"), + "ezrinet": netdev("ezrinet"), + "wg-mullvad": netdev("mullvad"), + }, + "connection": { + "ezrinet": ezrinet_last, + "internet": internet_last + }, + "trusted": trusted(wifi_data.get("ssid", None)) + } + print(json.dumps(result), flush=True) + counter += 1 + # Send pings every 10 seconds + counter %= 3 + sleep(1) diff --git a/.config/eww##hostname.rocinante/modules/network/network.yuck b/.config/eww##hostname.rocinante/modules/network/network.yuck new file mode 100644 index 0000000..c43aee6 --- /dev/null +++ b/.config/eww##hostname.rocinante/modules/network/network.yuck @@ -0,0 +1,76 @@ +(deflisten network--data + `~/.config/eww/modules/network/network.py`) + +(defwidget network--wlan [device] + (box :orientation "h" + :halign "start" + :space-evenly false + :spacing 10 + (label :class "offline" + :visible {network--data["network"][device]["offline"]} + :text "offline") + (label :visible {network--data["network"][device]["connecting"]} + :class "highlight" + :text "connecting...") + (label :visible {network--data["network"][device]["online"] && !network--data.network[device].connecting} + :class "special" + :text "${network--data['wifi']['ssid']}") + (label :visible {!network--data.connection.internet && network--data.network[device].online} + :class "highlight" + :text "- no internet" + ))) + +(defwidget network--lan [device] + (box :orientation "h" + :halign "start" + :space-evenly false + :spacing 0 + (label :class "offline" + :visible {network--data["network"][device]["offline"]} + :text "offline") + (label :visible {network--data["network"][device]["connecting"]} + :class "highlight" + :text "connecting...") + (label :visible {network--data["network"][device]["online"] && !network--data.network[device].connecting} + :class "special" + :text "${network--data['network'][device]['ip4_addr']}/${network--data['network'][device]['ip4_prefix']}"))) + +(defwidget network--proxy-vpn [device ?required] + (box :orientation "h" + :halign "start" + :space-evenly false + :spacing 0 + :visible {network--data.connection.internet && (network--data["network"][device]["exists"] || required)} + (label :class "highlight" + :text "- insecure" + :visible {! network--data["network"][device]["exists"]}) + (label :class "green" + :text "- secured" + :visible {network--data["network"][device]["exists"]}))) + +(defwidget vpn-network [] + (box :orientation "v" + :halign "start" + :space-evenly false + :spacing 0 + (label :class "highlight" + :text "offline" + :visible {!network--data.connection.ezrinet}) + (label :class "green" + :text "connected" + :visible {network--data.connection.ezrinet}) + "personal network")) + +(defwidget network [] + (box :orientation "v" + :halign "start" + :space-evenly false + :spacing 0 + (box :orientation "h" + :halign "center" + :space-evenly false + :spacing 10 + (network--wlan :device "wlan0") + (network--proxy-vpn :device "wg-mullvad" :required {!network--data.trusted})) + "communications")) + diff --git a/.config/eww##hostname.rocinante/modules/system/system.py b/.config/eww##hostname.rocinante/modules/system/system.py new file mode 100755 index 0000000..116bfb5 --- /dev/null +++ b/.config/eww##hostname.rocinante/modules/system/system.py @@ -0,0 +1,56 @@ +#!/usr/bin/env python3 + +from time import sleep +import json +import psutil +import subprocess +import os + +def cpu(): + util = psutil.cpu_percent(percpu=False) + per_core = psutil.cpu_percent(percpu=True) + return { + "avg_display": f"{int(util):2}", + "avg": util, + "cores": per_core, + "core_count": len(per_core) + } + +def sensor(chip: str): + return { temp.label: temp.current for temp in psutil.sensors_temperatures()[chip] } + +def memory(): + mem = psutil.virtual_memory() + return { + 'used': mem.used, + 'available': mem.available, + 'total': mem.total, + 'percent': mem.percent + } + +def swap(): + mem = psutil.swap_memory() + return { + 'used': mem.used, + 'total': mem.total, + 'percent': mem.percent + } + +def reboot(): + running = os.uname().release + file_proc = subprocess.run("file /boot/vmlinuz-linux | cut -d',' -f2 | cut -d' ' -f3", shell=True, capture_output=True) + installed = file_proc.stdout.strip().decode('utf-8') + return running != installed + +sensor_list = ['coretemp'] + +while True: + result = { + "cpu": cpu(), + "sensors": { chip: sensor(chip) for chip in sensor_list }, + "memory": memory(), + "swap": swap(), + "reboot": reboot() + } + print(json.dumps(result), flush=True) + sleep(1) diff --git a/.config/eww##hostname.rocinante/modules/system/system.yuck b/.config/eww##hostname.rocinante/modules/system/system.yuck new file mode 100644 index 0000000..3a008b4 --- /dev/null +++ b/.config/eww##hostname.rocinante/modules/system/system.yuck @@ -0,0 +1,20 @@ +(defpoll system--hostname :interval "30s" + `bash -c 'eval $(cat /etc/machine-info); echo $PRETTY_HOSTNAME'`) + +(defvar system--battery-text "") + +(defwidget system-battery [battery] + (box :orientation "v" + :halign "start" + :class "module text" + :space-evenly false + :spacing 0 + (label :class {EWW_BATTERY[battery].status == 'Charging' || EWW_BATTERY[battery].status == 'Full' ? 'green' : EWW_BATTERY[battery].capacity <= 30 ? 'highlight' : 'special'} + :text "${EWW_BATTERY[battery].capacity}%") + (label :text "${EWW_BATTERY[battery].status == 'Charging' || EWW_BATTERY[battery].capacity == 100 ? 'external' : EWW_BATTERY[battery].capacity <= 15 ? 'emergency' : EWW_BATTERY[battery].capacity <= 30 ? 'reserve' : 'internal'} power"))) + +(defwidget system-name [] + (label :halign "center" + :valign "center" + :class "module text big" + :text {system--hostname})) diff --git a/.config/eww##hostname.rocinante/modules/volume/volume.py b/.config/eww##hostname.rocinante/modules/volume/volume.py new file mode 100755 index 0000000..8bc8bd9 --- /dev/null +++ b/.config/eww##hostname.rocinante/modules/volume/volume.py @@ -0,0 +1,50 @@ +#!/usr/bin/env python3 + +import asyncio +import signal +from contextlib import suppress +import pulsectl_asyncio +import json +import sys +import os + +async def get_values(pulse): + sink = None + try: + sink = await pulse.get_sink_by_name("@DEFAULT_SINK@") + source = await pulse.get_source_by_name("@DEFAULT_SOURCE@") + except: + # reload the script in the event of an exception + os.execv(sys.argv[0], sys.argv) + sink_result = { + "mute": sink.mute == 1, + "volume": f"{int(sink.volume.value_flat * 100):2}" + } + source_result = { + "mute": source.mute == 1, + "volume": f"{int(source.volume.value_flat * 100):2}" + } + result = { + "output": sink_result, + "input": source_result + } + print(json.dumps(result), flush=True) + return result + +async def listen(): + async with pulsectl_asyncio.PulseAsync("volume-monitor") as pulse: + await get_values(pulse) + async for _ in pulse.subscribe_events('all'): + await get_values(pulse) + +async def main(): + listen_task = asyncio.create_task(listen()) + + for sig in (signal.SIGTERM, signal.SIGHUP, signal.SIGINT): + loop.add_signal_handler(sig, listen_task.cancel) + + with suppress(asyncio.CancelledError): + await listen_task + +loop = asyncio.get_event_loop() +loop.run_until_complete(main()) diff --git a/.config/eww##hostname.rocinante/modules/volume/volume.yuck b/.config/eww##hostname.rocinante/modules/volume/volume.yuck new file mode 100644 index 0000000..31ef72c --- /dev/null +++ b/.config/eww##hostname.rocinante/modules/volume/volume.yuck @@ -0,0 +1,28 @@ +(deflisten volume--data + `~/.config/eww/modules/volume/volume.py`) + +(defwidget volume-h [] + (box :orientation "h" + :halign "start" + :class "module ${volume--data['mute'] ? 'offline' : ''}" + :space-evenly false + :spacing 0 + (label :visible {!volume--data["mute"]} :text "") + (label :visible { volume--data["mute"]} :text "") + (label :text " ${volume--data['volume']}"))) + +(defwidget audio [] + (box :orientation "v" + :halign "center" + :class "module text" + :space-evenly false + :spacing 0 + (box :orientation "h" + :halign "center" + :space-evenly false + :spacing 15 + (label :class {volume--data.output.mute ? "offline" : "special"} + :text "${volume--data.output.volume}%") + (label :class {volume--data.input.mute ? "offline" : "special"} + :text "${volume--data.input.volume}%")) + "audio system")) diff --git a/.config/eww##hostname.rocinante/modules/workspaces/hyprland.py b/.config/eww##hostname.rocinante/modules/workspaces/hyprland.py new file mode 100755 index 0000000..a332f07 --- /dev/null +++ b/.config/eww##hostname.rocinante/modules/workspaces/hyprland.py @@ -0,0 +1,213 @@ +#!/usr/bin/env python3 + +import asyncio +import json +import os +import sys + +WorkspaceTree = dict[str, dict[str, list['Workspace']]] +WorkspaceList = dict[str, 'Workspace'] + +socketdir = f"/tmp/hypr/{os.environ['HYPRLAND_INSTANCE_SIGNATURE']}" +socket1 = f"{socketdir}/.socket.sock" +socket2 = f"{socketdir}/.socket2.sock" + +listened_events = ['workspace', 'movewindow', 'createworkspace', 'destroyworkspace'] + +class Workspace: + + index: str = None + name: str = None + exec_cmd: str = None + group: str = None + + active: bool = False + visible: bool = False + focused: bool = False + alerted: bool = False + + def __init__(self, dictionary: dict[str, str | int], group: str): + self.index = str(dictionary.get('index')) + self.name = dictionary.get('name') + self.exec_cmd = dictionary.get('exec') + self.group = group + + self.active = dictionary.get('active', False) + self.visible = dictionary.get('visible', False) + self.focused = dictionary.get('focused', False) + self.alerted = dictionary.get('alerted', False) + + def update_state(self, state_update, monitor_dict): + self.active = True + self.visible = monitor_dict[state_update['monitor']]['activeWorkspace']['name'] == state_update['name'] + self.focused = self.visible and monitor_dict[state_update['monitor']]['focused'] + self.alerted = False + + def deactivate(self): + self.active = False + self.visible = False + self.focused = False + self.alerted = False + + @classmethod + def parse_file(cls: type, filename: str) -> tuple[WorkspaceTree, WorkspaceList]: + + result = {} + + initial: dict = None + with open(filename, 'r') as file: + initial = json.load(file) + + workspaces: WorkspaceList = {} + + def find_workspace(workspace: dict[str, str|int], group: str) -> 'Workspace': + index = str(workspace.get('index')) + if index in workspaces: + return workspaces[index] + else: + workspaces[index] = cls(workspace, group) + return workspaces[index] + + result = { + context: { + group: [ + find_workspace(workspace, group) for workspace in workspaces + ] for group, workspaces in groups.items() + } for context, groups in initial.items() + } + + return result, workspaces + + @staticmethod + def full_dictify(tree: WorkspaceTree): + return { + context: { + group: [ + workspace.dictify() for workspace in workspaces + ] for group, workspaces in groups.items() + } for context, groups in tree.items() + } + + def dictify(self): + + return { + 'index': self.index, + 'name': self.name, + 'exec': self.exec_cmd, + 'active': self.active, + 'visible': self.visible, + 'focused': self.focused, + 'alerted': self.alerted + } + +workspace_tree, workspace_list = Workspace.parse_file(f"{os.environ['HOME']}/.config/hypr/workspaces.json") + +data = { + "ws": {}, + "current": {}, + "context": "personal", + "visible": {}, + } + +def write_data(): + global data + print(json.dumps(data), flush=True) + +async def get_workspace_data(): + request = b"[[BATCH]] j/workspaces; j/monitors" + reader = None + writer = None + data = None + try: + reader, writer = await asyncio.open_unix_connection(socket1) + writer.write(request) + await writer.drain() + data = await reader.read() + finally: + if writer is not None: + writer.close() + if data is None: + return; + + workspace_data = None + monitor_data = None + try: + workspace_data, monitor_data = data.decode().split("}][{") + workspace_data += "}]" + monitor_data = "[{" + monitor_data + except ValueError: + print("Error unpacking response", file=sys.stderr) + return + + return [json.loads(workspace_data), json.loads(monitor_data)] + +async def update_workspaces(): + global workspace_tree, workspace_list, data + + workspaces, monitors = await get_workspace_data() + + ws_hypr_dict = { ws["name"]: ws for ws in workspaces } + monitor_dict = { monitor["name"]: monitor for monitor in monitors } + + touched = [] + + for index, ws_data in ws_hypr_dict.items(): + + # Update data for all active workspaces + ws_state = workspace_list.get(index) + if ws_state is None: + # If the workspace isn't configured, note it and handle gracefully + print(f"Warning: found unconfigured workspace {index}", file=sys.stderr, flush=True) + if monitor_dict[ws_data['monitor']]['activeWorkspace']['name'] == ws_data['name']: + data['current'] = { + 'index': index, + 'name': 'undefined', + 'exec': 'alacritty', + 'active': True, + 'visible': True, + 'focused': True, + 'alerted': False + } + else: + # Otherwise, 'touch' it and update status data + touched.append(index) + ws_state.update_state(ws_data, monitor_dict) + if ws_state.focused: + data['current'] = ws_state.dictify() + if ws_state.visible: + data['visible'][ws_state.group] = ws_state.dictify() + + inactives = [ + v for k, v in workspace_list.items() if v.active and k not in touched + ] + + for workspace in inactives: + workspace.deactivate() + + # Create an 'unknown' workspace object for each group that doesn't yet have a listed visible workspace + for group in workspace_tree[data['context'] or 'personal'].keys(): + if group not in data['visible']: + data['visible'][group] = { + 'index': 'U', + 'name': 'undefined', + 'exec': 'alacritty', + 'active': True, + 'visible': True, + 'focused': False, + 'alerted': False + } + data['ws'] = Workspace.full_dictify(workspace_tree) + write_data() + +async def main(): + reader, writer = await asyncio.open_unix_connection(socket2) + + await update_workspaces() + + while True: + event = (await reader.readline()).decode() + eventType, eventData = event.split(">>") + if eventType in listened_events: + await update_workspaces() + +asyncio.run(main()) diff --git a/.config/eww##hostname.rocinante/modules/workspaces/sway.py b/.config/eww##hostname.rocinante/modules/workspaces/sway.py new file mode 100755 index 0000000..e1c426e --- /dev/null +++ b/.config/eww##hostname.rocinante/modules/workspaces/sway.py @@ -0,0 +1,184 @@ +#!/usr/bin/env python3 + +import asyncio +import json +import os +import sys +from i3ipc.aio import Connection +from i3ipc import Event +from typing import List, Dict, Any, Union, Tuple + +WorkspaceTree = Dict[str, Dict[str, List['Workspace']]] +WorkspaceList = Dict[str, 'Workspace'] + +class Workspace: + + index: str = None + name: str = None + exec_cmd: str = None + group: str = None + + active: bool = False + visible: bool = False + focused: bool = False + alerted: bool = False + + def __init__(self, dictionary: Dict[str, Union[str, int]], group: str): + self.index = str(dictionary.get('index')) + self.name = dictionary.get('name') + self.exec_cmd = dictionary.get('exec') + self.group = group + + self.active = dictionary.get('active', False) + self.visible = dictionary.get('visible', False) + self.focused = dictionary.get('focused', False) + self.alerted = dictionary.get('alerted', False) + + def update_state(self, state_update: Any): + self.active = True + self.visible = state_update.visible + self.focused = state_update.focused + self.alerted = state_update.urgent + + def deactivate(self): + self.active = False + self.visible = False + self.focused = False + self.alerted = False + + @classmethod + def parse_file(cls: type, filename: str) -> Tuple[WorkspaceTree, WorkspaceList]: + + result = {} + + initial: dict = None + with open(filename, 'r') as file: + initial = json.load(file) + + workspaces: WorkspaceList = {} + + def find_workspace(workspace: Dict[str, Union[str, int]], group: str) -> 'Workspace': + index = str(workspace.get('index')) + if index in workspaces: + return workspaces[index] + else: + workspaces[index] = cls(workspace, group) + return workspaces[index] + + result = { + context: { + group: [ + find_workspace(workspace, group) for workspace in workspaces + ] for group, workspaces in groups.items() + } for context, groups in initial.items() + } + + return result, workspaces + + @staticmethod + def full_dictify(tree: WorkspaceTree): + return { + context: { + group: [ + workspace.dictify() for workspace in workspaces + ] for group, workspaces in groups.items() + } for context, groups in tree.items() + } + + def dictify(self): + + return { + 'index': self.index, + 'name': self.name, + 'exec': self.exec_cmd, + 'active': self.active, + 'visible': self.visible, + 'focused': self.focused, + 'alerted': self.alerted + } + +workspace_tree, workspace_list = Workspace.parse_file(f"{os.environ['HOME']}/.config/sway/workspaces.json") + +data = { + "ws": {}, + "mode": "default", + "current": {}, + "context": "personal", + "visible": {}, +} + +def write_data(): + global data + print(json.dumps(data), flush=True) + +async def workspace_event(self: Connection, _): + global workspace_tree, workspace_list, data + + ws_sway_dict = { ws.name: ws for ws in await self.get_workspaces()} + + touched = [] + + for index, ws_data in ws_sway_dict.items(): + + # Update data for all active workspaces + ws_state = workspace_list.get(index) + if ws_state is None: + # If the workspace isn't configured, note it and handle gracefully + print(f"Warning: found unconfigured workspace {index}", file=sys.stderr, flush=True) + if ws_data.focused: + data['current'] = { + 'index': index, + 'name': 'undefined', + 'exec': 'alacritty', + 'active': True, + 'visible': True, + 'focused': True, + 'alerted': False + } + else: + # Otherwise, 'touch' it and update the status data + touched.append(index) + ws_state.update_state(ws_data) + if ws_state.focused: + data['current'] = ws_state.dictify() + if ws_state.visible: + data['visible'][ws_state.group] = ws_state.dictify() + + # 'Deactivate' any untouched workspaces + inactives = [ + v for k, v in workspace_list.items() if v.active and k not in touched + ] + + for workspace in inactives: + workspace.deactivate() + + # Create an 'unknown' workspace object for each group that doesn't yet have a listed visible workspace + for group in workspace_tree[data['context'] or 'personal'].keys(): + if group not in data['visible']: + data['visible'][group] = { + 'index': 'U', + 'name': 'undefined', + 'exec': 'alacritty', + 'active': True, + 'visible': True, + 'focused': False, + 'alerted': False + } + + data['ws'] = Workspace.full_dictify(workspace_tree) + write_data() + +async def mode_event(self: Connection, event): + global data + data['mode'] = event.change + write_data() + +async def main(): + sway = await Connection(auto_reconnect = True).connect() + + sway.on(Event.WORKSPACE, workspace_event) + sway.on(Event.MODE, mode_event) + await workspace_event(sway, None) + await sway.main() + +asyncio.run(main()) diff --git a/.config/eww##hostname.rocinante/modules/workspaces/workspaces.yuck b/.config/eww##hostname.rocinante/modules/workspaces/workspaces.yuck new file mode 100644 index 0000000..0acaf87 --- /dev/null +++ b/.config/eww##hostname.rocinante/modules/workspaces/workspaces.yuck @@ -0,0 +1,89 @@ +(deflisten sway--data :initial '{mode: "default"}' + `~/.config/eww/modules/workspaces/sway.py`) + +(deflisten hypr--data :initial '{}' + `~/.config/eww/modules/workspaces/hyprland.py`) + +(defwidget sway--workspace [ws] + (button :onclick "swaymsg workspace ${ws['index']}" + (overlay :width 20 + :height 20 + (circular-progress + :class 'indicator-circle sway--ws ${ws.active ? "sway--active" : ""} ${ws.visible ? "sway--visible" : ""} ${ws.focused ? "sway--focused" : ""} ${ws.alerted ? "sway--alerted" : ""}' + :value 100 + :start-at 0 + :clockwise true + :width 20 + :thickness 1 + (box :class 'fill' :visible {ws.focused})) + ))) + +(defwidget hypr--workspace [ws] + (button :onclick "hyprmsg dispatch workspace ${ws['index']}" + (circular-progress + :class 'indicator-circle sway--ws ${ws.active ? "sway--active" : ""} ${ws.visible ? "sway--visible" : ""} ${ws.focused ? "sway--focused": ""}' + :value 100 + :start-at 0 + :clockwise true + :width 20 + :thickness 1 + (box :class 'fill' :visible {ws.focused})))) + +(defwidget sway-mode [] + (box :orientation "v" + :halign "center" + :space-evenly false + :spacing 0 + :class "module text" + :visible {sway--data.mode != "default"} + (label :class "special" + :text "${hypr--data.mode}") + "sway mode")) + +(defwidget sway-workspace [group] + (box :orientation "v" + :halign "center" + :space-evenly false + :spacing 0 + :class 'module text' + (label :class '${sway--data.visible[group].focused ? "special" : "offline"}' + :text '${sway--data.visible[group].name}') + (label :text "current workspace"))) + +(defwidget hypr-workspace [group] + (box :orientation "v" + :halign "center" + :space-evenly false + :spacing 0 + :class 'module text' + (label :class '${hypr--data.visible[group].focused ? "special" : "offline"}' + :text '${hypr--data.visible[group].name}') + (label :text "current workspace"))) + +(defwidget sway-workspaces [group] + (box :orientation "h" + :halign "start" + :space-evenly false + :spacing 5 + :class "sway--root" + (for workspace in {sway--data.ws[sway--data.context ?: "personal"][group]} + (sway--workspace :ws workspace)))) + +(defwidget sway-workspaces-v [group] + (box :orientation "v" + :halign "center" + :valign "start" + :space-evenly false + :spacing 5 + :class "sway--root" + (for workspace in {sway--data.ws[sway--data.context ?: "personal"][group]} + (sway--workspace :ws workspace)))) + +(defwidget hypr-workspaces [group] + (box :orientation "h" + :halign "start" + :space-evenly false + :spacing 5 + :class "sway--root" + (for workspace in {hypr--data.ws[hypr--data.context ?: "personal"][group]} + (hypr--workspace :ws workspace)))) diff --git a/.config/eww##hostname.rocinante/modules/workspaces/workspaces.yuck~ b/.config/eww##hostname.rocinante/modules/workspaces/workspaces.yuck~ new file mode 100644 index 0000000..2aa84a6 --- /dev/null +++ b/.config/eww##hostname.rocinante/modules/workspaces/workspaces.yuck~ @@ -0,0 +1,89 @@ +(deflisten sway--data :initial '{mode: "default"}' + `~/.config/eww/modules/sway.py`) + +(deflisten hypr--data :initial '{}' + `~/.config/eww/modules/workspaces/hyprland.py`) + +(defwidget sway--workspace [ws] + (button :onclick "swaymsg workspace ${ws['index']}" + (overlay :width 20 + :height 20 + (circular-progress + :class 'indicator-circle sway--ws ${ws.active ? "sway--active" : ""} ${ws.visible ? "sway--visible" : ""} ${ws.focused ? "sway--focused" : ""} ${ws.alerted ? "sway--alerted" : ""}' + :value 100 + :start-at 0 + :clockwise true + :width 20 + :thickness 1 + (box :class 'fill' :visible {ws.focused})) + ))) + +(defwidget hypr--workspace [ws] + (button :onclick "hyprmsg dispatch workspace ${ws['index']}" + (circular-progress + :class 'indicator-circle sway--ws ${ws.active ? "sway--active" : ""} ${ws.visible ? "sway--visible" : ""} ${ws.focused ? "sway--focused": ""}' + :value 100 + :start-at 0 + :clockwise true + :width 20 + :thickness 1 + (box :class 'fill' :visible {ws.focused})))) + +(defwidget sway-mode [] + (box :orientation "v" + :halign "center" + :space-evenly false + :spacing 0 + :class "module text" + :visible {sway--data.mode != "default"} + (label :class "special" + :text "${hypr--data.mode}") + "sway mode")) + +(defwidget sway-workspace [group] + (box :orientation "v" + :halign "center" + :space-evenly false + :spacing 0 + :class 'module text' + (label :class '${sway--data.visible[group].focused ? "special" : "offline"}' + :text '${sway--data.visible[group].name}') + (label :text "current workspace"))) + +(defwidget hypr-workspace [group] + (box :orientation "v" + :halign "center" + :space-evenly false + :spacing 0 + :class 'module text' + (label :class '${hypr--data.visible[group].focused ? "special" : "offline"}' + :text '${hypr--data.visible[group].name}') + (label :text "current workspace"))) + +(defwidget sway-workspaces [group] + (box :orientation "h" + :halign "start" + :space-evenly false + :spacing 5 + :class "sway--root" + (for workspace in {sway--data.ws[sway--data.context ?: "personal"][group]} + (sway--workspace :ws workspace)))) + +(defwidget sway-workspaces-v [group] + (box :orientation "v" + :halign "center" + :valign "start" + :space-evenly false + :spacing 5 + :class "sway--root" + (for workspace in {sway--data.ws[sway--data.context ?: "personal"][group]} + (sway--workspace :ws workspace)))) + +(defwidget hypr-workspaces [group] + (box :orientation "h" + :halign "start" + :space-evenly false + :spacing 5 + :class "sway--root" + (for workspace in {hypr--data.ws[hypr--data.context ?: "personal"][group]} + (hypr--workspace :ws workspace)))) diff --git a/.config/sway/swayidle b/.config/sway/swayidle deleted file mode 120000 index 7ff72f1..0000000 --- a/.config/sway/swayidle +++ /dev/null @@ -1 +0,0 @@ -swayidle##class.desktop \ No newline at end of file