diff --git a/config/eww/_scale.scss b/config/eww/_scale.scss
new file mode 100644
index 0000000..792aef5
--- /dev/null
+++ b/config/eww/_scale.scss
@@ -0,0 +1,2 @@
+// Auto-generated by ~/.config/eww/scripts/init
+$scale: 1.5;
diff --git a/config/eww/animations.scss b/config/eww/animations.scss
new file mode 100644
index 0000000..2415179
--- /dev/null
+++ b/config/eww/animations.scss
@@ -0,0 +1,108 @@
+@keyframes fade-in {
+ from {opacity: 0;}
+ to {opacity: 1;}
+}
+
+@keyframes fade-out {
+ from {opacity: 1;}
+ to {opacity: 0;}
+}
+
+@keyframes rainbow {
+ 0% {border-color: $x1;color: $x1;}
+ 20% {border-color: $x3;color: $x3;}
+ 40% {border-color: $x2;color: $x2;}
+ 60% {border-color: $x6;color: $x6;}
+ 80% {border-color: $x4;color: $x4;}
+ 99% {border-color: $x5;color: $x5;}
+ 100% {border-color: $x1;color: $x1;}
+}
+
+@keyframes rainbow-bg {
+ 0% {background: $x1;}
+ 20% {background: $x3;}
+ 40% {background: $x2;}
+ 60% {background: $x6;}
+ 80% {background: $x4;}
+ 99% {background: $x5;}
+ 100% {background: $x1;}
+}
+
+@keyframes rainbow-fg {
+ 0% {color: $x1;}
+ 20% {color: $x3;}
+ 40% {color: $x2;}
+ 60% {color: $x6;}
+ 80% {color: $x4;}
+ 99% {color: $x5;}
+ 100% {color: $x1;}
+}
+
+
+@keyframes blink {
+ 0% { opacity: 1; }
+ 50% { opacity: 0; }
+ 100% { opacity: 1; }
+}
+
+@keyframes flicker {
+ 0% {
+ opacity: 0.3;
+ }
+ 10% {
+ opacity: 0.3;
+ }
+ 20% {
+ opacity: 1;
+ }
+ 30% {
+ opacity: 0.3;
+ }
+ 40% {
+ opacity: 1;
+ }
+ 50% {
+ opacity: 1;
+ }
+ 100% {
+ opacity: 1;
+ }
+}
+
+@keyframes opacity-pulse {
+ 0% { opacity: 1.0; }
+ 50% { opacity: 0.3; }
+ 100% { opacity: 1; }
+}
+
+$diagonal_fade_stripe_size: 56px;
+$diagonal_fade_opacity: 0.7;
+@keyframes diagonal-fade {
+ 0% {
+ background: repeating-linear-gradient(
+ -55deg,
+ rgba($xbg, $diagonal_fade_opacity),
+ rgba($xbg, $diagonal_fade_opacity) dpi($diagonal_fade_stripe_size),
+ rgba($xbg, $diagonal_fade_opacity) dpi($diagonal_fade_stripe_size),
+ rgba($xbg, $diagonal_fade_opacity) dpi($diagonal_fade_stripe_size * 2)
+ );
+ }
+ 50% {
+ background: repeating-linear-gradient(
+ -55deg,
+ rgba($xbg, $diagonal_fade_opacity),
+ rgba($xbg, $diagonal_fade_opacity) dpi($diagonal_fade_stripe_size),
+ rgba($xbg, 1) dpi($diagonal_fade_stripe_size),
+ rgba($xbg, 1) dpi($diagonal_fade_stripe_size * 2)
+ );
+ }
+ 100% {
+ background: repeating-linear-gradient(
+ -55deg,
+ rgba($xbg, 1),
+ rgba($xbg, 1) dpi($diagonal_fade_stripe_size),
+ rgba($xbg, 1) dpi($diagonal_fade_stripe_size),
+ rgba($xbg, 1) dpi($diagonal_fade_stripe_size * 2)
+ );
+ }
+}
diff --git a/config/eww/colors.scss b/config/eww/colors.scss
new file mode 100644
index 0000000..6317e63
--- /dev/null
+++ b/config/eww/colors.scss
@@ -0,0 +1,18 @@
+$xbg: #101319;
+$xfg: #f4f3ee;
+$x0: #171b24;
+$x1: #E34F4F;
+$x2: #69bfce;
+$x3: #e37e4f;
+$x4: #5679E3;
+$x5: #956dca;
+$x6: #5599E2;
+$x7: #f4f3ee;
+$x8: #3A435A;
+$x9: #DE2B2B;
+$x10: #56B7C8;
+$x11: #DE642B;
+$x12: #3E66E0;
+$x13: #885AC4;
+$x14: #3F8CDE;
+$x15: #DDDBCF;
diff --git a/config/eww/colors.yuck b/config/eww/colors.yuck
new file mode 100644
index 0000000..7431d19
--- /dev/null
+++ b/config/eww/colors.yuck
@@ -0,0 +1 @@
+(defvar colors "[\"#171b24\", \"#E34F4F\", \"#69bfce\", \"#e37e4f\", \"#5679E3\", \"#956dca\", \"#5599E2\", \"#f4f3ee\", \"#3A435A\", \"#DE2B2B\", \"#56B7C8\", \"#DE642B\", \"#3E66E0\", \"#885AC4\", \"#3F8CDE\", \"#DDDBCF\"]")
diff --git a/config/eww/eww.scss b/config/eww/eww.scss
new file mode 100644
index 0000000..98aaac5
--- /dev/null
+++ b/config/eww/eww.scss
@@ -0,0 +1,46 @@
+// Unsets everything so you can style everything from scratch
+* {
+ all: unset;
+}
+
+// We get $scale from scale.scss, auto-generated by ~/.config/eww/scripts/init
+@import '_scale.scss';
+@function dpi($value) {
+ $unit: if(unitless($value), null, unit($value));
+
+ @if ($unit == "px" or $unit == "pt") {
+ @return ($value * $scale);
+ } @else {
+ // Return the original value if it's not px or pt
+ @return $value;
+ }
+}
+
+// Common
+$font_sans: "Kyok Medium";
+$font_sans_light: "Kyok Light";
+// $font_sans: "Google Sans Medium";
+$round_max: 999px;
+$sidebar_corner_radius: dpi(20px);
+
+@import 'colors.scss';
+@import 'animations.scss';
+
+@import 'widgets/circular-symbol-icon.scss';
+@import 'widgets/circular-symbol-with-text.scss';
+
+@import 'windows/sidebar.scss';
+@import 'windows/workspaces.scss';
+@import 'windows/bottom-bar.scss';
+@import 'windows/alarms.scss';
+@import 'windows/networks.scss';
+@import 'windows/powermenu.scss';
+@import 'windows/split-indicator.scss';
+@import 'windows/microphone-indicator.scss';
+@import 'windows/mode-indicator.scss';
+
+// Global Styles
+window {
+ background: transparent;
+ color: $xfg;
+}
diff --git a/config/eww/eww.yuck b/config/eww/eww.yuck
new file mode 100644
index 0000000..cb28a4e
--- /dev/null
+++ b/config/eww/eww.yuck
@@ -0,0 +1,28 @@
+; >>> Variables and constants
+; ---------------------------
+(include "colors.yuck")
+(include "globals.yuck")
+
+; >>> Widgets
+; -----------
+(include "widgets/circular-symbol-icon.yuck")
+(include "widgets/circular-symbol-with-text.yuck")
+
+; >>> Windows
+; -----------
+; >> Bars
+(include "windows/workspaces.yuck")
+(include "windows/bottom-bar.yuck")
+
+; >> Toggleable windows
+(include "windows/powermenu.yuck")
+(include "windows/sidebar.yuck")
+(include "windows/alarms.yuck")
+(include "windows/networks.yuck")
+(include "windows/microphone-indicator.yuck")
+(include "windows/split-indicator.yuck")
+(include "windows/mode-indicator.yuck")
+
+; >> Invisible utility windows
+; (include "windows/background.yuck")
+(include "windows/dismiss.yuck")
diff --git a/config/eww/globals.yuck b/config/eww/globals.yuck
new file mode 100644
index 0000000..e421a89
--- /dev/null
+++ b/config/eww/globals.yuck
@@ -0,0 +1,56 @@
+(defvar eww-scripts "~/.config/eww/scripts")
+
+; Set "constants" from commands by using a very long update interval
+(defpoll user :interval "9999h" `whoami`)
+(defpoll hostname :interval "9999h" `hostname`)
+
+; Status of various widgets (updated by manage script)
+(defvar sidebar-visible false)
+(defvar networks-visible false)
+(defvar alarms-visible false)
+
+; We are using eww to render a lockscreen and it would be unsafe to attempt to
+; authenticate as $USER since our password might leak. Thus, we are using a
+; separate password just for this lockscreen.
+; Use any ASCII printable characters (see characters 32-126 below)
+; https://theasciicode.com.ar/ascii-printable-characters/space-ascii-code-32.html
+; Limitations:
+; - The backslash "\" cannot be used
+; - Maximum 16 characters (for aesthetics: same length as flavor text)
+; - Caps Lock does not work while the lock screen input is active. You can use
+; Shift for capitalizing characters instead.
+(defvar screen-lock-password "eww")
+(defvar screen-locked false)
+
+(defvar vpn-status "off") ; updated by vpn.sh script
+
+(defvar gpu "0") ; updated by gpu script
+
+(defvar brightness "0") ; updated by brightness.sh script
+
+(defvar kdeconnect-battery "") ; updated by kdeconnect.sh script
+(defvar kdeconnect-reachable "") ; updated by kdeconnect.sh script
+
+(defvar uptime "") ; updated by uptime.sh script
+
+; We use charger.sh instead of EWW_BATTERY because charger status updates
+; instantly in the former vs the maximum ~2 sec delay of the latter
+(defvar charger false) ; updated by charger.sh script
+
+(defvar weather-temperature "") ; updated by weather.sh script
+(defvar weather-description "") ; updated by weather.sh script
+(defvar weather-icon "") ; updated by weather.sh script
+
+(defvar volume "0") ; updated by volume.sh script
+(defvar volume-muted "") ; updated by volume.sh script
+
+(defvar networks-json "[]") ; updated by networks.sh
+
+(defvar alarms-json "[]") ; updated by alarms.sh script
+
+(defvar sidebar-page-index "0")
+
+(defpoll date-json
+ :interval "10s"
+ :initial "{}"
+ `date +'{"hour": "%H", "min": "%M", "week_day_name": "%A", "week_day_number": "%u", "month_day_number": "%d", "month_name": "%B", "year": "%Y"}'`)
diff --git a/config/eww/scripts/agenda.py b/config/eww/scripts/agenda.py
new file mode 100755
index 0000000..0057e41
--- /dev/null
+++ b/config/eww/scripts/agenda.py
@@ -0,0 +1,158 @@
+#!/usr/bin/env python3
+import json, re, os, time, textwrap, sys
+from datetime import datetime, timedelta, date
+from pathlib import Path
+
+org_deadline_warning_days = 14
+org_schedule_warning_days = 7
+
+# TODO optimization: use grep/rg to get relevant lines and then just use capture
+# groups on these lines
+pattern_to_search = r'^(SCHEDULED|DEADLINE):\s*<(\d{4}-\d{2}-\d{2})\s+(\w{3})\s*((\d{2}:\d{2})(?:-(\d{2}:\d{2}))?)?\s*(\.\+)?([\+\d\w]*)'
+def search_entries_in_files(file_list):
+ results = []
+ for file_path in file_list:
+ with open(file_path, 'r', encoding='utf-8') as file:
+ previous_line = ''
+ for line in file:
+ line = line.strip() # Remove leading/trailing whitespace
+ if re.search(pattern_to_search, line):
+ title_pattern = r'(\*+)\s+(?:([A-Z]+)\s+)?(.*)'
+ match_title = re.match(title_pattern, previous_line)
+ _, status, title = match_title.groups()
+
+ match_info = re.match(pattern_to_search, line)
+ event_type, date_str, _, _, time_start, time_end, _, recurrence = match_info.groups()
+
+ date_obj = datetime.strptime(date_str, "%Y-%m-%d").date()
+
+ results.append({
+ 'file': Path(file_path).stem,
+ 'title': title,
+ 'status': status,
+ "event_type": event_type,
+ "date_obj": date_obj,
+ "date": date_str,
+ "time_start": time_start,
+ "time_end": time_end,
+ "recurrence": recurrence
+ })
+ previous_line = line
+ return results
+
+def calculate_recurrence(date, recurrence):
+ if not recurrence:
+ return None
+ match = re.match(r'\+(\d+)([dmy])', recurrence)
+ if match:
+ amount, unit = match.groups()
+ amount = int(amount)
+ if unit == 'd':
+ return date + timedelta(days=amount)
+ elif unit == 'm':
+ month = date.month - 1 + amount
+ year = date.year + month // 12
+ month = month % 12 + 1
+ day = min(date.day, [31, 29 if year % 4 == 0 and not year % 100 == 0 or year % 400 == 0 else 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31][month - 1])
+ return date.replace(year=year, month=month, day=day)
+ elif unit == 'y':
+ return date.replace(year=date.year + amount)
+ return None
+
+def pretty_print_date(input_date):
+ # Convert the input string to a date object
+ today = datetime.today().date()
+
+ # Calculate the difference in days between the input date and today
+ delta = (input_date - today).days
+
+ # Determine the human-readable string based on the difference in days
+ if delta == 0:
+ return "today"
+ elif delta == 1:
+ return "tomorrow"
+ elif delta == -1:
+ return "yesterday"
+ elif -5 <= delta <= -2:
+ return f"{abs(delta)} days ago"
+ elif 2 <= delta <= 5:
+ return f"in {delta} days"
+ else:
+ return input_date.strftime("%A %d %B").lower()
+
+def generate_agenda(entries):
+ today = datetime.now().date()
+
+ # When to show done/cancelled tasks
+ done_time_range_start = today
+ done_time_range_end = today # Never
+ # done_time_range_end = today + timedelta(days=7)
+ # done_time_range_start = today - timedelta(days=today.weekday())
+ # done_time_range_end = done_time_range_start + timedelta(days=6)
+
+ agenda = []
+ for entry in entries:
+ date_obj = entry["date_obj"]
+ status = entry["status"]
+ event_type = entry["event_type"]
+ title = entry["title"]
+ while True:
+ if not date_obj:
+ break
+
+ # Only handle DONE/KILL tasks that are within the time range
+ if (status == "DONE" or status == "KILL") and not (done_time_range_start <= date_obj <= done_time_range_end):
+ break;
+
+ if (event_type == "DEADLINE"):
+ if date_obj > (today + timedelta(days=org_deadline_warning_days)):
+ break;
+ else:
+ if date_obj > (today + timedelta(days=org_schedule_warning_days)):
+ break;
+
+ agenda.append({
+ "file": entry["file"],
+ "title": textwrap.shorten(title, width=26, placeholder="..."),
+ "type": event_type.lower(),
+ "status": status and status.lower(),
+ "repeatable": False if entry["recurrence"] == "" else True,
+ "overdue": False if date_obj >= today else True,
+ "date": datetime.strftime(date_obj, "%Y-%m-%d"),
+ "date_pretty": pretty_print_date(date_obj),
+ "time_start": entry["time_start"],
+ "time_end": entry["time_end"],
+ })
+
+ # If the task was DONE/KILL, do not show it again
+ if (status == "DONE" or status == "KILL"):
+ break
+
+ date_obj = calculate_recurrence(date_obj, entry["recurrence"])
+
+ # Sort results
+ def get_datetime(entry):
+ date_str = entry['date']
+ time_str = entry['time_start'] or "00:00"
+ combined_str = f"{date_str} {time_str}"
+ return datetime.strptime(combined_str, "%Y-%m-%d %H:%M")
+
+ # Sort entries using the get_datetime function as the key
+ sorted_agenda = sorted(agenda, key=get_datetime)
+
+ return sorted_agenda
+
+
+# Get org files list from arguments
+if len(sys.argv) == 0:
+ print("No files to parse")
+ exit(1)
+
+org_files = sys.argv[1:]
+
+entries = search_entries_in_files(org_files)
+
+agenda = generate_agenda(entries)
+
+# print(json.dumps(agenda, indent=2))
+os.system("eww update tasks-json=\"%s\"" % (json.dumps(agenda).replace('"', '\\"')))
diff --git a/config/eww/scripts/alarm-set-minutes.sh b/config/eww/scripts/alarm-set-minutes.sh
new file mode 100755
index 0000000..07c1e0b
--- /dev/null
+++ b/config/eww/scripts/alarm-set-minutes.sh
@@ -0,0 +1,26 @@
+#!/usr/bin/env bash
+# Update total minutes and turn them into HH:MM format.
+# This can also be done with eww expressions but in a more verbose and
+# inefficient way, which is why we created this script
+total_minutes="$(eww get alarms-total-minutes)"
+operation=$1
+amount=$2
+if [[ "$operation" == "plus" ]]; then
+ # 24 * 60 = 1440 minutes in a day
+ total_minutes=$(((total_minutes + amount) % 1440))
+elif [[ "$operation" == "minus" ]]; then
+ total_minutes=$(((total_minutes - amount + 1440) % 1440))
+else
+ echo "Invalid operation"
+ exit 1
+fi
+
+hours=$((total_minutes / 60))
+minutes=$((total_minutes % 60))
+
+eww update \
+ alarms-input-hours="$hours" \
+ alarms-input-minutes="$minutes" \
+ alarms-preview-hours="$(printf "%02d" $hours)" \
+ alarms-preview-minutes="$(printf "%02d" $minutes)" \
+ alarms-total-minutes="$total_minutes"
diff --git a/config/eww/scripts/alarm.sh b/config/eww/scripts/alarm.sh
new file mode 100755
index 0000000..efd9d1a
--- /dev/null
+++ b/config/eww/scripts/alarm.sh
@@ -0,0 +1,78 @@
+#!/bin/bash
+# Alarm manager for eww
+eww="$HOME/.config/eww/scripts"
+alarm_sound="$HOME/.config/mpv/alarm.mp3"
+
+alarm_dir="/tmp/eww-alarms"
+
+mkdir -p "$alarm_dir"
+
+function help {
+ echo -e "Usage:\nalarm in 5m\nalarm at 18:40"
+}
+
+action="$1"
+alarm_time_input="$2"
+alarm_label="${3:-beep beep}"
+
+# To transform everything to seconds
+declare -A symbols_to_seconds=(
+ ["d"]=86400
+ ["h"]=3600
+ ["m"]=60
+ ["s"]=1
+)
+
+now="$(date +%s)"
+if [[ "$action" == "in" ]]; then
+ last_character="${alarm_time_input: -1}"
+ case $last_character in
+ d|h|m|s )
+ multiplier=${symbols_to_seconds[$last_character]}
+ time="${alarm_time_input::-1}"
+ ;;
+ * )
+ multiplier=1
+ time="$alarm_time_input"
+ esac
+ seconds_to_add=$((time * multiplier))
+ alarm_time_timestamp=$((now + seconds_to_add))
+elif [[ "$action" == "at" ]]; then
+ alarm_time_timestamp="$(date -d "$alarm_time_input" +%s)"
+ if [[ "$now" -ge "$alarm_time_timestamp" ]]; then
+ alarm_time_timestamp="$(date -d "tomorrow $alarm_time_input" +%s)"
+ fi
+elif [[ "$action" == "delete" ]]; then
+ alarm_index="$2"
+ # Since file names are timestamps, alphabetical order is also chronological order
+ alarm_file="$(find "$alarm_dir" -type f | sort | sed -n $((alarm_index + 1))p)"
+ if [ ! -f "$alarm_file" ]; then
+ exit 1
+ fi
+ kill "$(head -1 < "$alarm_file")"
+ rm "$alarm_file"
+ exit 0
+else
+ help
+ exit 1
+fi
+
+alarm_file="${alarm_dir}/$alarm_time_timestamp"
+echo -e "$$\n$alarm_label" > "$alarm_file"
+notify-send -t 2000 -a eww-alarm "Alarm" "Will ring at $(date -d @"${alarm_time_timestamp}" "+%H:%M")"
+
+# We use safe-sleep since with normal sleep, if the computer is suspended in the
+# middle of a long sleep, the alarm will not ring at the right time.
+
+# For less immediate alarms, we can be a bit more relaxed with the sleep interval
+if [[ "$((alarm_time_timestamp - now))" -lt 60 ]]; then
+ sleep_interval=1
+else
+ sleep_interval=5
+fi
+
+"${eww}"/safe-sleep until "$alarm_time_timestamp" with-interval "$sleep_interval"
+playsound "$alarm_sound"
+notify-send -t 0 -a eww-alarm "Alarm" "$alarm_label"
+
+rm "$alarm_file"
diff --git a/config/eww/scripts/cal.py b/config/eww/scripts/cal.py
new file mode 100755
index 0000000..6644913
--- /dev/null
+++ b/config/eww/scripts/cal.py
@@ -0,0 +1,55 @@
+#!/usr/bin/env python3
+import json, os, subprocess
+from datetime import datetime, timedelta, date
+
+def get_month_calendar(year, month):
+ # Determine the first day of the month
+ first_day = datetime(year, month, 1)
+
+ # Find the starting point (Monday of the week containing the first day)
+ start_date = first_day - timedelta(days=first_day.weekday())
+
+ # Determine the last day of the month
+ next_month = first_day + timedelta(days=32)
+ last_day = datetime(next_month.year, next_month.month, 1) - timedelta(days=1)
+
+ calendar = []
+ current_date = start_date
+ today = date.today().strftime('%Y-%m-%d')
+
+ other_month = True
+ while current_date <= last_day:
+ week = []
+ for _ in range(7):
+ value = current_date.strftime('%d'),
+ if value[0] == "01":
+ other_month = not other_month
+ week.append({
+ "value": value[0],
+ "today": True if today == current_date.strftime('%Y-%m-%d') else False,
+ "other_month": other_month,
+ })
+ current_date += timedelta(days=1)
+ calendar.append(week)
+
+ return calendar
+
+
+year = subprocess.run(["eww", "get", "calendar-selected-year"],
+ stdout=subprocess.PIPE, text=True).stdout.strip()
+month = subprocess.run(["eww", "get", "calendar-selected-month"],
+ stdout=subprocess.PIPE, text=True).stdout.strip()
+
+today_obj = date.today()
+# Empty year means it the vars are currently unset (right after reload), so we
+# get the actual month and year first
+if year == '':
+ year = today_obj.strftime('%Y')
+ month = today_obj.strftime('%m')
+
+selected_month_obj = datetime.strptime(month, "%m")
+os.system(f"eww update calendar-selected-year={year} calendar-selected-month={month} calendar-selected-month-pretty=\"{selected_month_obj.strftime("%b")}\"")
+
+calendar = get_month_calendar(int(year), int(month))
+# Get JSON format of calendar and escape double quotes
+os.system("eww update calendar-json=\"%s\"" % (json.dumps(calendar).replace('"', '\\"')))
diff --git a/config/eww/scripts/daemons-force-update b/config/eww/scripts/daemons-force-update
new file mode 100755
index 0000000..4d08044
--- /dev/null
+++ b/config/eww/scripts/daemons-force-update
@@ -0,0 +1,27 @@
+#!/usr/bin/env bash
+# The daemons which are responsible for updating variables are started *once* on
+# login, independently of eww. Furthermore, whenever eww reloads, it resets the
+# values of all variables, and the daemons have no way of knowing that.
+# This leads to some widgets having null values right after eww reloading.
+# Solution: We request daemon updates from all relevant daemons in this script.
+# Sadly there is no known way (that is not a fugly hack) to automatically
+# run a script on eww reload, so we have to run this script manually or with a
+# keybind.
+eww="$HOME/.config/eww/scripts"
+
+"$eww/daemons/agenda.sh" oneshot &
+"$eww/daemons/alarms.sh" oneshot &
+"$eww/daemons/brightness.sh" oneshot &
+"$eww/daemons/charger.sh" oneshot &
+"$eww/daemons/days-of-the-week.sh" oneshot &
+"$eww/daemons/kdeconnect.sh" oneshot &
+"$eww/daemons/microphone.sh" oneshot &
+"$eww/daemons/uptime.sh" oneshot &
+"$eww/daemons/volume.sh" oneshot &
+"$eww/daemons/vpn.sh" oneshot &
+"$eww/daemons/weather.sh" oneshot &
+"$eww/daemons/sway-dock.py" oneshot &
+"$eww/daemons/sway-workspaces.py" oneshot &
+
+"$eww/networks.sh" update_networks &
+
diff --git a/config/eww/scripts/daemons/agenda.sh b/config/eww/scripts/daemons/agenda.sh
new file mode 100755
index 0000000..f347b97
--- /dev/null
+++ b/config/eww/scripts/daemons/agenda.sh
@@ -0,0 +1,23 @@
+#!/usr/bin/env bash
+
+org_files=("$HOME/notes/todo.org" "$HOME/notes/birthdays.org" "$HOME/notes/finance.org" "$HOME/notes/health.org" "$HOME/notes/home.org")
+
+update() {
+ ~/.config/eww/scripts/agenda.py "${org_files[@]}"
+ ~/.config/eww/scripts/cal.py
+}
+
+if [ "$1" == "oneshot" ]; then
+ update
+ exit
+fi
+
+(while (true) do
+ update
+ sleep 1m
+done)&
+
+while true; do
+ inotifywait -qq -e modify "${org_files[@]}"
+ update;
+done
diff --git a/config/eww/scripts/daemons/alarms.sh b/config/eww/scripts/daemons/alarms.sh
new file mode 100755
index 0000000..8ff8271
--- /dev/null
+++ b/config/eww/scripts/daemons/alarms.sh
@@ -0,0 +1,31 @@
+#!/bin/sh
+alarm_dir="/tmp/eww-alarms"
+mkdir -p "$alarm_dir"
+
+update() {
+ alarms="["
+ for alarm_file in "$alarm_dir"/*; do
+ timestamp="$(basename "$alarm_file")"
+ # alarm_file gets this value when directory is empty
+ if [[ "$alarm_file" == "$alarm_dir/*" ]]; then
+ eww update alarms-json="[]" alarms-index=0
+ return
+ fi
+ alarms="$alarms""{ \"hour\": \"$(date -d @"$timestamp" "+%H")\", \"min\": \"$(date -d @"$timestamp" "+%M")\", \"label\": \"$(tail -1 $alarm_file)\"},"
+ done
+ alarms="${alarms::-1}" # Remove last comma
+ alarms="$alarms""]"
+
+ eww update \
+ alarms-json="$alarms" \
+ alarms-index=0 \
+ alarms-state=select-type
+}
+
+update
+if [ "$1" == "oneshot" ]; then
+ exit
+fi
+
+while (inotifywait -e modify,delete "$alarm_dir" -qq) do update; done
+
diff --git a/config/eww/scripts/daemons/brightness.sh b/config/eww/scripts/daemons/brightness.sh
new file mode 100755
index 0000000..093f376
--- /dev/null
+++ b/config/eww/scripts/daemons/brightness.sh
@@ -0,0 +1,11 @@
+#!/bin/sh
+update() {
+ eww update brightness=$(light -G)
+}
+
+update
+if [ "$1" == "oneshot" ]; then
+ exit
+fi
+
+while (inotifywait -e modify /sys/class/backlight/?*/brightness -qq) do update; done
diff --git a/config/eww/scripts/daemons/charger.sh b/config/eww/scripts/daemons/charger.sh
new file mode 100755
index 0000000..5629036
--- /dev/null
+++ b/config/eww/scripts/daemons/charger.sh
@@ -0,0 +1,19 @@
+#!/bin/sh
+file="$(find /sys/class/power_supply/A*/online)"
+
+update() {
+ if [[ $(<$file) == "1" ]]; then
+ eww update charger=true
+ else
+ eww update charger=false
+ fi
+}
+
+update
+if [ "$1" == "oneshot" ]; then
+ exit
+fi
+
+acpi_listen | grep --line-buffered ac_adapter | while read -r line ; do
+ update
+done
diff --git a/config/eww/scripts/daemons/days-of-the-week.sh b/config/eww/scripts/daemons/days-of-the-week.sh
new file mode 100755
index 0000000..6e59a33
--- /dev/null
+++ b/config/eww/scripts/daemons/days-of-the-week.sh
@@ -0,0 +1,34 @@
+#!/bin/sh
+date_file=/tmp/eww-days-of-the-week
+
+old_index=-1
+update() {
+ week_day="$(date +%u)"
+ # Generate / empty date file
+ > "$date_file"
+ for i in $(seq 1 $((week_day - 1)) | tac); do echo $i days ago >> "$date_file"; done
+ echo today >> "$date_file"
+ for i in $(seq 1 $((7 - week_day))); do echo $i days >> "$date_file"; done
+
+ # Update relevant vars
+ update_str="["
+ update_str=$update_str"$(date -f "$date_file" '+"%d",' | tr '\n' ' ' )"
+ update_str=${update_str::-2} # Remove last comma and space
+ update_str=$update_str"]"
+ current_index=$((week_day - 1))
+ # Only update if day changed
+ if [ "$old_index" != "$current_index" ]; then
+ eww update dotw-current-index="$current_index" dotw-numbers="$update_str"
+ old_index=$current_index
+ fi
+}
+
+if [ "$1" == "oneshot" ]; then
+ update
+ exit
+fi
+
+while true; do
+ update
+ sleep 10
+done
diff --git a/config/eww/scripts/daemons/gpu.sh b/config/eww/scripts/daemons/gpu.sh
new file mode 100755
index 0000000..94423ec
--- /dev/null
+++ b/config/eww/scripts/daemons/gpu.sh
@@ -0,0 +1,13 @@
+#!/bin/bash
+# Not sure if this works on NVIDIA cards
+update() {
+ eww update gpu="$(cat /sys/class/drm/card0/device/gpu_busy_percent)"
+
+ # Alternatively: With amdgpu_top
+ # eww update gpu="$(amdgpu_top -gm --single | awk '/average_gfx_activity/ {print substr($2, 1, length($2)-1)}')"
+}
+
+while true; do
+ update
+ sleep 5
+done
diff --git a/config/eww/scripts/daemons/input-buffer.sh b/config/eww/scripts/daemons/input-buffer.sh
new file mode 100755
index 0000000..3d66d51
--- /dev/null
+++ b/config/eww/scripts/daemons/input-buffer.sh
@@ -0,0 +1,13 @@
+#!/usr/bin/env bash
+# This script is started by the init script for each eww widget that can accepts
+# input.
+widget=$1 # Widget name e.g. alarms
+sink=$2 # Path to sink script, e.g. input-sink-alarms.sh
+
+pipe=/tmp/eww-widget-input-pipe-${widget}
+
+[ ! -p $pipe ] && mkfifo $pipe
+
+tail -f $pipe | while IFS= read -r cmd; do
+ eval "$cmd"
+done
diff --git a/config/eww/scripts/daemons/kdeconnect.sh b/config/eww/scripts/daemons/kdeconnect.sh
new file mode 100755
index 0000000..39b886d
--- /dev/null
+++ b/config/eww/scripts/daemons/kdeconnect.sh
@@ -0,0 +1,46 @@
+#!/bin/sh
+# Reachable + battery state of device linked with KDEconnect
+# This helped a lot:
+# https://github.com/haideralipunjabi/polybar-kdeconnect/blob/master/polybar-kdeconnect.sh
+
+# Find (first) device ID
+deviceid="$(qdbus org.kde.kdeconnect /modules/kdeconnect org.kde.kdeconnect.daemon.devices | head -1)"
+
+# TODO (someday): Multiple devices
+# for deviceid in $(qdbus org.kde.kdeconnect /modules/kdeconnect org.kde.kdeconnect.daemon.devices); do
+# echo $deviceid
+# done
+
+update() {
+ # name="$(qdbus org.kde.kdeconnect "/modules/kdeconnect/devices/$deviceid" org.kde.kdeconnect.device.name)"
+ reachable="$(qdbus org.kde.kdeconnect "/modules/kdeconnect/devices/$deviceid" org.kde.kdeconnect.device.isReachable)"
+ if [[ "$reachable" == "true" ]]; then
+ battery="$(qdbus org.kde.kdeconnect /modules/kdeconnect/devices/${deviceid}/battery org.kde.kdeconnect.device.battery.charge | tr -d '\n')"
+ eww update kdeconnect-battery=$battery kdeconnect-reachable=1
+ else
+ eww update kdeconnect-reachable=0
+ fi
+}
+
+if [ "$1" == "oneshot" ]; then
+ update
+ exit
+fi
+
+while true; do
+ update
+ sleep 15
+done
+
+# To get all methods, properties and signals use this:
+# qdbus org.kde.kdeconnect "/modules/kdeconnect/devices/$deviceid"
+
+# TODO (someday) Instead of polling, we could subscribe to 'reachable' changes
+# dbus-monitor "type='signal',interface='org.kde.kdeconnect.device',member='reachableChanged'" |
+# while read -r line; do
+# if [[ "$line" == *"boolean true"* ]]; then
+# echo "Device is reachable"
+# elif [[ "$line" == *"boolean false"* ]]; then
+# echo "Device is not reachable"
+# fi
+# done
diff --git a/config/eww/scripts/daemons/media.sh b/config/eww/scripts/daemons/media.sh
new file mode 100755
index 0000000..0fec1c3
--- /dev/null
+++ b/config/eww/scripts/daemons/media.sh
@@ -0,0 +1,152 @@
+#!/usr/bin/env bash
+# Requires: wget, playerctl
+# playerctl bugs that affect this script:
+# - Wrong position sometimes due to https://github.com/altdesktop/playerctl/issues/58
+# - Wrong position sometimes due to https://github.com/altdesktop/playerctl/issues/102
+# - Firefox position retrieved through playerctl is often wrong, especially
+# after seeking / skipping. It could be because Firefox is re-using the same
+# player instance even if playing different media in different tabs.
+# https://github.com/altdesktop/playerctl/issues/289
+# - If using mpv + yt-dlp, and a video is loading, position starts moving before
+# the video has started playing
+
+eww_media_dir=/tmp/eww-media
+mkdir -p $eww_media_dir
+art=$eww_media_dir/art
+
+last_art_url="-1"
+
+# Helper
+duration_to_seconds() {
+ colons="${1//[^:]}"
+ colon_count="${#colons}"
+ tokens="$(tr ':' ' ' <<< $1)"
+ # It can only be 0, 1 or 2
+ if [[ $colon_count -eq 2 ]]; then
+ hours="$(awk '{printf "%d", $1;}' <<< "$tokens")"
+ minutes="$(awk '{printf "%d", $2;}' <<< "$tokens")"
+ seconds="$(awk '{printf "%d", $3;}' <<< "$tokens")"
+ elif [[ $colon_count -eq 1 ]]; then
+ hours=0
+ minutes="$(awk '{printf "%d", $1;}' <<< "$tokens")"
+ seconds="$(awk '{printf "%d", $2;}' <<< "$tokens")"
+ else
+ hours=0
+ minutes=0
+ seconds=0
+ fi
+ total_seconds=$((hours * 3600 + minutes * 60 + seconds))
+ echo $total_seconds
+}
+
+update_art() {
+ art_path="$1"
+ # Crop biggest square from the center
+ magick "$art_path" -gravity Center -extent 1:1 "$art_path"
+ cp "$art_path" "$art"
+ eww update media-art="$art"
+}
+
+# Get info in this weird format
+# - @@property@@value@@property@@ is highly unlikely to be used inside artist or
+# title names
+# - JSON format would be tricky with quotes that need to be escaped
+playerctl --follow metadata --format '@@playerName@@{{playerName}}@@playerName@@ @@volume@@{{volume * 100}}@@volume@@ @@status@@{{lc(status)}}@@status@@ @@position@@{{duration(position)}}@@position@@ @@length@@{{duration(mpris:length)}}@@length@@ @@artUrl@@{{mpris:artUrl}}@@artUrl@@ @@artist@@{{artist}}@@artist@@ @@title@@{{title}}@@title@@ @@url@@{{url}}@@url@@' | while read -r line; do
+ player_name="$(sed 's/.*@@playerName@@\(.*\)@@playerName@@.*/\1/' <<< "$line")"
+ volume="$(sed 's/.*@@volume@@\(.*\)@@volume@@.*/\1/' <<< "$line")"
+ status="$(sed 's/.*@@status@@\(.*\)@@status@@.*/\1/' <<< "$line")"
+ url="$(sed 's/.*@@url@@\(.*\)@@url@@.*/\1/' <<< "$line")"
+ position="$(sed 's/.*@@position@@\(.*\)@@position@@.*/\1/' <<< "$line")"
+ length="$(sed 's/.*@@length@@\(.*\)@@length@@.*/\1/' <<< "$line")"
+ art_url="$(sed 's/.*@@artUrl@@\(.*\)@@artUrl@@.*/\1/' <<< "$line")"
+
+ artist="$(sed 's/.*@@artist@@\(.*\)@@artist@@.*/\1/' <<< "$line")"
+ title="$(sed 's/.*@@title@@\(.*\)@@title@@.*/\1/' <<< "$line")"
+
+ if [ "$artist" == "" ]; then
+ artist="N/A"
+ fi
+
+ if [ "$title" == "" ]; then
+ title="N/A"
+ fi
+
+ if [[ "$title" == "ytsearch:'"* ]]; then
+ # Remove final quote
+ title=${title::-1}
+ # Remove ^ytsearch:'
+ title=${title:10}
+ fi
+
+ if [[ "$artist" == *" - Topic" ]]; then
+ artist=${artist::-8}
+ fi
+
+ # Escape double quotes and backslashes
+ artist="$(sed 's/\\/\\\\/g;s/"/\\"/g' <<< "$artist")"
+ title="$(sed 's/\\/\\\\/g;s/"/\\"/g' <<< "$title")"
+
+ # >>> Progress
+ position_seconds="$(duration_to_seconds "$position")"
+ length_seconds="$(duration_to_seconds "$length")"
+ if [[ $length_seconds -eq 0 ]]; then
+ length_seconds=1
+ fi
+ progress="$(awk 'BEGIN { printf "%.f", '"$((position_seconds*100))"'/'"${length_seconds}"' }')"
+ if [[ $progress -gt 100 ]]; then
+ progress=100
+ fi
+
+ # Update media-json and media-progress variables together
+ # Art is updated separately only if needed
+ eww update media-progress="$progress" media-json="{
+ \"player_name\": \"$player_name\",
+ \"volume\": \"$volume\",
+ \"status\": \"$status\",
+ \"artist\": \"$artist\",
+ \"title\": \"$title\",
+ \"position\": \"$position\",
+ \"length\": \"$length\",
+ \"length_seconds\": \"$length_seconds\",
+ \"url\": \"$url\"
+ }"
+
+ # >>> Art
+ if [ "$art_url" == "$last_art_url" ]; then
+ # echo skipping
+ continue
+ fi
+
+ if [ -n "$art_url" ] && [ "$art_url" != "" ]; then
+ # Chromium flatpak stores files in a different directory
+ if [[ "$art_url" =~ ^file:///tmp/.org.chromium.Chromium ]]; then
+ art_path="$(sed 's@file://@'$XDG_RUNTIME_DIR'/.flatpak/org.chromium.Chromium@' <<< "$art_url")"
+ update_art "$art_path"
+ # Local file
+ elif [[ "$art_url" =~ ^file:/// ]]; then
+ art_path="$(sed 's@file://@@' <<< "$art_url")"
+ update_art "$art_path"
+ else
+ # Do not redownload with every update
+ # Store art in "$art_path" and check if it exists before redownloading
+ art_path=$eww_media_dir/"$(base64 <<< "$art_url")"
+ if [ ! -f "$art_path" ]; then
+ # Set art to placeholder here because the download might take a while
+ eww update media-art=""
+ # Note: You can also use curl instead of wget, but I noticed it
+ # occasionally fails to download thumbnails from ytimg.com
+ # if curl -sf $art_url --output "$art_path"; then
+ if wget --quiet "$art_url" -O "$art_path"; then
+ update_art "$art_path"
+ fi
+ else
+ # Art has been downloaded and processed before
+ cp "$art_path" "$art"
+ eww update media-art="$art"
+ fi
+ fi
+ else
+ eww update media-art=""
+ fi
+ last_art_url="$art_url"
+done
diff --git a/config/eww/scripts/daemons/microphone.sh b/config/eww/scripts/daemons/microphone.sh
new file mode 100755
index 0000000..abcb9dc
--- /dev/null
+++ b/config/eww/scripts/daemons/microphone.sh
@@ -0,0 +1,32 @@
+#!/bin/bash
+old_status=-1
+update() {
+ info="$(pactl list sources | awk '/Name: alsa_input.pci/{nr[NR+6]}; NR in nr')"
+ status="$([[ "$info" == *"Mute: yes"* ]] && echo off || echo on)"
+ # Only update if status changed
+ if [ "$old_status" != "$status" ]; then
+ eww update microphone="$status"
+ old_status=$status
+ fi
+}
+
+update
+if [ "$1" == "oneshot" ]; then
+ exit
+fi
+
+LANG=C pactl subscribe 2> /dev/null | grep --line-buffered "Event 'change' on source #" | while read -r line ; do
+ update
+done
+
+# ------ Before pipewire
+# update() {
+# info="$(pacmd list-sources | awk '/\\* index: /{nr[NR+7];nr[NR+11]}; NR in nr' | tail -1)"
+# muted="$([[ "$info" == *"muted: yes"* ]] && echo '' || echo active)"
+# eww update microphone="$muted"
+# }
+
+# update
+# pactl subscribe 2> /dev/null | grep --line-buffered "source #" | while read -r line ; do
+# update
+# done
diff --git a/config/eww/scripts/daemons/network-events.sh b/config/eww/scripts/daemons/network-events.sh
new file mode 100755
index 0000000..a8aade5
--- /dev/null
+++ b/config/eww/scripts/daemons/network-events.sh
@@ -0,0 +1,24 @@
+#!/usr/bin/env bash
+eww="$HOME/.config/eww/scripts"
+network_notif="/tmp/eww-network-events-notif"
+
+# We use uniq to remove duplicate lines sometimes caused when wifi connects
+ip monitor link | grep --line-buffered -e "\(wlan\|enp\).*state \(DOWN\|UP\)" | stdbuf -oL uniq | while read -r line; do
+ interface="$(awk '{print substr($2, 1, length($2)-1)}' <<< "$line")"
+ state="$(awk '{for(i=1;i<=NF;i++) if ($i=="state") print $(i+1)}' <<< "$line")"
+
+ if [[ "$interface" =~ ^wlan ]]; then
+ interface_pretty_name="Wi-Fi"
+ elif [[ "$interface" =~ ^enp ]]; then
+ interface_pretty_name="Ethernet"
+ fi
+
+ if [[ "$state" == "UP" ]]; then
+ event_verb="connected"
+ elif [[ "$state" =~ "DOWN" ]]; then
+ event_verb="disconnected"
+ fi
+
+ notify-send.sh -R "$network_notif" -u low "Network" "${interface_pretty_name} ${event_verb}"
+ "$eww/networks.sh" update_networks
+done
diff --git a/config/eww/scripts/daemons/network-scan.sh b/config/eww/scripts/daemons/network-scan.sh
new file mode 100755
index 0000000..e42b2b2
--- /dev/null
+++ b/config/eww/scripts/daemons/network-scan.sh
@@ -0,0 +1,10 @@
+#!/usr/bin/env bash
+eww="$HOME/.config/eww/scripts"
+update() {
+ $eww/networks.sh scan
+}
+
+while true; do
+ update
+ "$eww/safe-sleep" 5m with-interval 10
+done
diff --git a/config/eww/scripts/daemons/sway-dock.py b/config/eww/scripts/daemons/sway-dock.py
new file mode 100755
index 0000000..856bb2e
--- /dev/null
+++ b/config/eww/scripts/daemons/sway-dock.py
@@ -0,0 +1,150 @@
+#!/usr/bin/env python3
+import i3ipc
+import os
+import json
+import sys
+
+window_information = {
+ "firefox": { "symbol": "", "color": "3" },
+ "kitty": { "symbol": "", "color": "5" },
+ "htop": { "symbol": "", "color": "2" },
+ "btop": { "symbol": "", "color": "2" },
+ "Sxiv": { "symbol": "", "color": "1" },
+ "nemo": { "symbol": "", "color": "3" },
+ "chrome-discordapp.com__channels_@me-Default": { "symbol": "", "color": "4" },
+ "chrome-chatgpt.com__-Default": { "symbol": "", "color": "2" },
+ "lutris": { "symbol": "", "color": "6" },
+ "editor": { "symbol": "", "color": "5" },
+ "scratchpad": { "symbol": "", "color": "1" },
+ "scratchpad_input": { "symbol": "", "color": "3" },
+ "gucharmap": { "symbol": "", "color": "2" },
+ "steam": { "symbol": "", "color": "2" },
+ "mpv": { "symbol": "", "color": "6" },
+ "chromium-browser": { "symbol": "", "color": "4" },
+ "org.chromium.Chromium": { "symbol": "", "color": "4" },
+ "org.pwmt.zathura": { "symbol": "", "color": "3" },
+ "Emacs": { "symbol": "", "color": "2" },
+ "org": { "symbol": "", "color": "2" },
+ "explorer.exe": { "symbol": "", "color": "4" },
+ "appimagekit_3a48058ef277d45ae2228089429c0259-Telegram_Desktop": { "symbol": "", "color": "4" },
+ "pavucontrol": { "symbol": "", "color": "4" },
+ "chrome-open.spotify.com__-Default": { "symbol": "", "color": "2" },
+ "Wine": { "symbol": "", "color": "1" },
+ "chrome-mail.proton.me__-Default": { "symbol": "", "color": "6" },
+ "Gimp": { "symbol": "", "color": "4" },
+ "deluge": { "symbol": "", "color": "1" },
+ "___default___": { "symbol": "", "color": "10" },
+}
+
+# Key: window identifier
+# Value: command to run
+# Order matters
+pinned_apps = {
+ "firefox": "firefox",
+ "chrome-discordapp.com__channels_@me-Default": "discord",
+ "org": "org",
+ "editor": "swaymsg exec '$cmd_editor'",
+ "nemo": "nemo",
+ "lutris": "lutris",
+ "steam": "steam",
+}
+
+# ----------------------------------------
+
+i3 = i3ipc.Connection()
+eww_manage=os.environ['HOME']+"/.config/eww/scripts/manage"
+window_counter = {}
+dock_items = []
+focused = ""
+last = ""
+recently_used = {}
+
+def update_eww():
+ dock_representation = []
+ for window_identifier in pinned_apps:
+ item = window_information.get(window_identifier) or window_information.get("___default___")
+ if (window_identifier == focused):
+ state = "focused"
+ elif (window_counter.get(window_identifier) == None or window_counter.get(window_identifier) == 0):
+ state = "empty"
+ else:
+ state = "unfocused"
+ dock_representation.append({
+ 'identifier': window_identifier,
+ 'symbol': item['symbol'],
+ 'color': item['color'],
+ 'launcher': pinned_apps.get(window_identifier) or None,
+ 'recently_used_con_id': recently_used.get(window_identifier) or None,
+ 'state': state
+ })
+
+ for window_identifier in dock_items:
+ if window_identifier in pinned_apps:
+ continue
+ item = window_information.get(window_identifier) or window_information.get("___default___")
+ dock_representation.append({
+ 'identifier': window_identifier,
+ 'symbol': item['symbol'],
+ 'color': item['color'],
+ 'recently_used_con_id': recently_used.get(window_identifier) or None,
+ 'state': "focused" if (window_identifier == focused) else "unfocused"
+ })
+ os.system("eww update dock-items-json='%s'" % (json.dumps(dock_representation)))
+
+def on_focus(i3, e):
+ global focused
+ window_identifier = e.container.app_id or e.container.window_class
+ focused = window_identifier if window_identifier else ''
+ recently_used[window_identifier] = e.container.id
+ update_eww()
+
+def on_workspace_focus(i3, e):
+ global focused
+ # If workspace is empty
+ if (len(e.current.focus) == 0):
+ focused = ""
+ update_eww()
+
+def on_new(i3, e):
+ window_identifier = e.container.app_id or e.container.window_class
+ window_counter[window_identifier] = (window_counter.get(window_identifier) or 0) + 1
+ if (window_counter[window_identifier] == 1):
+ dock_items.append(window_identifier)
+ update_eww()
+
+def on_close(i3, e):
+ global focused
+ window_identifier = e.container.app_id or e.container.window_class
+ window_counter[window_identifier] = window_counter.get(window_identifier) - 1
+ if (window_counter[window_identifier] == 0):
+ dock_items.remove(window_identifier)
+
+ # If the container was focused before it died
+ if (e.container.focused):
+ focused = ""
+
+ update_eww()
+
+i3.on("window::focus", on_focus)
+i3.on("window::new", on_new)
+i3.on("window::close", on_close)
+i3.on("workspace::focus", on_workspace_focus)
+
+# Initial update
+for con in i3.get_tree():
+ window_identifier = con.app_id or con.window_class
+ if (not window_identifier):
+ continue
+ window_counter[window_identifier] = (window_counter.get(window_identifier) or 0) + 1
+ if (window_counter[window_identifier] > 1):
+ continue
+ dock_items.append(window_identifier)
+
+update_eww()
+
+if len(sys.argv) > 1 and sys.argv[1] == "oneshot":
+ exit(0)
+
+# Start the main loop and wait for events to come in.
+i3.main()
+
diff --git a/config/eww/scripts/daemons/sway-modes.py b/config/eww/scripts/daemons/sway-modes.py
new file mode 100755
index 0000000..6a3943b
--- /dev/null
+++ b/config/eww/scripts/daemons/sway-modes.py
@@ -0,0 +1,24 @@
+#!/usr/bin/env python
+import i3ipc
+import os
+
+eww_manage=os.environ['HOME']+"/.config/eww/scripts/manage"
+
+i3 = i3ipc.Connection()
+
+def on_mode(i3, e):
+ # 'default' means no mode is active
+ # We also ignore modes that start with underscore
+ if e.change == 'default' or e.change[:1] == '_':
+ os.system(f"{eww_manage} hide mode-indicator")
+ elif e.change[:1] != '_':
+ os.system(f"eww update mode-current='{e.change}' && {eww_manage} show mode-indicator")
+
+i3.on("mode", on_mode)
+
+# For filtering per mode this could work too.
+# def on_mode_apps(i3, e):
+# print('aaaaaaaaapps')
+# i3.on("mode::apps", on_mode_apps)
+
+i3.main()
diff --git a/config/eww/scripts/daemons/sway-split-indicator.py b/config/eww/scripts/daemons/sway-split-indicator.py
new file mode 100755
index 0000000..fdfb16f
--- /dev/null
+++ b/config/eww/scripts/daemons/sway-split-indicator.py
@@ -0,0 +1,56 @@
+#!/usr/bin/env python3
+# Adapted from: https://github.com/altdesktop/i3ipc-python/blob/master/examples/tiling-indicator.py
+import i3ipc
+import os
+import threading
+
+i3 = i3ipc.Connection()
+last = ""
+
+eww_manage=os.environ['HOME']+"/.config/eww/scripts/manage"
+
+def eww_split_indicator_hide():
+ os.system(eww_manage+" hide split-indicator")
+
+autohide_delay = 1.5 # seconds
+timer = threading.Timer(autohide_delay, eww_split_indicator_hide)
+def reset_timer():
+ global timer
+ timer.cancel()
+ timer = threading.Timer(autohide_delay, eww_split_indicator_hide)
+ timer.start()
+reset_timer()
+
+def eww_split_indicator_show():
+ global timer
+ reset_timer()
+ os.system(eww_manage+" show split-indicator")
+
+def on_event(i3, e):
+ global last
+ focused = i3.get_tree().find_focused()
+ if not (hasattr(focused, "parent")):
+ return
+ layout = focused.parent.layout
+ if layout == "output":
+ # Empty workspace
+ layout = focused.workspace().layout
+ if layout != last:
+ os.system("eww update split-direction='%s'" % layout)
+ reset_timer()
+ last = layout
+
+def on_tick(i3, e):
+ if e.payload == "split":
+ eww_split_indicator_show()
+ on_event(i3, e)
+
+# Subscribe to events
+i3.on("window::focus", on_event)
+# i3.on("binding", on_event)
+# Custom event emitted by splitting keybings and scripts
+i3.on("tick", on_tick)
+
+# Start the main loop and wait for events to come in.
+i3.main()
+
diff --git a/config/eww/scripts/daemons/sway-workspaces.py b/config/eww/scripts/daemons/sway-workspaces.py
new file mode 100755
index 0000000..2550de3
--- /dev/null
+++ b/config/eww/scripts/daemons/sway-workspaces.py
@@ -0,0 +1,41 @@
+#!/usr/bin/env python
+import i3ipc
+import os
+import sys
+
+i3 = i3ipc.Connection()
+
+def on_workspace_init(i3, e):
+ # when change is "init" the current obj shows which ws was initialized
+ os.system("eww update ws%s=''" % (e.current.name))
+
+def on_workspace_empty(i3, e):
+ # when change is "empty" the current obj shows which ws became empty
+ os.system("eww update ws%s='empty'" % (e.current.name))
+
+def on_workspace_focus(i3, e):
+ os.system("eww update ws%s='' ws%s='focused'" % (e.old.name, e.current.name))
+
+# TODO: 'urgent' state
+# When change is "urgent" the current obj shows which workspace became
+# urgent/non-urgent. We can probably get the state from the event itself.
+
+# https://i3wm.org/docs/ipc.html#_workspace_event
+# i3.on("workspace", on_workspace)
+i3.on("workspace::focus", on_workspace_focus)
+i3.on("workspace::empty", on_workspace_empty)
+i3.on("workspace::init", on_workspace_init)
+
+# Initial update
+for ws in i3.get_workspaces():
+ # It only returns non-empty workspaces
+ # That is why we initialize workspace state as 'empty' in our eww config
+ if (ws.focused):
+ os.system("eww update ws%s='focused'" % (ws.num))
+ else:
+ os.system("eww update ws%s=''" % (ws.num))
+
+if len(sys.argv) > 1 and sys.argv[1] == "oneshot":
+ exit(0)
+
+i3.main()
diff --git a/config/eww/scripts/daemons/uptime.sh b/config/eww/scripts/daemons/uptime.sh
new file mode 100755
index 0000000..8270b2c
--- /dev/null
+++ b/config/eww/scripts/daemons/uptime.sh
@@ -0,0 +1,14 @@
+#!/bin/sh
+update() {
+ eww update uptime="$(uptime -p | sed 's/^...//;s/ weeks\?,/w/;s/ days\?,/d/;s/ hours\?,/h/;s/ minutes\?/m/')"
+}
+
+if [ "$1" == "oneshot" ]; then
+ update
+ exit
+fi
+
+while true; do
+ update
+ sleep 1m
+done
diff --git a/config/eww/scripts/daemons/volume.sh b/config/eww/scripts/daemons/volume.sh
new file mode 100755
index 0000000..32c2281
--- /dev/null
+++ b/config/eww/scripts/daemons/volume.sh
@@ -0,0 +1,32 @@
+#!/usr/bin/env bash
+old_sink=$(pactl get-default-sink)
+old_muted=-1
+old_volume=-1
+update() {
+ default_sink=$(pactl get-default-sink)
+ # info="$(pactl list sinks | awk '/Name: '$default_sink'/{nr[NR+6];nr[NR+7]}; NR in nr')"
+ # muted="$([[ "$info" == *"Mute: yes"* ]] && echo muted || echo '')"
+ info="$(pactl get-sink-volume $default_sink)"
+ muted="$([[ "$(pactl get-sink-mute $default_sink)" == *"Mute: yes"* ]] && echo muted || echo '')"
+ volume="$(echo "$info" | awk '{ str = substr($5, 1, length($5)-1); gsub("%", "", str); print str }')"
+ if [[ "$default_sink" != "$old_sink" ]]; then
+ device=$(pactl list sinks | awk '/Name: '$default_sink'/{nr[NR+1]}; NR in nr {print substr($0, index($0, $2))}')
+ notify-send.sh -R /tmp/audio-device-notification --urgency low "Audio device" "$device"
+ old_sink=$default_sink
+ fi
+
+ if [[ "$volume" != "$old_volume" || "$muted" != "$old_muted" ]]; then
+ eww update volume-muted="$muted" volume=$volume
+ old_volume=$volume
+ old_muted=$muted
+ fi
+}
+
+update
+if [ "$1" == "oneshot" ]; then
+ exit
+fi
+
+pactl subscribe 2> /dev/null | grep --line-buffered "sink #" | while read -r _ ; do
+ update
+done
diff --git a/config/eww/scripts/daemons/vpn.sh b/config/eww/scripts/daemons/vpn.sh
new file mode 100755
index 0000000..e520338
--- /dev/null
+++ b/config/eww/scripts/daemons/vpn.sh
@@ -0,0 +1,35 @@
+#!/usr/bin/env bash
+vpn_notif="/tmp/eww-vpn-notif"
+# It can also be "tap" instead of "tun" depending on the config (?)
+vpn_interface=tun0
+
+# Human friendly name to be displayed in notifications
+declare -A vpn_names
+vpn_names=(
+ ["mullvad_gr_ath.conf"]="Greece - Athens"
+ ["mullvad_nl_ams.conf"]="Netherlands - Amsterdam"
+)
+
+update() {
+ eww update vpn-status="$1"
+}
+
+update "$(ip addr | grep "${vpn_interface}:" >/dev/null && echo on || echo off)"
+if [ "$1" == "oneshot" ]; then
+ exit
+fi
+
+# Ignore the first 3 informational lines (udevadm monitor has no quiet mode)
+udevadm monitor -u --subsystem-match=net | stdbuf -oL awk 'NR > 3' | while read -r line; do
+ if grep -q -e "add.*/devices/virtual/net/${vpn_interface}" <<< "$line"; then
+ update "on"
+ vpn_config="$(ps ax | grep -e "openvpn --config.*" | grep -v grep | awk 'NR==1 {sub(/.*--config /, ""); print}' | xargs basename)"
+ notify-send.sh -R "$vpn_notif" -u low "VPN" "Connected to ${vpn_names[$vpn_config]}"
+ elif grep -q -e "remove.*/devices/virtual/net/${vpn_interface}" <<< "$line"; then
+ update "off"
+ notify-send.sh -R "$vpn_notif" -u low "VPN" "Disconnected"
+ fi
+ eww update networks-vpn-loading=false
+done
+
+# Alternatively use `ip monitor all`
diff --git a/config/eww/scripts/daemons/weather.sh b/config/eww/scripts/daemons/weather.sh
new file mode 100755
index 0000000..544b543
--- /dev/null
+++ b/config/eww/scripts/daemons/weather.sh
@@ -0,0 +1,108 @@
+#!/usr/bin/env bash
+eww="$HOME/.config/eww/scripts"
+# Get location from pass vault
+location="$(pass-field wttr.in location)"
+# Or hardcode it
+# location="Moscow,Russia"
+
+# Associative array with weather icon codes and icons
+# https://github.com/chubin/wttr.in/blob/master/lib/constants.py
+declare -A icons
+icons[113]="" # Sunny
+icons[116]="" # PartlyCloudy
+icons[119]="" # Cloudy
+icons[122]="" # VeryCloudy
+icons[143]="" # Fog
+icons[176]="" # LightShowers
+icons[179]="" # LightSleetShowers
+icons[182]="" # LightSleet
+icons[185]="" # LightSleet
+icons[200]="" # ThunderyShowers
+icons[227]="" # LightSnow
+icons[230]="" # HeavySnow
+icons[248]="" # Fog
+icons[260]="" # Fog
+icons[263]="" # LightShowers
+icons[266]="" # LightRain
+icons[281]="" # LightSleet
+icons[284]="" # LightSleet
+icons[293]="" # LightRain
+icons[296]="" # LightRain
+icons[299]="" # HeavyShowers
+icons[302]="" # HeavyRain
+icons[305]="" # HeavyShowers
+icons[308]="" # HeavyRain
+icons[311]="" # LightSleet
+icons[314]="" # LightSleet
+icons[317]="" # LightSleet
+icons[320]="" # LightSnow
+icons[323]="" # LightSnowShowers
+icons[326]="" # LightSnowShowers
+icons[329]="" # HeavySnow
+icons[332]="" # HeavySnow
+icons[335]="" # HeavySnowShowers
+icons[338]="" # HeavySnow
+icons[350]="" # LightSleet
+icons[353]="" # LightShowers
+icons[356]="" # HeavyShowers
+icons[359]="" # HeavyRain
+icons[362]="" # LightSleetShowers
+icons[365]="" # LightSleetShowers
+icons[368]="" # LightSnowShowers
+icons[371]="" # HeavySnowShowers
+icons[374]="" # LightSleetShowers
+icons[377]="" # LightSleet
+icons[386]="" # ThunderyShowers
+icons[389]="" # ThunderyHeavyRain
+icons[392]="" # ThunderySnowShowers
+icons[395]="" # HeavySnowShowers
+icons[placeholder]=""
+icons[error]=""
+
+if [ -z "$location" ]; then
+ eww update \
+ weather-temperature="--" \
+ weather-description="Weather location unset" \
+ weather-icon="${icons[error]}"
+ exit 1
+fi
+
+update() {
+ # weather=$(curl -sf "https://wttr.in/${location}?format=j1")
+ weather=$(wget -O - "https://wttr.in/${location}?format=j1" 2>/dev/null)
+
+ if [ -n "$weather" ]; then
+ weather_temp=$(jq -r ".current_condition[0].temp_C" <<< "$weather")
+ weather_icon=$(jq -r ".current_condition[0].weatherCode" <<< "$weather")
+ weather_description=$(jq -r ".current_condition[0].weatherDesc[0].value" <<< "$weather")
+
+ eww update \
+ weather-temperature="$weather_temp" \
+ weather-description="$weather_description" \
+ weather-icon="${icons[$weather_icon]}"
+ true
+ else
+ eww update \
+ weather-temperature="--" \
+ weather-description="Weather request failed" \
+ weather-icon="${icons[error]}"
+ false
+ fi
+}
+
+if [ "$1" == "oneshot" ]; then
+ update
+ exit
+fi
+
+while true; do
+ if update; then
+ # Update succeeded, try again in 20 minutes.
+ # Use safe-sleep to fix the case when the machine is resumed after
+ # suspend which might cause up to 20m delay until the next update.
+ "${eww}/safe-sleep" 20m with-interval 10
+ else
+ # Update failed, retry in 30 seconds.
+ sleep 30
+ fi
+done
diff --git a/config/eww/scripts/do-alarms-action b/config/eww/scripts/do-alarms-action
new file mode 100755
index 0000000..fa22415
--- /dev/null
+++ b/config/eww/scripts/do-alarms-action
@@ -0,0 +1,93 @@
+#!/usr/bin/env bash
+eww="$HOME/.config/eww/scripts"
+
+
+if [[ -z "$1" ]]; then
+ echo You did not specify an action
+ exit 1
+fi
+
+action=$1
+
+alarm-quick() {
+ builtin echo alarms > /tmp/eww-widget-input-active
+ swaymsg mode _eww_input
+ eww update \
+ alarms-state=select-time-quick \
+ alarms-total-minutes=0 \
+ alarms-input-hours= \
+ alarms-input-minutes= \
+ alarms-preview-hours=00 \
+ alarms-preview-minutes=00 \
+ alarms-input-selected-field=hours \
+ alarms-input-label= \
+ alarms-preview-label=
+}
+
+alarm-standard() {
+ builtin echo alarms > /tmp/eww-widget-input-active
+ swaymsg mode _eww_input
+ eww update \
+ alarms-state=select-time-standard \
+ alarms-total-minutes=0 \
+ alarms-input-hours= \
+ alarms-input-minutes= \
+ alarms-preview-hours=00 \
+ alarms-preview-minutes=00 \
+ alarms-input-selected-field=hours \
+ alarms-input-label= \
+ alarms-preview-label=
+}
+
+previous() {
+ index="$(eww get alarms-index)"
+ json_length="$(eww get alarms-json | jq 'length')"
+ if ((json_length <= 1)) || ((index == 0)); then
+ exit 1
+ fi
+ eww update alarms-index="$(( (index - 1 + json_length) % json_length ))"
+}
+
+
+next() {
+ index="$(eww get alarms-index)"
+ json_length="$(eww get alarms-json | jq 'length')"
+ if [[ $json_length -eq 0 || $index -eq $((json_length - 1)) ]]; then
+ exit 1
+ fi
+ eww update alarms-index="$(( (index + 1) % json_length ))"
+}
+
+delete() {
+ "${eww}/alarm.sh" delete "$(eww get alarms-index)";
+ eww update alarms-index=0
+}
+
+cancel() {
+ swaymsg mode _alarms
+ eww update \
+ alarms-state=select-type
+}
+
+confirm() {
+ state="$(eww get alarms-state)"
+ if [[ "$state" == "select-time-quick" ]]; then
+ total_minutes="$(eww get alarms-total-minutes)"
+ if [[ "$total_minutes" == "0" ]]; then
+ exit 1
+ fi
+ "${eww}/alarm.sh" in "${total_minutes}"m "$(eww get alarms-preview-label)" &
+ elif [[ "$state" == "select-time-standard" ]]; then
+ "${eww}/alarm.sh" at "$(eww get alarms-preview-hours)":"$(eww get alarms-preview-minutes)" "$(eww get alarms-preview-label)" &
+ else
+ exit 1
+ fi
+
+ swaymsg mode _alarms
+ # Normally we would also `eww update alarms-state=select-type` here but
+ # this can cause flickering since alarms-state may update before
+ # alarms-json changes in alarms.sh. Therefore, we have moved this update to
+ # alarms.sh.
+}
+
+$action
diff --git a/config/eww/scripts/do-calendar-action b/config/eww/scripts/do-calendar-action
new file mode 100755
index 0000000..86698f5
--- /dev/null
+++ b/config/eww/scripts/do-calendar-action
@@ -0,0 +1,54 @@
+#!/usr/bin/env bash
+eww="$HOME/.config/eww/scripts"
+
+if [[ -z "$1" ]]; then
+ echo You did not specify an action
+ exit 1
+fi
+
+action="$1"
+
+reset() {
+ eww update calendar-selected-year="$(date +%Y)" calendar-selected-month="$(date +%m)"
+ "$eww/cal.py"
+}
+
+month_prev() {
+ month="$(eww get calendar-selected-month | sed 's/^0*//')"
+ year="$(eww get calendar-selected-year | sed 's/^0*//')"
+ if [[ "$month" == 1 ]]; then
+ month=12
+ year=$((year - 1))
+ else
+ month=$((month - 1))
+ fi
+ eww update calendar-selected-month="$month" calendar-selected-year="$year"
+ "$eww/cal.py"
+}
+
+month_next() {
+ month="$(eww get calendar-selected-month | sed 's/^0*//')"
+ year="$(eww get calendar-selected-year | sed 's/^0*//')"
+ if [[ "$month" == 12 ]]; then
+ month=1
+ year=$((year + 1))
+ else
+ month=$((month + 1))
+ fi
+ eww update calendar-selected-month="$month" calendar-selected-year="$year"
+ "$eww/cal.py"
+}
+
+year_prev() {
+ year="$(eww get calendar-selected-year)"
+ eww update calendar-selected-year=$((year - 1))
+ "$eww/cal.py"
+}
+
+year_next() {
+ year="$(eww get calendar-selected-year)"
+ eww update calendar-selected-year=$((year + 1))
+ "$eww/cal.py"
+}
+
+$action
diff --git a/config/eww/scripts/do-dock-action b/config/eww/scripts/do-dock-action
new file mode 100755
index 0000000..b690b37
--- /dev/null
+++ b/config/eww/scripts/do-dock-action
@@ -0,0 +1,37 @@
+#!/usr/bin/env bash
+if [[ -z "$1" ]]; then
+ echo You did not specify an action
+ exit 1
+fi
+
+action="$1"
+dock_item_json="$2"
+
+# Get needed variables from dock item json.
+# Careful of empty string fields returned by jq, they will be skipped by `read`.
+# 'null' is fine though.
+read -r identifier launcher state recently_used_con_id \
+ < <(jq -r '.identifier, .launcher, .state, .recently_used_con_id' <<< "$dock_item_json" | tr '\n' ' ')
+
+# echo identifier="$identifier"
+# echo launcher="$launcher"
+# echo state="$state"
+# echo recently_used_con_id="$recently_used_con_id"
+
+activate() {
+ if [[ "$state" == "empty" ]]; then
+ $launcher
+ else
+ swaymsg -q "[con_id=${recently_used_con_id}] focus" 2>/dev/null \
+ || swaymsg -q "[app_id=${identifier}] focus" 2>/dev/null \
+ || swaymsg -q "[class=${identifier}] focus" 2>/dev/null
+ fi
+}
+
+close() {
+ swaymsg -q "[con_id=${recently_used_con_id}] kill" 2>/dev/null \
+ || swaymsg -q "[app_id=${identifier}] kill" 2>/dev/null \
+ || swaymsg -q "[class=${identifier}] kill" 2>/dev/null
+}
+
+$action
diff --git a/config/eww/scripts/do-powermenu-action b/config/eww/scripts/do-powermenu-action
new file mode 100755
index 0000000..cd3ffae
--- /dev/null
+++ b/config/eww/scripts/do-powermenu-action
@@ -0,0 +1,54 @@
+#!/usr/bin/env bash
+eww="$HOME/.config/eww/scripts"
+eww_powermenu_selected_action="$(eww get powermenu-button-selected)"
+
+if [[ -z "$1" ]]; then
+ echo You did not specify an action
+ exit 1
+fi
+
+action=$1
+
+poweroff() {
+ loginctl poweroff
+}
+
+reboot() {
+ loginctl reboot
+}
+
+suspend() {
+ "${eww}/manage" hide powermenu
+ loginctl suspend
+}
+
+hibernate() {
+ "${eww}/manage" hide powermenu
+ loginctl hibernate
+}
+
+exit() {
+ swaymsg exit
+}
+lock() {
+ builtin echo lockscreen > /tmp/eww-widget-input-active
+ swaymsg mode _eww_input
+ eww update \
+ screen-locked=true \
+ powermenu-button-selected= \
+ screen-lock-input= \
+ screen-lock-input-masked= \
+ screen-lock-input-last-action=clear \
+ screen-lock-auth-failed=false
+}
+clear_selection() {
+ eww update powermenu-button-selected=
+}
+
+if [[ "$action" == "$eww_powermenu_selected_action" || "$action" == "confirm" ]]; then
+ $eww_powermenu_selected_action
+elif [[ "$action" == "clear" ]]; then
+ clear_selection
+else
+ eww update powermenu-button-selected="$action"
+fi
diff --git a/config/eww/scripts/do-sidebar-keybind b/config/eww/scripts/do-sidebar-keybind
new file mode 100755
index 0000000..e58da83
--- /dev/null
+++ b/config/eww/scripts/do-sidebar-keybind
@@ -0,0 +1,62 @@
+#!/usr/bin/env bash
+eww="$HOME/.config/eww/scripts"
+sway="$HOME/.config/sway/scripts"
+
+if [[ -z "$1" ]]; then
+ echo You did not specify a keybind
+ exit 1
+fi
+
+eww_sidebar_page="$(eww get sidebar-page-index)"
+keybind=$1
+
+org() {
+ "$sway/window-do" '[app_id=^org$] focus' "org"
+ "$eww/manage" hide sidebar
+}
+
+# These keys are passed from sway to this script
+# Add more keys in ~/.config/sway/modes.conf under the _sidebar mode and use
+# them here
+case "$eww_sidebar_page" in
+ 0 )
+ declare -A keybinds=(
+ ["j"]="swaymsg exec \$cmd_volume_lower"
+ ["k"]="swaymsg exec \$cmd_volume_raise"
+ ["v"]="swaymsg exec \$cmd_volume_mute"
+ ["J"]="light -U 10"
+ ["K"]="light -A 10"
+ )
+ ;;
+ 1 )
+ declare -A keybinds=(
+ ["o"]="org"
+ ["r"]="$eww/do-calendar-action reset"
+ ["j"]="$eww/do-calendar-action month_prev"
+ ["k"]="$eww/do-calendar-action month_next"
+ ["J"]="$eww/do-calendar-action year_prev"
+ ["K"]="$eww/do-calendar-action year_next"
+ )
+ ;;
+ 2 )
+ declare -A keybinds=(
+ ["q"]="$eww/playerctl-current stop"
+ ["f"]="$eww/media-add-to-favorites.sh"
+ ["space"]="$eww/playerctl-current play-pause"
+ ["p"]="playerctl -a pause"
+ ["slash"]="$eww/rofi-playerctl-switch.sh"
+ ["h"]="$eww/playerctl-current previous"
+ ["l"]="$eww/playerctl-current next"
+ ["H"]="$eww/playerctl-current position 10-"
+ ["L"]="$eww/playerctl-current position 10+"
+ ["v"]="swaymsg exec \$cmd_volume_mute"
+ ["j"]="swaymsg exec \$cmd_volume_lower"
+ ["k"]="swaymsg exec \$cmd_volume_raise"
+ ["J"]="$eww/playerctl-current volume 0.1-"
+ ["K"]="$eww/playerctl-current volume 0.1+"
+ )
+ ;;
+esac
+
+${keybinds[$keybind]}
+
diff --git a/config/eww/scripts/init b/config/eww/scripts/init
new file mode 100755
index 0000000..9808152
--- /dev/null
+++ b/config/eww/scripts/init
@@ -0,0 +1,92 @@
+#!/usr/bin/env bash
+eww_root="$HOME/.config/eww"
+eww="$eww_root/scripts"
+eww_daemons="$eww/daemons"
+
+# Normally, we would add something like `output eDP-1 scale 1.5` to our sway
+# config which would make all clients (including eww) scale automatically.
+# However, this can cause blurry xwayland apps.
+# See: https://github.com/swaywm/sway/issues/2966
+# Fix: https://gitlab.freedesktop.org/xorg/xserver/-/merge_requests/733
+# --------------------------------------------------------------------------
+# Workaround: Use scale 1 in sway BUT scale eww manually :(
+# To do that we need to apply scaling to both *.yuck values and *.scss values.
+# - In yuck we can use a (defvar) to set the scale or use get_env() to get an env
+# var and multiply all values with it.
+# - In scss we can declare a $scale var and multiply all values with it.
+# See DPI function in #~/.config/eww/eww.scss
+# To configure both in one place (this script), we use the EWW_SCALE variable to
+# auto-generate a scss file declaring the $scale variable. This file will be
+# imported by eww.scss. In *.yuck we will use get_env(EWW_SCALE).
+# To see the result of changing this value, we have to kill eww and re-run this
+# script that it can see the new value of EWW_SCALE:
+# pkill eww; ~/.config/eww/scripts/init
+export EWW_SCALE=1.5
+# shellcheck disable=2016
+echo -e '// Auto-generated by ~/.config/eww/scripts/init\n$scale: '"$EWW_SCALE"';' > "$eww_root/_scale.scss"
+
+open_all() {
+ # It is important to open the background widget last to avoid it being above other widgets
+ eww open-many \
+ bottom-bar \
+ workspaces \
+ ;
+ # sidebar-activator \
+ # background \
+}
+
+# Start eww and widgets
+if pgrep eww >/dev/null; then
+ open_all
+else
+ eww daemon
+ sleep 1
+ open_all
+fi
+
+# Start eww script daemons if not already running
+# shellcheck disable=2009
+running_daemons="$(ps x | grep "$eww")"
+start() {
+ daemon="$1"
+ shift
+
+ if [[ $# == 0 ]]; then
+ [[ "$running_daemons" == *"$eww_daemons/${daemon}"* ]] && found=true || found=false
+ else
+ [[ "$running_daemons" == *"$eww_daemons/${daemon} $*"* ]] && found=true || found=false
+ fi
+
+ if ! $found; then
+ echo "> starting daemon ${daemon} $*"
+ "$eww_daemons/${daemon}" "$@" >/dev/null &
+ fi
+}
+
+sleep 1
+
+# Initial updates
+"$eww/networks.sh" update_networks
+
+# Daemons
+start sway-workspaces.py
+start sway-modes.py
+start sway-split-indicator.py
+start sway-dock.py
+start microphone.sh
+start brightness.sh
+start volume.sh
+start charger.sh
+start weather.sh
+start vpn.sh
+start days-of-the-week.sh
+start gpu.sh
+start kdeconnect.sh
+start uptime.sh
+start media.sh
+start alarms.sh
+start agenda.sh
+start network-scan.sh
+start network-events.sh
+start input-buffer.sh alarms
+start input-buffer.sh lockscreen
diff --git a/config/eww/scripts/input-controller.sh b/config/eww/scripts/input-controller.sh
new file mode 100755
index 0000000..95eb482
--- /dev/null
+++ b/config/eww/scripts/input-controller.sh
@@ -0,0 +1,19 @@
+#!/usr/bin/env bash
+# Forwards input to the correct script depending on eww state
+eww="$HOME/.config/eww/scripts"
+input=$1
+
+declare -A sinks
+sinks=(
+ ["lockscreen"]="${eww}/input-sink-lockscreen.sh"
+ ["alarms"]="${eww}/input-sink-alarms.sh"
+)
+
+# All widgets that can accept input update this file with their name when their
+# input is active. This is faster than using an eww variable since an `eww get`
+# is more expensive than writing to / reading from tmpfs
+widget=$( "$pipe"
diff --git a/config/eww/scripts/input-sink-alarms.sh b/config/eww/scripts/input-sink-alarms.sh
new file mode 100755
index 0000000..cbe79d3
--- /dev/null
+++ b/config/eww/scripts/input-sink-alarms.sh
@@ -0,0 +1,112 @@
+#!/usr/bin/env bash
+eww="$HOME/.config/eww/scripts"
+
+if [[ -z "$1" ]]; then
+ echo You did not specify a key
+ exit 1
+fi
+
+declare -A next_field
+next_field=(
+ ["hours"]="minutes"
+ ["minutes"]="label"
+ ["label"]="hours"
+)
+
+declare -A previous_field
+previous_field=(
+ ["hours"]="label"
+ ["minutes"]="hours"
+ ["label"]="minutes"
+)
+
+declare -A field_char_limit
+field_char_limit=(
+ ["hours"]=2
+ ["minutes"]=2
+ ["label"]=16
+)
+
+declare -A field_max
+field_max=(
+ ["hours"]=23
+ ["minutes"]=59
+)
+
+format_field() {
+ field_type=$1
+ input=$2
+ if [[ "$field_type" =~ ^(hours|minutes)$ ]]; then
+ printf "%02d" "${input}"
+ else
+ echo "$input"
+ fi
+}
+
+key=$1
+
+declare -A inputs
+inputs["hours"]="$(eww get alarms-input-hours)"
+inputs["minutes"]="$(eww get alarms-input-minutes)"
+inputs["label"]="$(eww get alarms-input-label)"
+
+selected_field="$(eww get alarms-input-selected-field)"
+char_limit="${field_char_limit[$selected_field]}"
+total_minutes="$(eww get alarms-total-minutes)"
+current_input="${inputs[$selected_field]}"
+input_length=${#current_input}
+
+if [[ "$key" == "backspace" ]]; then
+ if [[ "$input_length" == "0" ]]; then
+ exit 1
+ fi
+
+ inputs[$selected_field]="${current_input::-1}"
+
+ hours=${inputs["hours"]}
+ minutes=${inputs["minutes"]}
+ total_minutes=$((hours * 60 + minutes))
+
+ eww update \
+ alarms-input-"${selected_field}"="${inputs[$selected_field]}" \
+ alarms-preview-"${selected_field}"="$(format_field "$selected_field" "${inputs[$selected_field]}")" \
+ alarms-total-minutes="$total_minutes"
+elif [[ "$key" == "c-backspace" ]]; then
+ eww update \
+ alarms-input-"${selected_field}"= \
+ alarms-preview-"${selected_field}"=
+elif [[ "$key" == "escape" ]]; then
+ "${eww}/do-alarms-action" cancel
+elif [[ "$key" == "return" ]]; then
+ "${eww}/do-alarms-action" confirm
+elif [[ "$key" == "s-tab" ]]; then
+ eww update \
+ alarms-input-selected-field="${previous_field[$selected_field]}"
+elif [[ "$key" == "tab" ]]; then
+ eww update \
+ alarms-input-selected-field="${next_field[$selected_field]}"
+elif (( input_length < char_limit )); then
+ new_input="${current_input}${key}"
+ if [[ "$selected_field" =~ ^(hours|minutes)$ ]]; then
+ if [[ ! $key =~ [0-9] ]]; then
+ exit 1
+ fi
+
+ if (( new_input > field_max[$selected_field] )); then
+ inputs[$selected_field]="${field_max[$selected_field]}"
+ else
+ inputs[$selected_field]="$new_input"
+ fi
+ else
+ inputs[$selected_field]=$new_input
+ fi
+
+ hours=${inputs["hours"]}
+ minutes=${inputs["minutes"]}
+ total_minutes=$((hours * 60 + minutes))
+
+ eww update \
+ alarms-input-"${selected_field}"="${inputs[$selected_field]}" \
+ alarms-preview-"${selected_field}"="$(format_field "$selected_field" "${inputs[$selected_field]}")" \
+ alarms-total-minutes="$total_minutes"
+fi
diff --git a/config/eww/scripts/input-sink-lockscreen.sh b/config/eww/scripts/input-sink-lockscreen.sh
new file mode 100755
index 0000000..6565b4d
--- /dev/null
+++ b/config/eww/scripts/input-sink-lockscreen.sh
@@ -0,0 +1,40 @@
+#!/usr/bin/env bash
+eww="$HOME/.config/eww/scripts"
+
+if [[ -z "$1" ]]; then
+ echo You did not specify a key
+ exit 1
+fi
+
+key=$1
+current_input="$(eww get screen-lock-input)"
+current_input_length=${#current_input}
+if [[ "$key" == "backspace" ]]; then
+ if (( current_input_length == 0 )); then
+ exit 1
+ fi
+ eww update \
+ screen-lock-input="${current_input::-1}" \
+ screen-lock-input-masked="$(printf '%*s\n' "$((current_input_length - 1))" '' | tr ' ' '*')" \
+ screen-lock-input-last-action="$([[ $((current_input_length - 1)) == 0 ]] && echo clear || echo delete)" \
+ screen-lock-auth-failed=false
+elif [[ "$key" == "c-backspace" || "$key" == "escape" ]]; then
+ eww update \
+ screen-lock-input= \
+ screen-lock-input-masked= \
+ screen-lock-input-last-action=clear \
+ screen-lock-auth-failed=false
+elif [[ "$key" == "return" ]]; then
+ if [[ "$(eww get screen-lock-input)" == "$(eww get screen-lock-password)" ]]; then
+ "${eww}/manage" hide powermenu
+ eww update screen-locked=false screen-lock-input-last-action=clear
+ else
+ eww update screen-lock-auth-failed=true
+ fi
+elif (( current_input_length < 16 )); then
+ eww update \
+ screen-lock-input="${current_input}${key}" \
+ screen-lock-input-masked="$(printf '%*s\n' "$((current_input_length + 1))" '' | tr ' ' '*')" \
+ screen-lock-input-last-action=insert \
+ screen-lock-auth-failed=false
+fi
diff --git a/config/eww/scripts/manage b/config/eww/scripts/manage
new file mode 100755
index 0000000..d53c40d
--- /dev/null
+++ b/config/eww/scripts/manage
@@ -0,0 +1,284 @@
+#!/usr/bin/env bash
+# This script:
+# - Changes the visibility of an eww widget, by also setting its state variable
+# and accounting for a delay to allow the hiding animation to complete.
+# Related: https://github.com/elkowar/eww/issues/196
+# - Manages pre-show, post-show, pre-hide and post-hide commands.
+# - Automatically enables or disables the respective sway binding mode when
+# needed.
+# - Automatically opens the dismiss-overlay widget when needed which can be used
+# to achieve "close when clicking outside the widget" functionality.
+# - Before showing the selected widget, it automatically hides all other widgets
+# if needed.
+#
+# The script assumes that:
+# - The widget state variable is in the format ${widget_name}-visible
+# (e.g. variable `sidebar-visible` refers to the visibility state of the
+# `sidebar` widget)
+# Value: true/false
+# - Windows that need overlays in secondary monitors declare an `is-secondary`
+# argument.
+#
+# Usage:
+# manage (show|hide|toggle|hide_all_except) widget_name
+
+eww="$HOME/.config/eww/scripts"
+
+# Fullscreen widgets will be shown in the primary monitor, and if needed,
+# overlays will be shown in the following secondary monitors.
+# You can find OUTPUT_NAME with `swaymsg -t get_outputs`
+secondary_monitors=(
+ "HEADLESS-3"
+ "HDMI-A-1"
+)
+
+# Overlay widgets are spawned in all monitors (using the `is-secondary` argument
+# to customize their appearance or functionality depending on monitor type)
+declare -A overlay_widgets
+overlay_widgets[powermenu]=true
+
+# How long to wait for the revealer animation to complete. This should be
+# match the revealer delay in the respective *.yuck files.
+declare -A revealer_delay
+revealer_delay[sidebar]=0.3
+revealer_delay[powermenu]=0.3
+revealer_delay[split-indicator]=0.3
+revealer_delay[mode-indicator]=0.3
+revealer_delay[microphone-indicator]=0.2
+revealer_delay[networks]=0.3
+revealer_delay[alarms]=0.3
+
+# Only one of these should be visible at a time
+mutually_exclusive_widgets=(
+ "sidebar"
+ "networks"
+ "alarms"
+ "powermenu"
+)
+
+# These widgets capture keys while they are visible (through sway binding
+# modes). The convention currently is that for every such widget there exists a
+# sway binding mode with the same name that starts with an underscore.
+declare -A mode_widget
+mode_widget[sidebar]="_sidebar"
+mode_widget[networks]="_networks"
+mode_widget[alarms]="_alarms"
+mode_widget[powermenu]="_powermenu"
+
+# Clicking out of these widgets should close them
+dismiss_when_clicking_outside_widgets=(
+ "sidebar"
+ "networks"
+ "alarms"
+)
+
+declare -A pre_show_cmds
+# shellcheck disable=2016
+pre_show_cmds[networks]='[ "$(eww get networks-ping-loading)" = "true" ] || eww update networks-ping-result=ping'
+
+declare -A post_show_cmds
+post_show_cmds[networks]="($eww/networks.sh update_networks &)"
+
+declare -A pre_hide_cmds
+
+declare -A post_hide_cmds
+post_hide_cmds[powermenu]='eww update powermenu-button-selected= screen-lock-input='
+post_hide_cmds[alarms]='eww update alarms-state=select-type'
+
+# ------------------------------------------------------------
+
+cmd="$1"
+widget_name="$2"
+active_windows="$(eww active-windows 2>/dev/null)"
+active_monitors="$(swaymsg -t get_outputs | jq -r '.[] | .name')"
+
+# Helpers
+_is_secondary_monitor() {
+ mon="$1"
+ grep -q "^${mon}$" <<< "$(printf '%s\n' "${secondary_monitors[@]}")"
+}
+
+_open() {
+ widget_name="$1"
+
+ if [[ -v overlay_widgets["$widget_name"] ]]; then
+ extra_args="--arg ${widget_name}:is-secondary=false"
+ for mon in ${active_monitors}; do
+ if _is_secondary_monitor "$mon"; then
+ extra_args+=" ${widget_name}:${widget_name}-${mon} --arg ${widget_name}-${mon}:screen=${mon} --arg ${widget_name}-${mon}:is-secondary=true"
+ fi
+ done
+ fi
+
+ # shellcheck disable=2086
+ eww open-many "$widget_name" $extra_args >/dev/null
+}
+
+_close() {
+ widget_name="$1"
+
+ if [[ -v overlay_widgets["$widget_name"] ]]; then
+ for mon in ${active_monitors}; do
+ if _is_secondary_monitor "$mon"; then
+ extra_args+=" ${widget_name}-${mon}"
+ fi
+ done
+ fi
+
+ # shellcheck disable=2086
+ eww close "$widget_name" $extra_args >/dev/null
+}
+
+_mode_enable() {
+ widget_name="$1"
+ if [[ -v mode_widget["$widget_name"] ]]; then
+ swaymsg -q mode "${mode_widget[$widget_name]}"
+ fi
+}
+_mode_disable() {
+ widget_name="$1"
+ if [[ -v mode_widget["$widget_name"] ]]; then
+ swaymsg -q mode default
+ fi
+}
+
+_pre_show() {
+ widget_name="$1"
+ if [[ -v pre_show_cmds["$widget_name"] ]]; then
+ sh <<< "${pre_show_cmds[$widget_name]}"
+ fi
+ if [[ ${mutually_exclusive_widgets[*]} =~ $widget_name ]]; then
+ hide_all_except "$widget_name"
+ fi
+}
+
+_post_show() {
+ widget_name="$1"
+ if [[ -v post_show_cmds["$widget_name"] ]]; then
+ sh <<< "${post_show_cmds[$widget_name]}"
+ fi
+ if [[ ${dismiss_when_clicking_outside_widgets[*]} =~ $widget_name ]]; then
+ secondary_overlays=''
+ # Show an overlay for all active secondary monitors
+ for mon in ${active_monitors}; do
+ if _is_secondary_monitor "$mon"; then
+ secondary_overlays+=" dismiss-overlay:dismiss-overlay-${mon} --arg dismiss-overlay-${mon}:screen=${mon}"
+ fi
+ done
+
+ # shellcheck disable=2086
+ eww open-many \
+ dismiss-overlay:dismiss-overlay \
+ $secondary_overlays \
+ --arg widget-to-dismiss="${widget_name}" \
+ >/dev/null;
+ fi
+}
+
+_pre_hide() {
+ widget_name="$1"
+ if [[ -v pre_hide_cmds["$widget_name"] ]]; then
+ sh <<< "${pre_hide_cmds[$widget_name]}"
+ fi
+ if [[ ${dismiss_when_clicking_outside_widgets[*]} =~ $widget_name ]]; then
+ secondary_overlays=''
+ # Hide the overlay for all active secondary monitors
+ for mon in ${active_monitors}; do
+ if _is_secondary_monitor "$mon"; then
+ secondary_overlays+=" dismiss-overlay-${mon}"
+ fi
+ done
+
+ # shellcheck disable=2086
+ eww close \
+ dismiss-overlay \
+ $secondary_overlays \
+ >/dev/null;
+ fi
+}
+
+_post_hide() {
+ widget_name="$1"
+ if [[ -v post_hide_cmds["$widget_name"] ]]; then
+ sh <<< "${post_hide_cmds[$widget_name]}"
+ fi
+}
+
+_show() {
+ widget_name="$1"
+ _pre_show "$widget_name"
+ _mode_enable "$widget_name"
+ _open "$widget_name"
+ eww update "${widget_name}"-visible=true
+ _post_show "$widget_name"
+}
+
+_hide() {
+ widget_name="$1"
+ delay="${revealer_delay[$widget_name]:-0}" # default delay is 0
+ _pre_hide "$widget_name"
+ _mode_disable "$widget_name"
+ eww update "${widget_name}"-visible=false
+ sleep "$delay"
+ _close "$widget_name"
+ _post_hide "$widget_name"
+}
+
+# Available commands
+toggle() {
+ widget_name="$1"
+ if [ "$(eww get "${widget_name}-visible")" = "false" ]; then
+ _show "$widget_name"
+ else
+ _hide "$widget_name"
+ fi
+}
+
+show() {
+ if [ "$(eww get "${widget_name}-visible")" = "false" ]; then
+ _show "$widget_name"
+ fi
+}
+
+hide() {
+ if [ "$(eww get "${widget_name}-visible")" = "true" ]; then
+ _hide "$widget_name"
+ fi
+}
+
+hide_all_except() {
+ widget_except="$1"
+
+ for widget in "${mutually_exclusive_widgets[@]}"; do
+ if [[ ! "$widget" == "$widget_except" ]] && grep -e "^${widget}:" >/dev/null <<< "$active_windows"; then
+ delay="${revealer_delay[$widget]:-0}" # default delay is 0
+ # We do not call the `hide` helper because we do not necessarily
+ # want to mess with the sway modes.
+ # The widget to be activated will set its own mode through the
+ # `show` function if needed.
+ # If:
+ # - Widget to be hidden uses a mode AND
+ # - Widget to stay active does not use any mode
+ # -> We need to disable the mode.
+ # We need to do this outside the subshell running in the background
+ # to avoid a race condition causing the mode that was enabled in
+ # `show` to be disabled right after.
+ if [[ -v mode_widget["$widget"] && ! -v mode_widget["$widget_except"] ]]; then
+ swaymsg -q mode default
+ fi
+
+ # Run in the background to avoid delaying the next command while
+ # waiting for the animation
+ (
+ _pre_hide "$widget"
+ eww update "${widget}-visible"=false
+ sleep "$delay"
+ _close "$widget"
+ _post_hide "$widget"
+ )&
+ fi
+ done
+}
+
+# Run selected cmd
+$cmd "$widget_name"
diff --git a/config/eww/scripts/media-add-to-favorites.sh b/config/eww/scripts/media-add-to-favorites.sh
new file mode 100755
index 0000000..3057b26
--- /dev/null
+++ b/config/eww/scripts/media-add-to-favorites.sh
@@ -0,0 +1,14 @@
+#!/usr/bin/env bash
+# This script assumes you are saving your favorite songs in a text file like a
+# caveman.
+favorites_file="$HOME/mus/playlists/songs"
+
+# Get current media information displayed in eww
+eww_media_json="$(eww get media-json)"
+artist="$(jq -r '.artist' <<< "$eww_media_json")"
+title="$(jq -r '.title' <<< "$eww_media_json")"
+
+line="${artist} - ${title}"
+echo "$line" >> "$favorites_file"
+
+notify-send.sh -u low -R /tmp/eww-media-favorites-notif "Added to favorites" "$line"
diff --git a/config/eww/scripts/networks.sh b/config/eww/scripts/networks.sh
new file mode 100755
index 0000000..f459137
--- /dev/null
+++ b/config/eww/scripts/networks.sh
@@ -0,0 +1,216 @@
+#!/usr/bin/env bash
+# Network widget controls
+# Intended for IWD not NetworkManager.
+interface=wlan0
+networks_dir=/var/lib/iwd/
+rofi="$HOME/.config/eww/scripts/rofi-with-mode-restore networks"
+
+is_saved_network() {
+ local key="$1"
+ [[ -v saved_networks_assoc["$key"] ]]
+}
+
+update_networks() {
+ iwctl station $interface scan
+ iwctl_out="$(iwctl station $interface get-networks)"
+
+ # iwctl version 2.22 BUG: Right after manually disconnecting from a network,
+ # the next few get-networks runs may fail with one of the following errors:
+ # 1. Device wlan0 not found.
+ # No station on device: 'wlan0'
+ # 2. No matching method found
+ # Failed to retrieve IWD dbus objects, quitting...
+ # It also does not correctly set a failure return code and it prints errors
+ # in stdout.
+ # HACK: Retry until get-networks succeeds. Usually takes 1-2 retries.
+ # while true; do
+ # iwctl_out="$(iwctl station $interface get-networks)"
+ # [[ ! "$iwctl_out" =~ "Available networks" ]] || break
+ # sleep 0.1
+ # done
+
+ # Remove the first 4 lines of output (headers) and escape codes.
+ # iwctl prints color escape codes even if piped to another command :(
+ networks="$(sed -e '1,4d;s/\x1b\[[0-9;]*m/SIGNAL_END/g;s/SIGNAL_END\*.*//;s/SIGNAL_END//g' <<< "$iwctl_out")"
+
+ if [[ "$networks" =~ "No networks available" ]]; then
+ eww update networks-json="[]"
+ exit
+ fi
+
+ # Find saved networks
+ # We can connect to those without entering a password
+ saved_networks="$(find "$networks_dir" -maxdepth 1 -name "*.psk" | sed 's@/var/lib/iwd/@@; s@\.psk$@@')"
+ declare -A saved_networks_assoc
+ while IFS= read -r network; do
+ saved_networks_assoc["$network"]=true
+ done <<< "$saved_networks"
+
+ json='['
+ while read -r line; do
+ first_column="$(awk '{print $1}' <<< "$line")"
+
+ if [[ "$first_column" = ">" ]]; then
+ selected="true"
+ name="$(awk '{for (i=2; i<=NF-2; i++) printf $i " "}' <<< "$line" | sed 's/^[[:blank:]]*//;s/[[:blank:]]*$//')"
+ else
+ selected="false"
+ name="$(awk '{for (i=1; i<=NF-2; i++) printf $i " "}' <<< "$line" | sed 's/^[[:blank:]]*//;s/[[:blank:]]*$//')"
+ fi
+
+ saved="$(is_saved_network "$name" && echo "true" || echo "false")"
+
+ signal_strength="$(awk '{ last_col = $NF; star_count = gsub("\\*", "", last_col); print star_count }' <<< "$line")"
+
+ # Escape double quotes from network name
+ json+="{\"name\": \"${name//\"/\\\"}\","
+ json+="\"selected\": \"$selected\","
+ json+="\"saved\": \"$saved\","
+ json+="\"signal_strength\": $signal_strength},"
+ done <<< "$networks"
+ json=${json::-1} # Remove last comma
+ json+=']'
+
+ eww update networks-json="$json"
+}
+
+scan() {
+ if [[ "$(eww get networks-scan-loading)" == "true" ]]; then
+ exit 1
+ fi
+ eww update networks-scan-loading=true
+ iwctl station $interface scan
+ for (( i = 0; i < 5; i++ )); do
+ sleep 1
+ update_networks
+ done
+ eww update networks-scan-loading=false
+}
+
+disconnect() {
+ network_name="$1"
+ iwctl known-networks "$network_name" set-property AutoConnect no
+ iwctl station $interface disconnect
+ # We do not need to update_networks here. It will happen automatically due
+ # to ./daemons/network-events.sh
+}
+
+connect() {
+ networks_json="$(eww get networks-json)"
+ network_name="$1"
+ saved=$(jq -r --arg name "$network_name" '.[] | select(.name == $name) | .saved' <<< "$networks_json")
+
+ if [[ "$saved" == "true" ]]; then
+ iwctl_connect_out="$(iwctl station "$interface" connect "$network_name")"
+ else
+ if ! passphrase="$($rofi -dmenu -no-fixed-num-lines -p "Enter passphrase >")"; then
+ exit 1
+ fi
+
+ if [[ "$passphrase" == "" ]]; then
+ iwctl_connect_out="$(iwctl station "$interface" connect "$network_name")"
+ else
+ iwctl_connect_out="$(iwctl station "$interface" connect "$network_name" --passphrase "$passphrase")"
+ fi
+ fi
+ iwctl known-networks "$network_name" set-property AutoConnect yes
+
+ if [[ "$iwctl_connect_out" =~ "Operation failed" ]]; then
+ notify-send -u low "Networks" "Could not connect to $network_name"
+ fi
+
+ # We do not need to update_networks here. It will happen automatically due
+ # to ./daemons/network-events.sh
+}
+
+forget() {
+ network_name=$1
+ answer="$(echo -e "No\nYes" | $rofi -i -p "Forget network $network_name?" -dmenu)"
+ if [[ "$answer" == "Yes" ]]; then
+ iwctl known-networks "$network_name" forget
+ update_networks
+ else
+ exit 1
+ fi
+}
+
+speedtest() {
+ chromium --app='https://www.speedtest.net'
+}
+
+rofi_select() {
+ source ~/.config/sway/scripts/sway-colors
+ grey="${x8:-"#666666"}"
+ red="${x1:-"#de5063"}"
+ yellow="${x3:-"#fcc93a"}"
+ green="${x2:-"#5de381"}"
+ blue="${x4:-"#4287f5"}"
+ networks_json="$(eww get networks-json)"
+ # Unfortunately, breaking the if statement inside jq into multiple lines
+ # will break the rofi list
+ index="$(jq -r ' .[] | .name as $name | .selected as $selected |
+ .saved as $saved | .signal_strength as $signal_strength |
+ "\( if $signal_strength == 1 then "⏽" elif $signal_strength == 2 then "⏽" elif $signal_strength == 3 then "⏽" else "⏽" end) \( if $selected == "true" then "" elif $saved == "true" then "" else "" end) \($name)" ' <<< "$networks_json" | $rofi -i -p 'Networks' -dmenu -format "i" -markup-rows)"
+
+ if [[ "$index" == "" || "$index" == "-1" ]]; then
+ exit 1
+ fi
+ network_obj="$(jq -r ".[$index]" <<< "$networks_json")"
+ network_name="$(jq -r ".name" <<< "$network_obj")"
+
+ choices=""
+ if [ "$(jq -r ".selected" <<< "$network_obj")" == "true" ]; then
+ choices+="Disconnect\n"
+ else
+ choices+="Connect\n"
+ fi
+
+ if [ "$(jq -r ".saved" <<< "$network_obj")" == "true" ]; then
+ choices+="Forget\n"
+ fi
+
+ choices=${choices::-2} # Remove last \n
+ answer="$(echo -e "$choices" | $rofi -i -p "Network $network_name" -dmenu)"
+ if [[ "$answer" == "Disconnect" ]]; then
+ disconnect "$@"
+ elif [[ "$answer" == "Connect" ]]; then
+ connect "$network_name"
+ elif [[ "$answer" == "Forget" ]]; then
+ forget "$network_name"
+ else
+ exit 1
+ fi
+}
+
+ping() {
+ if [[ "$(eww get networks-ping-loading)" == "true" ]]; then
+ exit 1
+ fi
+ eww update networks-ping-loading=true networks-ping-result="ping"
+ # packet_loss=''
+ stdbuf -oL ping -w 30 -c 10 1.1.1.1 2>&1 | while read -r line; do
+ if [[ "$line" =~ "time=" ]]; then
+ ping="$(awk -F 'time=' '{gsub(/ ms/, "", $2); printf("%d\n", $2 + 0.5)}' <<< "$line")"
+ eww update networks-ping-result="${ping}ms"
+ # elif [[ "$line" =~ "packet loss" ]]; then
+ # packet_loss="/$(awk '{print $6}' <<< "$line")"
+ elif [[ "$line" =~ "rtt min" ]]; then
+ avg="$(awk -F '[ /=]' '{printf("%d\n", $9 + 0.5)}' <<< "$line")"
+ # eww update networks-ping-result="${avg}ms${packet_loss}"
+ eww update networks-ping-result="${avg}ms"
+ elif [[ "$line" =~ "Network is unreachable" ]]; then
+ eww update networks-ping-result="dc"
+ fi
+ done
+ eww update networks-ping-loading=false
+}
+
+if [[ -z "$1" ]]; then
+ echo No argument given
+ exit 1
+fi
+
+# Call whichever function 1st argument says
+cmd=$1
+shift
+$cmd "$@"
diff --git a/config/eww/scripts/playerctl-current b/config/eww/scripts/playerctl-current
new file mode 100755
index 0000000..70dc895
--- /dev/null
+++ b/config/eww/scripts/playerctl-current
@@ -0,0 +1,35 @@
+#!/usr/bin/env bash
+# Controls the player currently visible in eww widgets
+# To control the correct player we need to find its instance name.
+# We cannot just use the player name reported by `playerctl --follow` because
+# there can be for example multiple mpv players, all reporting "mpv" as the
+# player name, while their instance name could be something like
+# "mpv.instance12345".
+# Thus, instead:
+# We can retrieve all player instance names with `playerctl --list-all` and
+# filter by player name.
+# Then we can use the media title shown in eww and compare it with the title of
+# all matching players to find the correct player name instance.
+
+eww_media_json="$(eww get media-json)"
+current_name="$(jq -r .player_name <<< "$eww_media_json")"
+all_matching="$(playerctl --list-all | grep "^${current_name}")"
+
+# When comparing strings, remove all whitespace and ellipsis that was used to
+# truncate the title in eww
+current_title="$(jq -r .title <<< "$eww_media_json" | tr -d "[:space:]" | sed 's/\.\.\.$//')"
+player_name=''
+for match in $all_matching ; do
+ actual_player_title="$(playerctl --player="$match" --format='{{title}}' metadata | tr -d "[:space:]")"
+ if [[ "$actual_player_title" == *"$current_title"* ]]; then
+ player_name="$match"
+ break;
+ fi
+done
+
+if [[ "$player_name" == '' ]]; then
+ # If we could not find it, let playerctl handle it
+ playerctl "$@"
+else
+ playerctl --player="$player_name" "$@"
+fi
diff --git a/config/eww/scripts/rofi-playerctl-switch.sh b/config/eww/scripts/rofi-playerctl-switch.sh
new file mode 100755
index 0000000..e9503f3
--- /dev/null
+++ b/config/eww/scripts/rofi-playerctl-switch.sh
@@ -0,0 +1,30 @@
+#!/usr/bin/env bash
+rofi="$HOME/.config/eww/scripts/rofi-with-mode-restore sidebar"
+# Select a player from all available players reported by `playerctl --list-all`
+# using rofi. The selected player's volume will be adjusted very very very
+# slightly so that `playerctl --follow` will detect it as the the currently
+# active player.
+# Note: Sadly this does not work with players that do not implement the mpris
+# volume interface (e.g. Firefox, Chromium).
+players="$(playerctl --ignore-player=firefox,chromium --list-all)"
+players_array=()
+for player in $players; do
+ if ! title="$(playerctl --player="$player" --format='{{title}}' metadata 2>/dev/null)"; then
+ # Failed to get title, skip
+ continue
+ fi
+ players_array+=("$player")
+ rofi_input+="$title"$'\n'
+done
+
+rofi_input=${rofi_input::-1} # Remove last new line
+choice="$(echo "$rofi_input" | $rofi -dmenu -format d -i -p "> Select player")"
+
+if [[ -z "$choice" ]]; then
+ exit 1
+fi
+
+selected_player="${players_array[$((choice - 1))]}"
+tiny_amount="0.000001"
+playerctl --player="$selected_player" volume "$tiny_amount"+ &
+playerctl --player="$selected_player" volume "$tiny_amount"-
diff --git a/config/eww/scripts/rofi-vpn.sh b/config/eww/scripts/rofi-vpn.sh
new file mode 100755
index 0000000..12ebe05
--- /dev/null
+++ b/config/eww/scripts/rofi-vpn.sh
@@ -0,0 +1,83 @@
+#!/usr/bin/env bash
+rofi="$HOME/.config/eww/scripts/rofi-with-mode-restore networks"
+source ~/.config/sway/scripts/sway-colors
+
+disconnect() {
+ eww update networks-vpn-loading=true
+ vpn_config=$1
+ pid=$2
+ kill "$pid"
+}
+
+connect() {
+ eww update networks-vpn-loading=true
+ # First disconnect from existing VPN(s)
+ pids="$(openvpn_pid "all")"
+ # shellcheck disable=2086
+ kill $pids 2>/dev/null
+
+ vpn_config=$1
+ # We need to cd here so that openvpn can find other configuration files needed to connect
+ cd "${vpn_dirs[$vpn_config]}" || exit
+ sudo openvpn --config "${vpn_dirs[$vpn_config]}/$vpn_config" >/dev/null & disown
+}
+
+openvpn_pid() {
+ if [[ "$1" == "all" ]]; then
+ # Find all "openvpn --config" processes
+ ps ax | grep -e "openvpn --config.*" | grep -v grep | awk '{printf "%s ", $1 }'
+ else
+ # Only find the first process with the specific config
+ ps ax | grep -e "openvpn --config.*$1" | grep -v grep | awk 'NR==1 {printf "%s", $1 }'
+ fi
+}
+
+# We need a non associative array
+vpns=(
+ "mullvad_de_fra.conf"
+ "mullvad_nl_ams.conf"
+)
+
+# VPN configs and their parent directory
+declare -A vpn_dirs
+vpn_dirs=(
+ ["mullvad_de_fra.conf"]="$HOME/dox/mullvadvpn/mullvad_config_linux_de_fra"
+ ["mullvad_nl_ams.conf"]="$HOME/dox/mullvadvpn/mullvad_config_linux_nl_ams"
+)
+
+# Human friendly name to be displayed in rofi
+declare -A vpn_names
+vpn_names=(
+ ["mullvad_de_fra.conf"]="Germany - Frankfurt"
+ ["mullvad_nl_ams.conf"]="Netherlands - Amsterdam"
+)
+
+# This will be filled by do_rofi
+declare -A actions
+
+do_rofi() {
+ rofi_input=''
+ for conf in "${vpns[@]}"; do
+ pid="$(openvpn_pid "$conf")"
+ if [[ "$pid" == "" ]]; then
+ actions[$conf]="connect $conf"
+ color=${x8:-"#666666"}
+ else
+ actions[$conf]="disconnect $conf $pid"
+ color="${x2:-"#5de381"}"
+ fi
+
+ line="⏽ ${vpn_names[$conf]}"
+ rofi_input=$rofi_input${line}\\n
+ done
+ rofi_input=${rofi_input::-2}
+
+ index="$(echo -e "$rofi_input" | $rofi -markup-rows -i -p 'VPN' -dmenu -format 'i')"
+ if [[ "$index" == "" || "$index" == "-1" ]]; then
+ exit 1
+ fi
+
+ ${actions[${vpns[$index]}]}
+}
+
+do_rofi
diff --git a/config/eww/scripts/rofi-with-mode-restore b/config/eww/scripts/rofi-with-mode-restore
new file mode 100755
index 0000000..40cca0a
--- /dev/null
+++ b/config/eww/scripts/rofi-with-mode-restore
@@ -0,0 +1,35 @@
+#!/usr/bin/env bash
+# In this setup, we heavily use sway/i3 binding modes for controlling widgets
+# with the keyboard. There are times when we may open a rofi menu while a
+# binding mode is enabled. In this case, the binding mode has priority over
+# rofi, leading to missed keystrokes in rofi and accidental keypresses in the
+# active eww windows.
+# Solution:
+# Use this script as a rofi wrapper for these cases.
+# It will temporarily disable the current binding mode and optionally restore it
+# after rofi closes.
+# Note: The user is able to close the active eww window while rofi is open. In
+# this case we do not restore the mode.
+# Assumes that:
+# 1. the first argument is the widget name (e.g. mywidget).
+# 2. the sway/i3 binding mode used for this widget is called _mywidget
+# 3. there is an eww variable tracking whether the widget is visible called mywidget-visible
+# Note: We also could use `eww active-windows | grep widget_name` instead of
+# retrieving this variable.
+# Usage:
+# rofi-with-mode-restore
+# rofi-with-mode-restore networks -dmenu -markup-rows -i -p "Choose network"
+
+if [ -z "$1" ]; then
+ exit 1
+fi
+
+widget_name="$1"
+shift
+
+swaymsg -q mode default 2>/dev/null
+rofi "$@"
+
+if [ "$(eww get "${widget_name}"-visible)" = "true" ]; then
+ swaymsg -q mode "_${widget_name}" 2>/dev/null
+fi
diff --git a/config/eww/scripts/safe-sleep b/config/eww/scripts/safe-sleep
new file mode 100755
index 0000000..eb1d90a
--- /dev/null
+++ b/config/eww/scripts/safe-sleep
@@ -0,0 +1,72 @@
+#!/usr/bin/env bash
+# Continuously sleep for a small interval and stop if the current date passes
+# the given deadline.
+# Note: We could use the `at` utility instead of this script but it
+# does not have sub-minute resolution. It also requires a service to run.
+
+function help {
+ echo "Usage:"
+ echo " safe-sleep