{ config, pkgs, rice, domain, user, ... }: let sep = " "; in { sops.secrets = { "nx2site/radicale/password" = { }; }; home.packages = with pkgs; [ (writeShellApplication { name = "waybar_mode"; text = /*bash*/ '' print_help() { echo "Usage: waybar_mode {set |unset}" } if [ $# -lt 1 ]; then print_help; exit 1; fi case "$1" in set) # Check if there is a second argument for the 'set' operation if [ $# -eq 2 ]; then echo "$2" > /tmp/waybar-mode pkill -RTMIN+8 waybar else echo "Error: 'set' operation requires exactly one string argument." print_help exit 1 fi ;; unset) echo "" > /tmp/waybar-mode pkill -RTMIN+8 waybar ;; *) echo "Error: Unknown command '$1'" print_help exit 1 ;; esac exit 0 '';}) (writeShellApplication { name = "cclock"; text = /*bash*/ '' ord=$(date +"%e" | awk '{printf("%d%s\n", $1, ($1==11||$1==12||$1==13)?"th":((($1%10)==1)?"st":((($1%10)==2)?"nd":((($1%10)==3)?"rd":"th"))))}') if [ $# -eq 0 ]; then echo "󰃮${sep}$(date +'%A the')" "$ord" "of" "$(date +'%B')" " ${sep}$(date +'%R')" elif [ "$1" = "--no-icons" ]; then echo "$(date +'%A the')" "$ord" "of" "$(date +'%B')" "$(date +'%R')" fi '';}) (writers.writePython3Bin "caldav_event" { libraries = with pkgs.python3Packages; [ caldav ics pytz ]; flakeIgnore = [ "E302" "E305""E501" "E261" ]; } /* python */ '' import os import json from caldav import DAVClient from datetime import datetime, timezone from ics import Calendar from pytz import UTC def get_password(password_file): with open(password_file, "r") as file: return file.read().strip() def datetime_converter(obj): if isinstance(obj, datetime): return obj.isoformat() return obj def datetime_parser(dct): for key, value in dct.items(): if isinstance(value, str): try: dct[key] = datetime.fromisoformat(value) except ValueError: pass return dct def load_cache(cache_file): if os.path.exists(cache_file): with open(cache_file, "r") as file: return json.load(file, object_hook=datetime_parser) return None def save_cache(cache_file, data): with open(cache_file, "w") as file: json.dump(data, file, default=datetime_converter) def get_ongoing_or_next_event(url, username, password): now = datetime.now(timezone.utc) try: client = DAVClient(url, username=username, password=password) principal = client.principal() calendars = principal.calendars() next_event_dict = { 'event_name': "fake", 'event_begin': datetime(9000, 1, 1, tzinfo=UTC), # in the year 9000 'event_end': datetime(9000, 1, 1, 8, tzinfo=UTC), } for calendar in calendars: for event in calendar.events(): calendar_parsed = Calendar(event.data) for ics_event in calendar_parsed.events: event_dict = {} event_dict['event_name'] = ics_event.name or "(No Title)" event_dict['event_begin'] = ics_event.begin.astimezone(timezone.utc) event_dict['event_end'] = ics_event.end.astimezone(timezone.utc) if event_dict['event_begin'] <= now and now <= event_dict['event_end']: return event_dict elif event_dict['event_begin'] >= now and next_event_dict['event_begin'] > event_dict['event_begin']: next_event_dict = event_dict return next_event_dict except Exception as e: print(f"Error accessing {url}: {e}") return None if __name__ == "__main__": password_file = "/home/nx2/.config/sops-nix/secrets/nx2site/radicale/password" # Path to password file cache_file = "/tmp/caldav_event_cache.json" # Path to cache file url = "https://dav.${domain}/" username = "${user}" password = get_password(password_file) event_dict = load_cache(cache_file) now = datetime.now(timezone.utc).timestamp() if event_dict is None or event_dict['event_begin'].timestamp() <= now and now < event_dict['event_end'].timestamp(): event_dict = get_ongoing_or_next_event(url, username, password) if event_dict is None: print("No upcoming events found.") exit(0) cache_data = { "event_name": event_dict['event_name'] if event_dict is not None else None, "event_begin": event_dict['event_begin'] if event_dict is not None else None, "event_end": event_dict['event_end'] if event_dict is not None else None } save_cache(cache_file, cache_data) if event_dict: event_start = event_dict['event_begin'].timestamp() event_end = event_dict['event_end'].timestamp() if event_start <= now <= event_end: time_remaining = event_end - now hours, rem = divmod(int(time_remaining), 3600) minutes, _ = divmod(rem, 60) print(f"{event_dict['event_name']} ends in {hours} hour{'s ' if hours != 1 else ' '}and {minutes} minute{'s ' if minutes != 1 else ' '}") else: time_until_start = event_start - now hours, rem = divmod(int(time_until_start), 3600) minutes, _ = divmod(rem, 60) print(f"{event_dict['event_name']} starts in {hours} hour{'s ' if hours != 1 else ' '}and {minutes} minute{'s ' if minutes != 1 else ' '}") else: print("No upcoming events found.") '') ]; programs.waybar = { enable = true; package = pkgs.waybar; settings = { bar = { # height = 20; layer = "top"; position = "bottom"; margin-top = 0; # margin-left = rice.gap-size; # margin-bottom = rice.gap-size; # margin-right = rice.gap-size; margin-left = 0; margin-bottom = 0; margin-right = 0; spacing = 10; fixed-center = true; modules-left = [ # "cpu" # "memory" "wireplumber" "backlight" "battery" "network" "hyprland/window" ]; modules-center = [ "hyprland/workspaces" ]; modules-right = [ "custom/mode" # "custom/caldav_event" "custom/cclock" "tray" ]; "hyprland/workspaces" = { on-click = "activate"; format = "{name}"; all-outputs = false; active-only = false; }; "hyprland/window" = { # format = "${sep}{}"; format = "{}"; separate-outputs = true; }; "custom/cclock" = { exec = "cclock"; restart-interval = 60; }; "custom/caldav_event" = { format = "󰃰${sep}{}"; exec = "caldav_event"; restart-interval = 60; max-width = 60; }; "custom/mode" = { exec = "cat /tmp/waybar-mode"; interval = "once"; signal = 8; }; cpu = { interval = 1; format = "󰍛${sep}{}%"; max-length = 10; }; memory = { interval = 5; format = "${sep}{avail:.0f}G free"; }; battery = { interval = 60; tooltip = false; format = "{icon}${sep}{capacity}%"; states = { warning = 15; critical = 5; }; format-icons = [ " " " " " " " " " " ]; format-charging = "{icon}${sep}+{capacity}%"; format-plugged = "{icon}${sep}P{capacity}%"; format-full = "{icon}${sep}F{capacity}%"; }; backlight = { device = "eDP-1"; format = "{icon}${sep}{percent}%"; format-icons = [ "" "" "" "" "" "" "" "" "" ]; }; network = { format-wifi = "${sep}{essid}"; format-ethernet = "󰈀${sep}Wired"; format-disconnected = "󰌙${sep}Disconnected"; }; wireplumber = { format = "󰕾${sep}{volume}%"; format-muted = "󰝟${sep}--%"; }; }; }; style = with rice.color; let f = rice.lib.hex-to-rgb-comma-string; in '' * { font-family: ${rice.font.code.name}; font-size: 1em; min-height: 0px; margin: 0px; padding: 0px; } window#waybar { background-color: rgba(${f background},${builtins.toString rice.transparency}); transition-duration: 5s; transition-property: background-color; /* border: ${builtins.toString rice.border-width}px solid rgb(${f border}); */ /* margin: ${builtins.toString rice.gap-size}px; */ /* border-radius: ${builtins.toString rice.rounding}px; */ } #clock, #custom-cclock, #custom-mode, #custom-caldav-event, #battery, #cpu, #tray, #disk, #backlight, #network, #wireplumber, #memory, #window, #workspaces { padding: 0px 3px; margin-top: 0.3em; border-radius: ${builtins.toString rice.rounding}px; color: rgb(${f accent.bright}); } #workspaces { font-family: ${rice.font.code.name}; } #workspaces button { color: rgb(${f accent.base}); padding-left: 15px; padding-right: 15px; border-radius: ${builtins.toString rice.rounding}px; } #workspaces button.active { color: rgb(${f background}); background-color: rgb(${f accent.base}); } #workspaces button:hover { color: rgb(${f tertiary.bright}); } #workspaces button.urgent { background-color: rgba(${f magenta.base},${builtins.toString rice.transparency}); } #custom-mode { color: rgb(${f red.base}); } #window, #custom-caldav_event { font-family: ${rice.font.base.name}, ${rice.font.code.name}; color: rgb(${f tertiary.bright}); } #wireplumber.muted { color: rgb(${f tertiary.bright}); } #wireplumber { padding-left: 10px; } #battery.warning:not(.charging) { color: rgb(${f green.base});; } #battery.charging { color: rgb(${f green.base}); } #battery.critical { background: rgb(${f negative.base}); color: rgb(${f foreground}); } ''; #battery.critical:not(.charging) { }; }