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