diff --git a/home/programs/graphical/bars/default.nix b/home/programs/graphical/bars/default.nix new file mode 100644 index 0000000..160e671 --- /dev/null +++ b/home/programs/graphical/bars/default.nix @@ -0,0 +1,5 @@ +_: { + imports = [ + ./eww + ]; +} diff --git a/home/programs/graphical/bars/eww/config/css/_colors.scss b/home/programs/graphical/bars/eww/config/css/_colors.scss new file mode 100644 index 0000000..1534b69 --- /dev/null +++ b/home/programs/graphical/bars/eww/config/css/_colors.scss @@ -0,0 +1,43 @@ +$rosewater: #f5e0dc; +$flamingo: #f2cdcd; +$pink: #f5c2e7; +$mauve: #cba6f7; +$red: #f38ba8; +$maroon: #eba0ac; +$peach: #fab387; +$yellow: #f9e2af; +$gold: #efcb10; +$green: #a6e3a1; +$lime: #78db32; +$teal: #94e2d5; +$sky: #89dceb; +$sapphire: #74c7ec; +$blue: #89b4fa; +$lavender: #b4befe; +$orange: #ffa500; + +$text: #cdd6f4; +$subtext1: #bac2de; +$subtext0: #a6adc8; +$overlay2: #9399b2; +$overlay1: #7f849c; +$overlay0: #6c7086; + +$surface2: #585b70; +$surface1: #45475a; +$surface0: #313244; + +$base-a: rgba(30, 30, 40, 0.65); +$base: rgba(30, 30, 40, 1); +$base1-a: rgba(49, 50, 68, 0.85); +$base1: rgba(49, 50, 68, 1); +$mantle: #181825; +$crust: #11111b; + +$fg: $text; +$bg-a: $base-a; +$bg: $base; +$bg1: $base1; +$bg1-a: $base1-a; +$border: #28283d; +$shadow: $crust; diff --git a/home/programs/graphical/bars/eww/config/css/modules/_battery.scss b/home/programs/graphical/bars/eww/config/css/modules/_battery.scss new file mode 100644 index 0000000..d4894a4 --- /dev/null +++ b/home/programs/graphical/bars/eww/config/css/modules/_battery.scss @@ -0,0 +1,55 @@ +@keyframes blink { + 0%{ + opacity: 0; + } + 50%{ + opacity: 0.7; + } + 100%{ + opacity: 0; + } + } + +// .unplugged.low { +// color: #0000ff; +// } + +.battery { + + .icon, .icon label { + font-family: "Material Symbols Outlined"; + + .extra, .extra label { + font-family: "Font Awesome 6 Free"; + } + } + + .critical { + .unplugged { + color: #f00; + .extra { animation: blink 1.2s linear infinite; } + } + + .plugged { + .icon { color: $orange } + .extra { color: $green; } + } + } + + .normal { + .unplugged { + .icon { color: $orange } + } + .plugged { + .icon { color: $green; } + } + } + + .full { + .icon { color: $green; } + .extra { color: $lime; } + } + + .extra { margin-right: 5px; } + .icon { margin-right: 5px; } +} diff --git a/home/programs/graphical/bars/eww/config/css/modules/_bitcoin.scss b/home/programs/graphical/bars/eww/config/css/modules/_bitcoin.scss new file mode 100644 index 0000000..82d5a61 --- /dev/null +++ b/home/programs/graphical/bars/eww/config/css/modules/_bitcoin.scss @@ -0,0 +1,3 @@ +.bitcoin { + .icon { margin-right: 6px; color: $gold; } +} diff --git a/home/programs/graphical/bars/eww/config/css/modules/_clock.scss b/home/programs/graphical/bars/eww/config/css/modules/_clock.scss new file mode 100644 index 0000000..98d9823 --- /dev/null +++ b/home/programs/graphical/bars/eww/config/css/modules/_clock.scss @@ -0,0 +1,4 @@ +.clock { + // color: $sapphire; + .icon { margin-right: 6px; color: $sapphire; } +} diff --git a/home/programs/graphical/bars/eww/config/css/modules/_cpu.scss b/home/programs/graphical/bars/eww/config/css/modules/_cpu.scss new file mode 100644 index 0000000..a183c58 --- /dev/null +++ b/home/programs/graphical/bars/eww/config/css/modules/_cpu.scss @@ -0,0 +1,3 @@ +.cpu { + .icon { color: $lime; } +} diff --git a/home/programs/graphical/bars/eww/config/css/modules/_gammarelay.scss b/home/programs/graphical/bars/eww/config/css/modules/_gammarelay.scss new file mode 100644 index 0000000..f62dcc8 --- /dev/null +++ b/home/programs/graphical/bars/eww/config/css/modules/_gammarelay.scss @@ -0,0 +1,7 @@ +.gammarelay { + .icon { + color: $peach; + margin-left: 2px; + margin-right: 2px; + } +} diff --git a/home/programs/graphical/bars/eww/config/css/modules/_kernel.scss b/home/programs/graphical/bars/eww/config/css/modules/_kernel.scss new file mode 100644 index 0000000..652a60d --- /dev/null +++ b/home/programs/graphical/bars/eww/config/css/modules/_kernel.scss @@ -0,0 +1,3 @@ +.kernel { + .icon { color: $lavender; } +} diff --git a/home/programs/graphical/bars/eww/config/css/modules/_memory.scss b/home/programs/graphical/bars/eww/config/css/modules/_memory.scss new file mode 100644 index 0000000..37d8432 --- /dev/null +++ b/home/programs/graphical/bars/eww/config/css/modules/_memory.scss @@ -0,0 +1,3 @@ +.memory { + .icon { color: $maroon; } +} diff --git a/home/programs/graphical/bars/eww/config/css/modules/_uptime.scss b/home/programs/graphical/bars/eww/config/css/modules/_uptime.scss new file mode 100644 index 0000000..63b1353 --- /dev/null +++ b/home/programs/graphical/bars/eww/config/css/modules/_uptime.scss @@ -0,0 +1,3 @@ +.uptime { + .icon { color: $green; } +} diff --git a/home/programs/graphical/bars/eww/config/css/modules/_volume.scss b/home/programs/graphical/bars/eww/config/css/modules/_volume.scss new file mode 100644 index 0000000..7a94630 --- /dev/null +++ b/home/programs/graphical/bars/eww/config/css/modules/_volume.scss @@ -0,0 +1,9 @@ +.volume { + .icon { color: $peach; } + .speaker { + .icon { + margin-left: 8px; + margin-right: 5px; + } + } +} diff --git a/home/programs/graphical/bars/eww/config/css/modules/_window_name.scss b/home/programs/graphical/bars/eww/config/css/modules/_window_name.scss new file mode 100644 index 0000000..edf0d3e --- /dev/null +++ b/home/programs/graphical/bars/eww/config/css/modules/_window_name.scss @@ -0,0 +1,4 @@ +.window_name { + font-family: "JetBrains Mono", "Font Awesome 6 Free", sans-serif; + font-size: 1.4rem; +} diff --git a/home/programs/graphical/bars/eww/config/css/modules/_workspaces.scss b/home/programs/graphical/bars/eww/config/css/modules/_workspaces.scss new file mode 100644 index 0000000..e97b513 --- /dev/null +++ b/home/programs/graphical/bars/eww/config/css/modules/_workspaces.scss @@ -0,0 +1,28 @@ +.workspaces { + background-color: $bg1-a; + border-radius: 25px; + + .icon, .icon label { + font-family: "Material Symbols Outlined"; + font-size: 1.15rem; + } + + .value { + margin: 0 9px; + } + + .focused { + // text-decoration: underline; + // text-decoration-color: red; + // text-decoration-style: double; + color: $fg; + } + + .active { + color: #bbb; + } + + .inactive { + color: #555; + } +} diff --git a/home/programs/graphical/bars/eww/config/css/windows/_calendar.scss b/home/programs/graphical/bars/eww/config/css/windows/_calendar.scss new file mode 100644 index 0000000..2aadfa3 --- /dev/null +++ b/home/programs/graphical/bars/eww/config/css/windows/_calendar.scss @@ -0,0 +1,32 @@ +.calendar-win { + @include window; + background-color: $bg; + border: 1px solid $border; + color: $fg; + padding: .2em; +} + +calendar { + padding: 5px; + + :selected { + color: $mauve; + } + + .header { + color: $subtext1; + } + + .highlight { + color: $maroon; + font-weight: bold; + } + + .button { + color: $sapphire; + } + + :indeterminate { + color: $overlay0; + } +} diff --git a/home/programs/graphical/bars/eww/config/css/windows/_radio_menu.scss b/home/programs/graphical/bars/eww/config/css/windows/_radio_menu.scss new file mode 100644 index 0000000..e6d0e48 --- /dev/null +++ b/home/programs/graphical/bars/eww/config/css/windows/_radio_menu.scss @@ -0,0 +1,46 @@ +.radio-menu-box { + @include window; + background-color: $bg; + border: 1px solid $border; + color: $text; + font-family: "Jost *", "JetBrains Mono", "Font Awesome 6 Free", sans-serif; + + .icon, .icon label { + font-family: "Material Symbols Outlined"; + font-size: 1.15rem; + } + + .text-row { + margin: 1rem 1.5rem 0; + + .title { font-size: 1.2rem; } + } + + .element-row { + margin: .5rem .7rem; + + label { + font-size: 1rem; + margin: 0 .1rem; + } + } + + .element { + @include rounding; + background-color: $surface0; + margin: .3rem; + + button { + @include rounding; + padding: .5rem; + + label { + font-size: 1.5rem; + } + + &:hover { + background-color: $overlay0; + } + } + } +} diff --git a/home/programs/graphical/bars/eww/config/eww.scss b/home/programs/graphical/bars/eww/config/eww.scss new file mode 100644 index 0000000..7854a7b --- /dev/null +++ b/home/programs/graphical/bars/eww/config/eww.scss @@ -0,0 +1,114 @@ +@import "css/colors"; + +@mixin rounding { + border-radius: 16px; +} + +@mixin window { + border: 1px solid $border; + box-shadow: 0 2px 3px $shadow; + margin: 5px 5px 10px; + @include rounding; +} + +* { + all: unset; + transition: 200ms ease; +} + +@import "css/windows/calendar"; +@import "css/windows/radio_menu"; +@import "css/modules/clock"; +@import "css/modules/volume"; +@import "css/modules/bitcoin"; +@import "css/modules/cpu"; +@import "css/modules/memory"; +@import "css/modules/uptime"; +@import "css/modules/kernel"; +@import "css/modules/battery"; +@import "css/modules/workspaces"; +@import "css/modules/gammarelay"; +@import "css/modules/window_name"; + +.bar { + background-color: $bg-a; + color: $fg; + + font-family: "JetBrains Mono", "Jost *", sans-serif; + + label { + font-size: 14px; + } + + // TODO: Use percentages (for some reason it fails now) + min-width: 1900px; +} + +tooltip { + background: $bg; + border: 1px solid $border; + border-radius: 8px; + + label { + font-size: 1rem; + } +} + +.icon, +.icon label { + font-family: "Font Awesome 6 Free", "Material Symbols Outlined"; +} + +.module { + margin: 0 5px; +} + +.separ { + color: $surface0; + font-size: 1.5rem; + padding-bottom: 2px; +} + +scale trough { + background-color: $bg1-a; + border-radius: 24px; + margin: 0 1rem; + min-height: 10px; + min-width: 70px; +} + +.tray { + margin-right: 12px; +} + +menu { + background: $bg1; + border-bottom-left-radius: 12px; + border-bottom-right-radius: 12px; + border: 2px solid rgba($crust, 0.5); + padding: 0.5rem 0; +} + +menu menu { + border-top-left-radius: 12px; + border-top-right-radius: 12px; +} + +menu > menuitem { + padding: 0.4em 1rem; + background: transparent; + transition: 0.2s ease background; +} + +menu > menuitem:hover { + background: rgba($overlay1, 0.4); +} + +menu > menuitem check:checked ~ label { + color: $bg1-a; + font-weight: 600; +} + +menubar > menuitem { + margin-left: 0.6rem; +} diff --git a/home/programs/graphical/bars/eww/config/eww.yuck b/home/programs/graphical/bars/eww/config/eww.yuck new file mode 100644 index 0000000..ff4aef7 --- /dev/null +++ b/home/programs/graphical/bars/eww/config/eww.yuck @@ -0,0 +1,94 @@ +(defvar terminal "kitty -e") + +(include "./modules/variables.yuck") + +(include "./modules/clock.yuck") +(include "./modules/volume.yuck") +(include "./modules/bitcoin.yuck") +(include "./modules/cpu.yuck") +(include "./modules/memory.yuck") +(include "./modules/uptime.yuck") +; (include "./modules/kernel.yuck") +(include "./modules/battery.yuck") +(include "./modules/window_name.yuck") +(include "./modules/workspaces.yuck") +(include "./modules/gammarelay.yuck") + +(include "./windows/calendar.yuck") +(include "./windows/radio-menu.yuck") + +(defwidget sep [] + (label :class "separ module" :text "|")) + +(defwidget left [] + (box + :space-evenly false + :halign "start" + (gammarelay_module) + (sep) + (window_name_module) + )) + +(defwidget right [] + (box + :space-evenly false + :halign "end" + ; (kernel_module) + ; (sep) + (volume_module) + (sep) + (battery_module) + (sep) + (bitcoin_module) + (sep) + (cpu_module) + (sep) + (memory_module) + (sep) + ; (uptime_module) + ; (sep) + (clock_module) + (sep) + (systray + :pack-direction "left" + :class "module tray" + ) + )) + +(defwidget center [] + (box + :space-evenly false + :halign "center" + (workspaces_module) + )) + +(defwidget bar [] + (centerbox + :class "bar" + :orientation "horizontal" + (left) + (center) + (right))) + +(defwindow bar0 + :monitor 0 + :geometry (geometry :x "0%" + :y "0%" + :width: "100%" + :height "32px" + :anchor "top center") + :stacking "fg" + :exclusive true + (bar)) + + +(defwindow bar1 + :monitor 1 + :geometry (geometry :x "0%" + :y "0%" + :width: "100%" + :height "32px" + :anchor "top center") + :stacking "fg" + :exclusive true + (bar)) diff --git a/home/programs/graphical/bars/eww/config/modules/battery.yuck b/home/programs/graphical/bars/eww/config/modules/battery.yuck new file mode 100644 index 0000000..b0f033e --- /dev/null +++ b/home/programs/graphical/bars/eww/config/modules/battery.yuck @@ -0,0 +1,24 @@ +(defwidget battery_module [] + (eventbox + :class "module battery" + + (box + :class {battery.critical ? "critical" : battery.full ? "full" : "normal"} + (box + :space-evenly false + :class {battery.plugged ? "plugged" : "unplugged" } + + (box + :class "icon" + :space-evenly false + + (label + :class "extra" + :text {battery.extra_icon}) + (label + :text {battery.capacity_icon})) + (label + :class "value" + :text "${battery.percent}%" + ) + )))) diff --git a/home/programs/graphical/bars/eww/config/modules/bitcoin.yuck b/home/programs/graphical/bars/eww/config/modules/bitcoin.yuck new file mode 100644 index 0000000..963ef6c --- /dev/null +++ b/home/programs/graphical/bars/eww/config/modules/bitcoin.yuck @@ -0,0 +1,14 @@ +(defwidget bitcoin_module [] + (eventbox + :onclick "~/.local/bin/scripts/cli/bitcoin | xargs -I_ ${EWW_CMD} update bitcoin=_" + :class "module bitcoin" + + (box + :space-evenly false + + (label + :class "icon" + :text "") + (label :text {bitcoin})) + ) +) diff --git a/home/programs/graphical/bars/eww/config/modules/clock.yuck b/home/programs/graphical/bars/eww/config/modules/clock.yuck new file mode 100644 index 0000000..5217101 --- /dev/null +++ b/home/programs/graphical/bars/eww/config/modules/clock.yuck @@ -0,0 +1,18 @@ +(defwidget clock_module [] + + (eventbox + :tooltip {time.day} + :class "module clock" + ;; :onclick "${EWW_CMD} open --toggle calendar" + + (box + :space-evenly false + + (label + :class "icon" + :text "") + (label + :class "value" + :text "${time.date} ${time.hour}:${time.minute}") + ))) + diff --git a/home/programs/graphical/bars/eww/config/modules/cpu.yuck b/home/programs/graphical/bars/eww/config/modules/cpu.yuck new file mode 100644 index 0000000..940ffb3 --- /dev/null +++ b/home/programs/graphical/bars/eww/config/modules/cpu.yuck @@ -0,0 +1,15 @@ +(defwidget cpu_module [] + (eventbox + :class "module cpu" + + (box + :space-evenly false + + (label + :class "icon" + :text " ") + (label + :class "value" + :text "${round(EWW_CPU.avg,2)}%" + ) + ))) diff --git a/home/programs/graphical/bars/eww/config/modules/gammarelay.yuck b/home/programs/graphical/bars/eww/config/modules/gammarelay.yuck new file mode 100644 index 0000000..513d50a --- /dev/null +++ b/home/programs/graphical/bars/eww/config/modules/gammarelay.yuck @@ -0,0 +1,41 @@ +(defwidget gammarelay_module [] + (box + :class "module gammarelay" + + (eventbox + :onscroll "scripts/gammarelay temperature scroll {}" + :onclick "scripts/gammarelay temperature set toggle" + :onrightclick "scripts/gammarelay temperature set off" + :tooltip "${temperature} K" + :class "temperature" + (box + (label + :class "icon" + :text "") + )) + + (eventbox + :onscroll "scripts/gammarelay brightness scroll {}" + :onclick "scripts/gammarelay brightness set toggle" + :onrightclick "scripts/gammarelay brightness set off" + :tooltip "${brightness}%" + :class "brightness" + (box + (label + :class "icon" + :text "") + )) + + ; (eventbox + ; :onscroll "scripts/gammarelay gamma scroll {}" + ; :onclick "scripts/gammarelay gamma set toggle" + ; :onrightclick "scripts/gammarelay gamma set off" + ; :tooltip "${gamma}%" + ; :class "gamma" + ; :valign "top" + ; (box + ; (label + ; :class "icon" + ; :text "γ") + ; )) + )) diff --git a/home/programs/graphical/bars/eww/config/modules/kernel.yuck b/home/programs/graphical/bars/eww/config/modules/kernel.yuck new file mode 100644 index 0000000..c784049 --- /dev/null +++ b/home/programs/graphical/bars/eww/config/modules/kernel.yuck @@ -0,0 +1,15 @@ +(defwidget kernel_module [] + (eventbox + :class "module kernel" + + (box + :space-evenly false + + (label + :class "icon" + :text " ") + (label + :class "value" + :text {kernel} + ) + ))) diff --git a/home/programs/graphical/bars/eww/config/modules/memory.yuck b/home/programs/graphical/bars/eww/config/modules/memory.yuck new file mode 100644 index 0000000..43bcca4 --- /dev/null +++ b/home/programs/graphical/bars/eww/config/modules/memory.yuck @@ -0,0 +1,15 @@ +(defwidget memory_module [] + (eventbox + :class "module memory" + + (box + :space-evenly false + + (label + :class "icon" + :text " ") + (label + :class "value" + :text "${round(EWW_RAM.used_mem / 1000000000,1)}G: ${round(EWW_RAM.used_mem_perc,0)}%" + ) + ))) diff --git a/home/programs/graphical/bars/eww/config/modules/nightlight.yuck b/home/programs/graphical/bars/eww/config/modules/nightlight.yuck new file mode 100644 index 0000000..fd2f2fe --- /dev/null +++ b/home/programs/graphical/bars/eww/config/modules/nightlight.yuck @@ -0,0 +1,3 @@ +(deflisten nightlight + :initial `{"running": false,"temperature": 0}` + `scripts/nightlight --state`) diff --git a/home/programs/graphical/bars/eww/config/modules/uptime.yuck b/home/programs/graphical/bars/eww/config/modules/uptime.yuck new file mode 100644 index 0000000..972b3e1 --- /dev/null +++ b/home/programs/graphical/bars/eww/config/modules/uptime.yuck @@ -0,0 +1,15 @@ +(defwidget uptime_module [] + (eventbox + :class "module uptime" + + (box + :space-evenly false + + (label + :class "icon" + :text " ") + (label + :class "value" + :text {uptime} + ) + ))) diff --git a/home/programs/graphical/bars/eww/config/modules/variables.yuck b/home/programs/graphical/bars/eww/config/modules/variables.yuck new file mode 100644 index 0000000..3dbc09e --- /dev/null +++ b/home/programs/graphical/bars/eww/config/modules/variables.yuck @@ -0,0 +1,60 @@ +(defpoll time + :interval "5s" + :initial '{"date": "01 Jan", "hour": "00", "minute": "00", "day": "Monday"}' + `date +'{"date": "%d %b", "hour": "%H", "minute": "%M", "day": "%A"}'`) + +(deflisten volume + :initial '{ "speaker_vol": "100", "speaker_mute": false, "speaker_icon": "", "microphone_mute": false, "microphone_vol": "100", "microphone_icon": "" }' + `scripts/volume loop`) + +(deflisten window_name + :initial `{"class":"","name":"","formatted_name":""}` + `scripts/window_name`) + +(deflisten workspaces + :initial `[{"id": 1,"name": "N/A","monitor": "N/A","windows": 1,"hasfullscreen": false,"lastwindow": "N/A","lastwindowtitle": "N/A","format_name": "N/A","active": true}]` + `scripts/workspaces --loop`) + +(defpoll battery + :interval "1s" + :initial '{"percent":"0","plugged":"false","status":"N/A","capacity_icon":"","extra_icon":"","manufacturer":"N/A","model_name":"N/A","technology":"N/A","energy_now":"0","enerfy_full":"0","enerfy_full_design":"0","cycle_count":"0","critical":"false","full":"false"}' + `scripts/battery`) + +(defpoll uptime + :interval "1m" + :initial 'N/A' + `uptime -p | sed \\ + -e 's/^up //' \\ + -e 's/ years\\?,\\?/y/' \\ + -e 's/ months\\?,\\?/m/' \\ + -e 's/ weeks\\?,\\?/w/' \\ + -e 's/ days\\?,\\?/d/' \\ + -e 's/ hours\\?,\\?/h/' \\ + -e 's/ minutes\\?,\\?/m/' \\ + -e 's/ seconds\\?,\\?/s/' \\ + | cut -d' ' -f-2`) + +(defpoll bitcoin + :interval "5m" + :initial "$N/A" + `~/.local/bin/scripts/cli/bitcoin`) + +; TODO: Figure out how to store this one-time +(defpoll kernel + :interval "10000h" + :initial 'N/A' + ; `uname -r | sed -r 's/(.+)-arch(.+)/\\1/'` + `uname -r`) + +(deflisten temperature `scripts/gammarelay temperature watch`) +(deflisten brightness `scripts/gammarelay brightness watch`) +(deflisten gamma `scripts/gammarelay gamma watch`) + +(defpoll net + :interval "3s" + :initial '{"essid":"N/A","icon":"󱛇","state":"unknown","signal":"0"}' + `scripts/net status`) +(defpoll bluetooth + :interval "3s" + :initial '{"icon":"󰂲","status":"unknown","name":"","mac":"","battery":""}' + `scripts/bluetooth status`) diff --git a/home/programs/graphical/bars/eww/config/modules/volume.yuck b/home/programs/graphical/bars/eww/config/modules/volume.yuck new file mode 100644 index 0000000..1f689c9 --- /dev/null +++ b/home/programs/graphical/bars/eww/config/modules/volume.yuck @@ -0,0 +1,40 @@ +(defvar mic_rev false) + +(defwidget volume_module [] + (box + :class "module volume" + :space-evenly false + + (eventbox + :onscroll "scripts/volume setvol SOURCE 0.5 {}" + :onclick "scripts/volume togglemute SOURCE" + :onrightclick "${terminal} pulsemixer &" + :onhover "${EWW_CMD} update mic_rev=true" + :onhoverlost "${EWW_CMD} update mic_rev=false" + :class "microphone" + (box + (label + :class "icon" + :text {volume.microphone_icon}) + (label + :visible {mic_rev && !volume.microphone_mute} + :class "value" + :text "${volume.microphone_vol}%") + )) + + (eventbox + :onscroll "scripts/volume setvol SINK 0.5 {}" + :onclick "scripts/volume togglemute SINK" + :onrightclick "${terminal} pulsemixer &" + :class "speaker" + (box + (label + :class "icon" + :text {volume.speaker_icon}) + (label + :visible {!volume.speaker_mute} + :class "value" + :text "${volume.speaker_vol}%") + )) + + )) diff --git a/home/programs/graphical/bars/eww/config/modules/window_name.yuck b/home/programs/graphical/bars/eww/config/modules/window_name.yuck new file mode 100644 index 0000000..16ace74 --- /dev/null +++ b/home/programs/graphical/bars/eww/config/modules/window_name.yuck @@ -0,0 +1,11 @@ +; Consider making the window name clickable, opening up a full window that's showing +; the selected window details (class, unformatted name, and perhaps even more, like +; xwayland status, ...) +(defwidget window_name_module [] + (box + :class "module window_name" + + (label + :class "value" + :text "${window_name.formatted_name}") + )) diff --git a/home/programs/graphical/bars/eww/config/modules/workspaces.yuck b/home/programs/graphical/bars/eww/config/modules/workspaces.yuck new file mode 100644 index 0000000..3f4feb1 --- /dev/null +++ b/home/programs/graphical/bars/eww/config/modules/workspaces.yuck @@ -0,0 +1,21 @@ +; (defwidget sep [] +; (label :class "separ module" :text "|")) + + +; Consider making the window name clickable, opening up a full window that's showing +; the selected window details (class, unformatted name, and perhaps even more, like +; xwayland status, ...) +(defwidget workspaces_module [] + (box + :class "module workspaces" + + (for workspace in workspaces + (eventbox + :class {workspace.active ? 'focused' : workspace.windows > 0 ? 'active' : 'inactive'} + :onclick `scripts/workspaces --switch ${workspace.id}` + + (label + :class "value icon" + :text {workspace.format_name})) + ) + )) diff --git a/home/programs/graphical/bars/eww/config/scripts/.flake8 b/home/programs/graphical/bars/eww/config/scripts/.flake8 new file mode 100644 index 0000000..ce90fbd --- /dev/null +++ b/home/programs/graphical/bars/eww/config/scripts/.flake8 @@ -0,0 +1,11 @@ +[flake8] +max-line-length=119 +extend-ignore=E203 +extend-select=B902,B904 +exclude=.venv,.git,.cache +ignore= + ANN002, # *args annotation + ANN003, # **kwargs annotation + ANN101, # self param annotation + ANN102, # cls param annotation + ANN204, # return type annotation for special methods diff --git a/home/programs/graphical/bars/eww/config/scripts/battery b/home/programs/graphical/bars/eww/config/scripts/battery new file mode 100755 index 0000000..86d1152 --- /dev/null +++ b/home/programs/graphical/bars/eww/config/scripts/battery @@ -0,0 +1,85 @@ +#!/usr/bin/env bash + +# shellcheck source=include +source "./scripts/include" + +# $BATTERY and $ADAPTER env vars can be set manually, being the names of the +# devices (in /sys/class/power_supply/) i.e. BATTERY=BAT0 ADAPTER=ADP0 +# or, if left unset, they will be automatically picked. + +CAPACITY_ICONS=("󰁺" "󰁻" "󰁼" "󰁽" "󰁾" "󰁿" "󰂀" "󰂁" "󰂂" "󰁹") +CHARGING_ICON="" +DISCHARGING_ICON="" +FULL_ICON="" # Plugged in, but no longer charging (fully charged) +CRITICAL_ICON="" +CRITICAL_PERCENTAGE=15 + +if [ -z "$BATTERY" ]; then + # shellcheck disable=SC2010 + BATTERY="$(\ls -t /sys/class/power_supply | grep "BAT" | head -n 1)" +fi + +if [ -z "$ADAPTER" ]; then + # shellcheck disable=SC2010 + ADAPTER="$(\ls -t /sys/class/power_supply | grep -E "ADP|AC" | head -n 1)" +fi + +get_bat_info() { + cat /sys/class/power_supply/"$BATTERY"/"$1" +} + +get_adp_info() { + cat /sys/class/power_supply/"$ADAPTER"/"$1" +} + +manufacturer="$(get_bat_info manufacturer)" +model_name="$(get_bat_info model_name)" +technology="$(get_bat_info technology)" +energy_now="$(get_bat_info energy_now)" +energy_full="$(get_bat_info energy_full)" +energy_full_design="$(get_bat_info energy_full_design)" +cycle_count="$(get_bat_info cycle_count)" + +capacity="$(get_bat_info capacity)" +status="$(get_bat_info status)" +[ "$(get_adp_info online)" -eq 1 ] && adp_connected="true" || adp_connected="false" + +# Quick overrides to showcase how battery works +# capacity=100 +# adp_connected="true" +# status="Charging" +# status="Not charging" +# status="Discharging" + +full="false" +capacity_icon="$(pick_icon "$capacity" 0 100 "${CAPACITY_ICONS[@]}")" + +if [ "$status" = "Not charging" ] || [ "$status" = "Full" ] && [ "$adp_connected" = "true" ]; then + extra_icon="$FULL_ICON" + full="true" +elif [ "$status" = "Discharging" ] && [ "$capacity" -le "$CRITICAL_PERCENTAGE" ]; then + extra_icon="$CRITICAL_ICON" +elif [ "$status" = "Discharging" ]; then + extra_icon="$DISCHARGING_ICON" +elif [ "$status" = "Charging" ]; then + extra_icon="$CHARGING_ICON" +fi + +[ "$capacity" -le "$CRITICAL_PERCENTAGE" ] && critical="true" || critical="false" + +jq -n -c --monochrome-output \ + --arg percent "$capacity" \ + --arg plugged "$adp_connected" \ + --arg status "$status" \ + --arg capacity_icon "$capacity_icon" \ + --arg extra_icon "$extra_icon" \ + --arg manufacturer "$manufacturer" \ + --arg model_name "$model_name" \ + --arg technology "$technology" \ + --arg energy_now "$energy_now" \ + --arg energy_full "$energy_full" \ + --arg energy_full_design "$energy_full_design" \ + --arg cycle_count "$cycle_count" \ + --arg critical "$critical" \ + --arg full "$full" \ + '$ARGS.named' diff --git a/home/programs/graphical/bars/eww/config/scripts/bluetooth b/home/programs/graphical/bars/eww/config/scripts/bluetooth new file mode 100755 index 0000000..240c4ca --- /dev/null +++ b/home/programs/graphical/bars/eww/config/scripts/bluetooth @@ -0,0 +1,85 @@ +#!/usr/bin/env bash + +ICON_IDLE="󰂯" +ICON_CONNECTED="󰂱" +ICON_OFF="󰂲" + +toggle() { + status=$(rfkill -J | jq -r '.rfkilldevices[] | select(.type == "bluetooth") | .soft' | head -1) + + if [ "$status" = "unblocked" ]; then + rfkill block bluetooth + else + rfkill unblock bluetooth + if ! systemctl -q is-active bluetooth.service; then + # This will use polkit for privillege elevation + systemctl start bluetooth + fi + fi +} + +get_report() { + status=$(rfkill -J | jq -r '.rfkilldevices[] | select(.type == "bluetooth") | .soft' | head -1) + if [ "$status" = "blocked" ] || ! systemctl -q is-active bluetooth.service || ! command -v bluetoothctl >/dev/null 2>&1; then + jq -n -c --monochrome-output \ + --arg icon "$ICON_OFF" \ + --arg status "unknown" \ + --arg name "" \ + --arg mac "" \ + --arg battery "" \ + '$ARGS.named' + + return + fi + + status="$(bluetoothctl show)" + + powered="$(echo "$status" | grep Powered | cut -d' ' -f 2-)" + if [ "$powered" != "yes" ]; then + jq -n -c --monochrome-output \ + --arg icon "$ICON_OFF" \ + --arg status "unpowered" \ + --arg name "" \ + --arg mac "" \ + --arg battery "" \ + '$ARGS.named' + + return + fi + + status="$(bluetoothctl info)" + if [ "$status" == "Missing device address argument" ]; then + jq -n -c --monochrome-output \ + --arg icon "$ICON_IDLE" \ + --arg status "idle" \ + --arg name "" \ + --arg mac "" \ + --arg battery "" \ + '$ARGS.named' + + return + fi + + name="$(echo "$status" | grep Name | cut -d' ' -f 2-)" + mac="$(echo "$status" | head -1 | awk '{print $2}' | tr ':' '_')" + + if [ "$(echo "$status" | grep Percentage)" != "" ] && command -v upower >/dev/null 2>&1; then + battery="$(upower -i /org/freedesktop/UPower/devices/headset_dev_"$mac" | grep percentage | awk '{print $2}' | cut -f 1 -d '%')%" + else + battery="" + fi + + jq -n -c --monochrome-output \ + --arg icon "$ICON_CONNECTED" \ + --arg status "connected" \ + --arg name "$name" \ + --arg mac "$mac" \ + --arg battery "$battery" \ + '$ARGS.named' +} + +case "$1" in + "status") get_report ;; + "toggle") toggle ;; + *) >&2 echo "Invalid usage, argument '$1' not recognized."; exit 1 ;; +esac diff --git a/home/programs/graphical/bars/eww/config/scripts/gammarelay b/home/programs/graphical/bars/eww/config/scripts/gammarelay new file mode 100755 index 0000000..039dbea --- /dev/null +++ b/home/programs/graphical/bars/eww/config/scripts/gammarelay @@ -0,0 +1,82 @@ +#!/bin/env bash + +if [ "$1" = "temperature" ]; then + watch_cmd="{t}" + update_cmd="UpdateTemperature" + update_signature="n" + set_cmd="Temperature" + set_signature="q" + default_val=6500 + click_val=4500 + scroll_change=100 + cmp_op="<" + +elif [ "$1" = "brightness" ]; then + watch_cmd="{bp}" + update_cmd="UpdateBrightness" + update_signature="d" + set_cmd="Brightness" + set_signature="d" + default_val=1 + click_val=0.8 + scroll_change=0.02 + cmp_op="<" + +elif [ "$1" = "gamma" ]; then + watch_cmd="{g}" + update_cmd="UpdateGamma" + update_signature="d" + set_cmd="Gamma" + set_signature="d" + default_val=1 + click_val=1.1 + scroll_change=0.02 + cmp_op=">" + +else + >&2 echo "Invalid option, first argument must be one of: temperature, brightness, gamma" + exit 1 +fi + +if [ "$2" = "watch" ]; then + exec wl-gammarelay-rs watch "$watch_cmd" + +elif [ "$2" = "get" ]; then + exec busctl --user get-property rs.wl-gammarelay / rs.wl.gammarelay "$set_cmd" | cut -d' ' -f2 + +elif [ "$2" = "scroll" ]; then + if [ "$3" = "up" ]; then + sign="+" + elif [ "$3" = "down" ]; then + sign="-" + else + >&2 echo "Invalid sign, second argument must be one of: up, down" + exit 1 + fi + + exec busctl --user -- call rs.wl-gammarelay / rs.wl.gammarelay "$update_cmd" "$update_signature" ${sign}${scroll_change} + +elif [ "$2" = "set" ]; then + mode="$3" + if [ "$mode" = "toggle" ]; then + cur_val="$(busctl --user get-property rs.wl-gammarelay / rs.wl.gammarelay "$set_cmd" | cut -d' ' -f2)" + if [ "$(echo "$cur_val $cmp_op $default_val" | bc -l)" = "1" ]; then + mode="off" + else + mode="on" + fi + fi + + if [ "$mode" = "on" ]; then + exec busctl --user -- set-property rs.wl-gammarelay / rs.wl.gammarelay "$set_cmd" "$set_signature" "$click_val" + elif [ "$mode" = "off" ]; then + exec busctl --user -- set-property rs.wl-gammarelay / rs.wl.gammarelay "$set_cmd" "$set_signature" "$default_val" + else + >&2 echo "Invalid mode, third argument, must be one of: toggle, on, off" + exit 1 + fi + +else + >&2 echo "Invalid operation, second argument must be one of: watch, scroll, set" + exit 1 +fi diff --git a/home/programs/graphical/bars/eww/config/scripts/include b/home/programs/graphical/bars/eww/config/scripts/include new file mode 100755 index 0000000..49e58e9 --- /dev/null +++ b/home/programs/graphical/bars/eww/config/scripts/include @@ -0,0 +1,50 @@ +#!/bin/env bash + +# $1: Current number +# $2: Range minimum +# $3: Range maximum +# $4-: Icons as individual arguments +pick_icon() { + cur="$1" + min="$2" + max="$3" + shift 3 + icons=("$@") + + index="$(echo "($cur-$min)/(($max-$min)/${#icons[@]})" | bc)" + + # Print the picked icon, handling overflows/underflows, i.e. if our index is <0 or >len(icons) + if [ "$index" -ge "${#icons[@]}" ]; then + index=-1 + elif [ "$index" -lt 0 ]; then + index=0 + fi + + echo "${icons[index]}" +} + +# Will block and listen to the hyprland socket messages and output them +# Generally used like: hyprland_ipc | while read line; do handle $line; done +# Read for output format and available events +# Note: requires openbsd version of netcat. +# $1 - Optional event to listen for (no event filtering will be done if not provided) +hyprland_ipc() { + if [ -z "$HYPRLAND_INSTANCE_SIGNATURE" ]; then + >&2 echo "Hyprland is not running, IPC not available" + exit 1 + fi + + SOCKET_PATH="${XDG_RUNTIME_DIR:-/run/user/$UID}/hypr/$HYPRLAND_INSTANCE_SIGNATURE/.socket2.sock" + #SOCKET_PATH="/tmp/hypr/$HYPRLAND_INSTANCE_SIGNATURE/.socket2.sock" + + if [ -z "$1" ]; then + nc -U "$SOCKET_PATH" | while read -r test; do + echo "$test" + done + else + nc -U "$SOCKET_PATH" | while read -r test; do + # shellcheck disable=SC2016 + echo "$test" | grep --line-buffered -E "^$1>>" | stdbuf -oL awk -F '>>' '{print $2}' + done + fi +} diff --git a/home/programs/graphical/bars/eww/config/scripts/net b/home/programs/graphical/bars/eww/config/scripts/net new file mode 100755 index 0000000..7d84697 --- /dev/null +++ b/home/programs/graphical/bars/eww/config/scripts/net @@ -0,0 +1,52 @@ +#!/usr/bin/env bash + +# shellcheck source=include +source "./scripts/include" + +STRENGTH_ICONS=("󰤫" "󰤯" "󰤟" "󰤢" "󰤥" "󰤨") +DISCONNECTED_ICON="󰤮" +WIFI_OFF="󰖪" + +toggle() { + status=$(rfkill -J | jq -r '.rfkilldevices[] | select(.type == "wlan") | .soft' | head -1) + + if [ "$status" = "unblocked" ]; then + rfkill block wlan + else + rfkill unblock wlan + fi +} + +get_report() { + connection_details="$(nmcli -t -f NAME,TYPE,DEVICE connection show --active | grep wireless | head -n1)" + essid="$(echo $connection_details | cut -d':' -f1)" + device="$(echo $connection_details | cut -d':' -f3)" + if [ -n "$device" ]; then + state="$(nmcli -t -f DEVICE,STATE device status | grep "$device" | head -n1 | cut -d':' -f2)" + signal="$(nmcli -t -f in-use,signal dev wifi | grep "\*" | cut -d':' -f2)" + else + state="unknown" + signal="0" + fi + + if [ "$state" = "disconnected" ] ; then + icon="$DISCONNECTED_ICON" + elif [ "$state" = "unknown" ]; then + icon="$WIFI_OFF" + else + icon="$(pick_icon "$signal" 0 100 "${STRENGTH_ICONS[@]}")" + fi + + jq -n -c --monochrome-output \ + --arg essid "$essid" \ + --arg icon "$icon" \ + --arg state "$state" \ + --arg signal "$signal" \ + '$ARGS.named' +} + +case "$1" in + "toggle") toggle ;; + "status") get_report ;; + *) >&2 echo "Invalid usage, argument '$1' not recognized."; exit 1 ;; +esac diff --git a/home/programs/graphical/bars/eww/config/scripts/nightlight b/home/programs/graphical/bars/eww/config/scripts/nightlight new file mode 100644 index 0000000..71522f0 --- /dev/null +++ b/home/programs/graphical/bars/eww/config/scripts/nightlight @@ -0,0 +1,12 @@ +#!/bin/env bash + +# shellcheck source=include +source "./scripts/include" + +# Consider usning a file as a flag for whether nightlight is on or off +# as we might be in transition state and just killing the program might +# not be enough. + +if [ "$1" = "toggle" ]; then + gammastep -x +fi diff --git a/home/programs/graphical/bars/eww/config/scripts/pyproject.toml b/home/programs/graphical/bars/eww/config/scripts/pyproject.toml new file mode 100644 index 0000000..e3a2d4f --- /dev/null +++ b/home/programs/graphical/bars/eww/config/scripts/pyproject.toml @@ -0,0 +1,12 @@ +[tool.black] +line-length = 119 +extend-exclude = "^/.cache" + +[tool.isort] +profile = "black" +line_length = 119 +atomic = true +order_by_type = false +case_sensitive = true +combine_as_imports = true +skip = [".venv", ".git", ".cache"] diff --git a/home/programs/graphical/bars/eww/config/scripts/storage b/home/programs/graphical/bars/eww/config/scripts/storage new file mode 100755 index 0000000..f1ff4b4 --- /dev/null +++ b/home/programs/graphical/bars/eww/config/scripts/storage @@ -0,0 +1,35 @@ +#!/bin/env bash + +MOUNTPOINTS=("/" "/mnt/ext") + +data="$(df -H)" + +as_json() { + mountpoint="$1" + res="$2" + arr_res=($res) + + jq -n -c --monochrome-output \ + --arg mountpoint "$mountpoint" \ + --arg size "${arr_res[0]}" \ + --arg used "${arr_res[1]}" \ + --arg avail "${arr_res[2]}" \ + --arg percent "${arr_res[3]}" \ + '$ARGS.named' +} + +output_json="[]" +for mountpoint in "${MOUNTPOINTS[@]}"; do + res="$(echo "$data" | awk -v m="$mountpoint" '$6 == m {print $2 " " $3 " " $4 " " $5}')" + out="$(as_json "$mountpoint" "$res")" + + # echo "$output_json $out" | jq -c -s + + jq --argjson arr1 "$output_json" --argjson arr2 "[$out]" -n \ + '$arr1 + $arr2' + + # mount_data+=("$mountpoint" $res) + # echo "${mount_data[@]}" +done + +# echo "${mount_data[@]}" diff --git a/home/programs/graphical/bars/eww/config/scripts/temp b/home/programs/graphical/bars/eww/config/scripts/temp new file mode 100755 index 0000000..3857531 --- /dev/null +++ b/home/programs/graphical/bars/eww/config/scripts/temp @@ -0,0 +1,7 @@ +#!/bin/env bash + +# shellcheck source=include +source "./scripts/include" + +#hyprland_ipc "workspace|createworkspace|destroyworkspace|activewindow" +hyprland_ipc diff --git a/home/programs/graphical/bars/eww/config/scripts/volume b/home/programs/graphical/bars/eww/config/scripts/volume new file mode 100755 index 0000000..6003f79 --- /dev/null +++ b/home/programs/graphical/bars/eww/config/scripts/volume @@ -0,0 +1,112 @@ +#!/bin/env bash + +# Define some icons +SPEAKER_ICONS=("" "" "") +SPEAKER_MUTED_ICON="" +MIC_ICON="" +MIC_MUTED_ICON="" + +# Define some helper functions for getting/setting audio data using wireplumber (wpctl) + +# $1 can either be "SINK" (speaker) or "SOURCE" (microphone) +get_vol() { + wpctl get-volume "@DEFAULT_AUDIO_${1}@" | awk '{print int($2*100)}' +} + +# $1 can either be "SINK" (speaker) or "SOURCE" (microphone) +# #2 is the voulme (as percentage) to set the volume to +# $3 is optional, if set, it can be '+' or '-', which then adds/decreases volume, instead of setting +set_vol() { + wpctl set-volume "@DEFAULT_AUDIO_${1}@" "$(awk -v n="$2" 'BEGIN{print (n / 100)}')$3" +} + +# $1 can either be "SINK" (speaker) or "SOURCE" (microphone) +check_mute() { + wpctl get-volume "@DEFAULT_AUDIO_${1}@" | grep -i muted >/dev/null + echo $? +} + +# $1 can either be "SINK" (speaker) or "SOURCE" (microphone) +toggle_mute() { + wpctl set-mute "@DEFAULT_AUDIO_${1}@" toggle +} + +get_report() { + spkr_vol="$(get_vol "SINK")" + mic_vol="$(get_vol "SOURCE")" + + if [ "$(check_mute "SINK")" == "0" ]; then + spkr_mute="true" + spkr_icon="$SPEAKER_MUTED_ICON" + else + spkr_mute="false" + index="$(awk -v n="$spkr_vol" -v m="${#SPEAKER_ICONS[@]}" 'BEGIN{print int(n/(100/m))}')" + + # We might end up with an higher than the length of icons, if the volume is over 100% + # in this case, set the index to last icon + if [ "$index" -ge "${#SPEAKER_ICONS[@]}" ]; then + spkr_icon="${SPEAKER_ICONS[-1]}" + else + spkr_icon="${SPEAKER_ICONS[$index]}" + fi + fi + + if [ "$(check_mute "SOURCE")" = "0" ]; then + mic_mute="true" + mic_icon="$MIC_MUTED_ICON" + else + mic_mute="false" + mic_icon="$MIC_ICON" + fi + + echo "{ \"speaker_vol\": \"$spkr_vol\", \"speaker_mute\": $spkr_mute, \"speaker_icon\": \"$spkr_icon\", \"microphone_mute\": $mic_mute, \"microphone_vol\": \"$mic_vol\", \"microphone_icon\": \"$mic_icon\" }" +} + +# Continually run and report every volume change (into stdout) +loop() { + get_report + pactl subscribe | grep --line-buffered "change" | while read -r _; do + get_report + done +} + +case "$1" in +"loop") loop ;; + +"once") get_report ;; + +"togglemute") + if [ "$2" != "SOURCE" ] && [ "$2" != "SINK" ]; then + >&2 echo "Invalid usage, expected second argument to be 'SINK' or 'SOURCE', got '$2'" + exit 1 + fi + toggle_mute "$2" + ;; + +"setvol") + if [ "$2" != "SOURCE" ] && [ "$2" != "SINK" ]; then + >&2 echo "Invalid usage, expected second argument to be 'SINK' or 'SOURCE', got '$2'" + exit 1 + fi + + if [[ "$3" =~ ^[+-]?[0-9]*\.?[0-9]+$ ]]; then + case "$4" in + "") set_vol "$2" "$3" ;; + up | +) set_vol "$2" "$3" "+" ;; + down | -) set_vol "$2" "$3" "-" ;; + *) + >&2 echo "Invalid usage, expected fourth argument to be up/down or +/-, got '$4'" + exit 1 + ;; + esac + else + >&2 echo "Invalid usage, exepcted third argument to be a number, got '$3'" + exit 1 + fi + ;; + +*) + >&2 echo "Invalid usage, argument '$1' not recognized." + exit 1 + ;; +esac diff --git a/home/programs/graphical/bars/eww/config/scripts/window_name b/home/programs/graphical/bars/eww/config/scripts/window_name new file mode 100755 index 0000000..d32bff7 --- /dev/null +++ b/home/programs/graphical/bars/eww/config/scripts/window_name @@ -0,0 +1,6 @@ +#!/bin/env bash + +# shellcheck source=include +source "./scripts/include" + +hyprland_ipc "activewindow" | ./scripts/window_name.py diff --git a/home/programs/graphical/bars/eww/config/scripts/window_name.py b/home/programs/graphical/bars/eww/config/scripts/window_name.py new file mode 100755 index 0000000..acc5376 --- /dev/null +++ b/home/programs/graphical/bars/eww/config/scripts/window_name.py @@ -0,0 +1,112 @@ +#!/usr/bin/env python3 +"""This is a utility script for regex remaps on window names. + +This is done in python, because of the complex regex logic, which would be pretty hard to +recreate in bash. This python script is expected to be called from the bash script controlling +the window names. Window name and class are obtained from piped stdin, to prevent having to +needlessly keep restarting this program, which takes a while due to the interpreter starting +overhead. +""" +import json +import re +import sys +from typing import Iterator, Optional + + +class RemapRule: + __slots__ = ("name_pattern", "output_pattern", "class_pattern") + + def __init__( + self, + name_pattern: str, + output_pattern: str, + class_pattern: Optional[str] = None, + ): + self.name_pattern = re.compile(name_pattern) + self.output_pattern = output_pattern + self.class_pattern = re.compile(class_pattern) if class_pattern else None + + def apply(self, window_name: str, window_class: str) -> str: + """Returns new name after this remap rule was applied.""" + if self.class_pattern is not None: + if not self.class_pattern.fullmatch(window_class): + # Rule doesn't apply, class mismatch, return original name + return window_name + + res = self.name_pattern.fullmatch(window_name) + if not res: + # Rule doesn't apply, name mismatch, return original name + return window_name + + # NOTE: This is potentially unsafe, since output_pattern might be say {0.__class__}, etc. + # meaning this allows arbitrary attribute access, however it doesn't allow running functions + # here. That said, code could still end up being run if there's some descriptor defined, + # and generally I wouldn't trust this in production. However, this code is for my personal + # use here, and all of the output patterns are hard-coded in this file, so in this case, it's + # fine. But if you see this code and you'd like to use it in your production code, maybe don't + return self.output_pattern.format(*res.groups()) + + +# Rules will be applied in specified order +REMAP_RULES: list[RemapRule] = [ + RemapRule(r"", "", ""), + RemapRule(r"(.*) — Mozilla Firefox", " {}", "firefox"), + RemapRule(r"Mozilla Firefox", " Mozilla Firefox", "firefox"), + RemapRule(r"Alacritty", " Alacritty", "Alacritty"), + RemapRule( + r"zsh;#toggleterm#1 - \(term:\/\/(.+)\/\/(\d+):(.+)\) - N?VIM", + " Terminal: {0}", + ), + RemapRule(r"(.+) \+ \((.+)\) - N?VIM", " {0} ({1}) [MODIFIED]"), + RemapRule(r"(.+) \((.+)\) - N?VIM", " {0} ({1})"), + RemapRule(r"(?:\[\d+\] )?\*?WebCord - (.+)", " {}", "WebCord"), + RemapRule(r"(.+) - Discord", " {}", "discord"), + RemapRule(r"(?:\(\d+\) )?Discord \| (.+)", " {}", "vesktop"), + RemapRule(r"(.+) - mpv", " {}", "mpv"), + RemapRule(r"Stremio - (.+)", " Stremio - {}", r"(Stremio)|(com.stremio.stremio)"), + RemapRule(r"Spotify", " Spotify", "Spotify"), + RemapRule(r"pulsemixer", " Pulsemixer"), + RemapRule(r"(.*)", " {}", "Pcmanfm"), + RemapRule(r"(.*)", " {}", "pcmanfm-qt"), + # Needs to be last + RemapRule(r"(.*)", " {}", "kitty"), +] + +MAX_LENGTH = 65 + + +def iter_window() -> Iterator[tuple[str, str]]: + """Listen for the window parameters from stdin/pipe, yields (window_name, window_class).""" + for line in sys.stdin: + line = line.removesuffix("\n") + els = line.split(",", maxsplit=1) + if len(els) != 2: + raise ValueError(f"Expected 2 arguments from stdin line (name, class), but got {len(els)}") + yield els[1], els[0] + + +def main() -> None: + for window_name, window_class in iter_window(): + formatted_name = window_name + for remap_rule in REMAP_RULES: + new_name = remap_rule.apply(formatted_name, window_class) + if new_name != formatted_name: + formatted_name = new_name + break + + if len(formatted_name) > MAX_LENGTH: + formatted_name = formatted_name[: MAX_LENGTH - 3] + "..." + + ret = json.dumps( + { + "name": window_name, + "class": window_class, + "formatted_name": formatted_name, + } + ) + print(ret) + sys.stdout.flush() + + +if __name__ == "__main__": + main() diff --git a/home/programs/graphical/bars/eww/config/scripts/workspaces b/home/programs/graphical/bars/eww/config/scripts/workspaces new file mode 100755 index 0000000..992b7ab --- /dev/null +++ b/home/programs/graphical/bars/eww/config/scripts/workspaces @@ -0,0 +1,13 @@ +#!/bin/env bash + +# shellcheck source=include +source "./scripts/include" + +if [ "$1" = "--switch" ]; then + $HOME/.local/bin/scripts/gui/hyprland/swap-workspace "$2" >/dev/null + # hyprctl dispatch workspace "$2" >/dev/null +elif [ "$1" = "--loop" ]; then + hyprland_ipc "workspace|createworkspace|destroyworkspace" | ./scripts/workspaces.py "$@" +else + ./scripts/workspaces.py "$@" +fi diff --git a/home/programs/graphical/bars/eww/config/scripts/workspaces.py b/home/programs/graphical/bars/eww/config/scripts/workspaces.py new file mode 100755 index 0000000..4c31f4b --- /dev/null +++ b/home/programs/graphical/bars/eww/config/scripts/workspaces.py @@ -0,0 +1,188 @@ +#!/usr/bin/env python3 +import argparse +import json +import subprocess +import sys +from typing import TypedDict, TYPE_CHECKING + +if TYPE_CHECKING: + from _typeshed import SupportsRichComparison + + +class WorkspaceInfo(TypedDict): + id: int + name: str + monitor: str + windows: int + hasfullscreen: bool + lastwindow: str + lastwindowtitle: str + + +class ActiveWorkspaceInfo(TypedDict): + id: int + name: str + + +class MonitorInfo(TypedDict): + id: int + name: str + description: str + make: str + model: str + width: int + height: int + refreshRate: float + x: int + y: int + activeWorkspace: ActiveWorkspaceInfo + reserved: list[int] + scale: float + transform: int + focused: bool + dpmsStatus: bool + vrr: bool + + +class OutputWorkspaceInfo(WorkspaceInfo): + format_name: str + active: bool + monitor_id: int + + +# workspace id -> remapped name +REMAPS = { + 1: "󰞷", + 2: "󰈹", + 3: "󱕂", + 4: "󰭹", + 5: "󰝚", + 6: "󰋹", + 7: "7", + 8: "8", + 9: "9", +} + +# Skip the special (scratchpad) workspace +SKIP = {-99} + + +def workspace_sort(obj: OutputWorkspaceInfo) -> "SupportsRichComparison": + """Returns a key to sort by, given the current element.""" + return obj["id"] + + +def fill_blank_workspaces(open: list[OutputWorkspaceInfo]) -> list[OutputWorkspaceInfo]: + """Add in the rest of the workspaces which don't have any open windows on them. + + This is needed because hyprland deletes workspaces with nothing in them. + Note that this assumes all available workspaces were listed in REMAPS, and will + only fill those. These blank workspaces will have most string values set to "N/A", + and most int values set to 0. + """ + # Work on a copy, we don't want to alter the original list + lst = open.copy() + + for remap_id, format_name in REMAPS.items(): + # Skip for already present workspaces + if any(ws_info["id"] == remap_id for ws_info in lst): + continue + + blank_ws: OutputWorkspaceInfo = { + "id": remap_id, + "name": str(remap_id), + "monitor": "N/A", + "windows": 0, + "hasfullscreen": False, + "lastwindow": "N/A", + "lastwindowtitle": "N/A", + "active": False, + "format_name": format_name, + "monitor_id": 0, + } + lst.append(blank_ws) + + return lst + + +def get_workspaces() -> list[OutputWorkspaceInfo]: + """Obtain workspaces from hyprctl, sort them and add format_name arg.""" + proc = subprocess.run(["hyprctl", "workspaces", "-j"], stdout=subprocess.PIPE) + proc.check_returncode() + try: + workspaces: list[WorkspaceInfo] = json.loads(proc.stdout) + except json.JSONDecodeError: + sys.stderr.writelines([ + "Error decoding json response from hyprctl, returning empty workspaces", + f"Actual captured output from hyprctl: {proc.stdout!r}" + ]) + sys.stderr.flush() + workspaces = [] + + proc = subprocess.run(["hyprctl", "monitors", "-j"], stdout=subprocess.PIPE) + proc.check_returncode() + monitors: list[MonitorInfo] = json.loads(proc.stdout) + + active_workspaces = {monitor["activeWorkspace"]["id"] for monitor in monitors} + + out: list[OutputWorkspaceInfo] = [] + for workspace in workspaces: + if workspace["id"] in SKIP: + continue + format_name = REMAPS.get(workspace["id"], workspace["name"]) + active = workspace["id"] in active_workspaces + try: + mon_id = [monitor["id"] for monitor in monitors if monitor["name"] == workspace["monitor"]][0] + except IndexError: # Sometimes workspace["monitor"] is "?", which doesn't match any monitor + mon_id = -1 + out.append({**workspace, "format_name": format_name, "active": active, "monitor_id": mon_id}) + + out = fill_blank_workspaces(out) + out.sort(key=workspace_sort) + return out + + +def print_workspaces() -> None: + wks = get_workspaces() + ret = json.dumps(wks) + print(ret) + sys.stdout.flush() + + +def main() -> None: + parser = argparse.ArgumentParser() + parser.add_argument( + "--oneshot", + action="store_true", + help="Don't listen to stdout for updates, only run once and quit", + ) + parser.add_argument( + "--loop", + action="store_true", + help="Listen to stdout input, once something is received, re-print workspaces" + ) + args = parser.parse_args() + + if args.loop and args.oneshot: + print("Can't use both --oneshot and --loop", file=sys.stdout) + sys.exit(1) + + if args.loop is None and args.oneshot is None: + print("No option specified!", file=sys.stdout) + sys.exit(1) + + # Print workspaces here immediately, we don't want to have to wait for the first + # update from stdin as we only receive those on actual workspace change. + print_workspaces() + + if args.oneshot: + # We've already printed the workspaces once, we can exit now + return + + # Reprint workspaces on each stdin update (flush) + for _ in sys.stdin: + print_workspaces() + + +if __name__ == "__main__": + main() diff --git a/home/programs/graphical/bars/eww/config/windows/calendar.yuck b/home/programs/graphical/bars/eww/config/windows/calendar.yuck new file mode 100644 index 0000000..610f00a --- /dev/null +++ b/home/programs/graphical/bars/eww/config/windows/calendar.yuck @@ -0,0 +1,14 @@ +(defwidget calendar-win [] + (box + :class "calendar-win" + (calendar))) + +(defwindow calendar + :monitor 0 + :geometry (geometry + :x "0%" + :y "0%" + :anchor "top right" + :width "0px" + :height "0px") + (calendar-win)) diff --git a/home/programs/graphical/bars/eww/config/windows/radio-menu.yuck b/home/programs/graphical/bars/eww/config/windows/radio-menu.yuck new file mode 100644 index 0000000..c9a51b5 --- /dev/null +++ b/home/programs/graphical/bars/eww/config/windows/radio-menu.yuck @@ -0,0 +1,78 @@ +(defwidget radio-menu-win [] + (box + :class "radio-menu-box" + :space-evenly false + :orientation "v" + + (box + :class "text-row" + :space-evenly false + (label + :class "title" + :text "Radio/Connections Panel")) + + (box + :class "element-row" + + (box + :class "wifi-box" + :space-evenly false + :orientation "v" + (box + :class "element icon" + :space-evenly false + :halign "center" + (button + :class "wifi-button" + :tooltip "${net.state} (strength: ${net.signal}%)" + :onclick "scripts/net toggle" + {net.icon}) + (label + :class "separator" + :text "│") + (button + :class "wifi-arrow-btn" + :onclick "eww close radio-menu && nm-connection-editor &" + "󰅂")) + (label + :text {net.essid} + :xalign 0.5 + :limit-width 15)) + + (box + :class "bluetooth-box" + :space-evenly false + :orientation "v" + (box + :class "element icon" + :space-evenly false + :halign "center" + (button + :class "bluetooth-button" + :onclick "scripts/bluetooth toggle" + :tooltip "${bluetooth.name} (${bluetooth.mac}) ${bluetooth.battery}" + {bluetooth.icon}) + (label + :class "separator" + :text "│") + (button + :class "bluetooth-arrow-btn" + :onclick "eww close radio-menu && blueberry" + "󰅂")) + (label + :text {bluetooth.name} + :xalign 0.5 + :tooltip "${bluetooth.name} (${bluetooth.mac}) ${bluetooth.battery}" + :limit-width 15))) + )) + +(defwindow radio-menu + :stacking "fg" + :monitor 0 + :geometry (geometry + :x "0" + :y "0" + :width "0%" + :height "0%" + :anchor "right top") + (radio-menu-win)) diff --git a/home/programs/graphical/bars/eww/default.nix b/home/programs/graphical/bars/eww/default.nix new file mode 100644 index 0000000..cb06535 --- /dev/null +++ b/home/programs/graphical/bars/eww/default.nix @@ -0,0 +1,62 @@ +{ + lib, + pkgs, + osConfig, + ... +}: let + inherit (lib) mkIf; + + cfg = osConfig.myOptions.home-manager.programs.bars.eww; +in { + config = mkIf cfg.enable { + programs.eww = { + enable = true; + configDir = ./config; + }; + + systemd.user.services = { + "eww" = { + Unit = { + Description = "ElKowar's Wacky Widgets (eww) daemon"; + After = [ "graphical-session-pre.target" ]; + PartOf = [ "graphical-session.target" ]; + }; + + Service = { + Type = "simple"; + Restart = "always"; + ExecStart = pkgs.writeShellScript "eww-daemon" '' + ${pkgs.eww}/bin/eww daemon --no-daemonize + ''; + + # Takes a value between -20 and 19. Higher values (e.g. 19) mean lower priority. + # Lower priority means the process will get less CPU time and therefore will be slower. + # Fortunately, I do not need my status bar to be fast. Also, te difference is almost + # unnoticeable and definitely negligible. + Nice = 19; + }; + + Install.WantedBy = [ "graphical-session.target" ]; + }; + + "eww-window@" = { + Unit = { + Description = "Open %I eww (ElKowar's Wacky Widgets) window"; + After = [ "eww.service" ]; + PartOf = [ "graphical-session-pre.target" ]; + }; + + Service = { + Type = "oneshot"; + RemainAfterExit = true; + ExecStartPre = "${pkgs.eww}/bin/eww ping"; + ExecStart = "${pkgs.eww}/bin/eww open %i"; + ExecStop = "${pkgs.eww}/bin/eww close %i"; + Restart = "on-failure"; + }; + + Install.WantedBy = [ "graphical-session.target" ]; + }; + }; + }; +} diff --git a/home/programs/graphical/default.nix b/home/programs/graphical/default.nix index 6b2ded2..233cea0 100644 --- a/home/programs/graphical/default.nix +++ b/home/programs/graphical/default.nix @@ -3,5 +3,6 @@ _: { ./wms ./launchers ./apps + ./bars ]; } diff --git a/hosts/voyager/default.nix b/hosts/voyager/default.nix index 990e772..4b424dd 100644 --- a/hosts/voyager/default.nix +++ b/hosts/voyager/default.nix @@ -125,6 +125,7 @@ }; programs = { + bars.eww.enable = true; spotify.enable = true; }; }; diff --git a/options/home/programs/default.nix b/options/home/programs/default.nix index c6bdb95..f37aae7 100644 --- a/options/home/programs/default.nix +++ b/options/home/programs/default.nix @@ -12,6 +12,13 @@ in }; }; + bars = { + eww = { + enable = mkEnableOption "Eww bar"; + autostart.enable = mkEnableOption "auto-starting eww daemon on graphical-session.target"; + }; + }; + spotify.enable = mkEnableOption "Spotify"; }; }