Add eww bar

This configuration was simply copied from my old Arch Linux system.
There are some issues that still need to be solved, namely with fonts
and missing bitcoin price script, but it's mostly minor.
This commit is contained in:
ItsDrike 2024-06-20 14:05:43 +02:00
parent a2632b4dea
commit ac23da55c5
Signed by: ItsDrike
GPG key ID: FA2745890B7048C0
51 changed files with 1773 additions and 0 deletions

View file

@ -0,0 +1,5 @@
_: {
imports = [
./eww
];
}

View file

@ -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;

View file

@ -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; }
}

View file

@ -0,0 +1,3 @@
.bitcoin {
.icon { margin-right: 6px; color: $gold; }
}

View file

@ -0,0 +1,4 @@
.clock {
// color: $sapphire;
.icon { margin-right: 6px; color: $sapphire; }
}

View file

@ -0,0 +1,3 @@
.cpu {
.icon { color: $lime; }
}

View file

@ -0,0 +1,7 @@
.gammarelay {
.icon {
color: $peach;
margin-left: 2px;
margin-right: 2px;
}
}

View file

@ -0,0 +1,3 @@
.kernel {
.icon { color: $lavender; }
}

View file

@ -0,0 +1,3 @@
.memory {
.icon { color: $maroon; }
}

View file

@ -0,0 +1,3 @@
.uptime {
.icon { color: $green; }
}

View file

@ -0,0 +1,9 @@
.volume {
.icon { color: $peach; }
.speaker {
.icon {
margin-left: 8px;
margin-right: 5px;
}
}
}

View file

@ -0,0 +1,4 @@
.window_name {
font-family: "JetBrains Mono", "Font Awesome 6 Free", sans-serif;
font-size: 1.4rem;
}

View file

@ -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;
}
}

View file

@ -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;
}
}

View file

@ -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;
}
}
}
}

View file

@ -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;
}

View file

@ -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))

View file

@ -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}%"
)
))))

View file

@ -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}))
)
)

View file

@ -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}")
)))

View file

@ -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)}%"
)
)))

View file

@ -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 "γ")
; ))
))

View file

@ -0,0 +1,15 @@
(defwidget kernel_module []
(eventbox
:class "module kernel"
(box
:space-evenly false
(label
:class "icon"
:text " ")
(label
:class "value"
:text {kernel}
)
)))

View file

@ -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)}%"
)
)))

View file

@ -0,0 +1,3 @@
(deflisten nightlight
:initial `{"running": false,"temperature": 0}`
`scripts/nightlight --state`)

View file

@ -0,0 +1,15 @@
(defwidget uptime_module []
(eventbox
:class "module uptime"
(box
:space-evenly false
(label
:class "icon"
:text " ")
(label
:class "value"
:text {uptime}
)
)))

View file

@ -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`)

View file

@ -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}%")
))
))

View file

@ -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}")
))

View file

@ -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}))
)
))

View file

@ -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

View file

@ -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'

View file

@ -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

View file

@ -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

View file

@ -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 <https://wiki.hyprland.org/IPC/> 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
}

View file

@ -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

View file

@ -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

View file

@ -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"]

View file

@ -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[@]}"

View file

@ -0,0 +1,7 @@
#!/bin/env bash
# shellcheck source=include
source "./scripts/include"
#hyprland_ipc "workspace|createworkspace|destroyworkspace|activewindow"
hyprland_ipc

View file

@ -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

View file

@ -0,0 +1,6 @@
#!/bin/env bash
# shellcheck source=include
source "./scripts/include"
hyprland_ipc "activewindow" | ./scripts/window_name.py

View file

@ -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()

View file

@ -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

View file

@ -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()

View file

@ -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))

View file

@ -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))

View file

@ -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" ];
};
};
};
}

View file

@ -3,5 +3,6 @@ _: {
./wms
./launchers
./apps
./bars
];
}

View file

@ -125,6 +125,7 @@
};
programs = {
bars.eww.enable = true;
spotify.enable = true;
};
};

View file

@ -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";
};
}