{ pkgs, ... }@all: with all; let sep = " "; in { home = { file."${config.xdg.dataHome}/nx-gcal-event-credentials.json".text = '' { "installed": { "client_id": "${secrets.nx-gcal-event.client-client-id}", "project_id": "my-own-cal", "auth_uri": "https://accounts.google.com/o/oauth2/auth", "token_uri": "https://oauth2.googleapis.com/token", "auth_provider_x509_cert_url": "https://www.googleapis.com/oauth2/v1/certs", "client_secret": "${secrets.nx-gcal-event.client-secret}", "redirect_uris": [ "http://localhost" ] } } ''; packages = with pkgs; [ # TODO: make into real package, currently dependencies are in home.nix # (pkgs.python311.withPackages (python-pkgs: [ # python-pkgs.google # ])) (writeScriptBin "nx_gcal_event" /* python */ '' #!${pkgs.python3}/bin/python3 import datetime import os import pickle import sys from google.auth.transport.requests import Request from google.oauth2.credentials import Credentials from google_auth_oauthlib.flow import InstalledAppFlow from googleapiclient.discovery import build from googleapiclient.errors import HttpError from html import escape CREDENTIALS_PATH = f"{os.environ['XDG_DATA_HOME']}/nx-gcal-event-credentials.json" TOKEN_PATH = f"{os.environ['XDG_CACHE_HOME']}/nx-gcal-event-token.json" PICKLE_PATH = "/tmp/nx-gcal-event.pickle" def sec_to_nice_string(seconds: int): (hours, rsec) = divmod(seconds, 3600) minutes = rsec // 60 sep = " " if hours == 0: s_hours = f"" sep = "" elif hours == 1: s_hours = f"{hours} hour" else: s_hours = f"{hours} hours" if minutes == 0: s_minutes = f"" sep = "" elif minutes == 1: s_minutes = f"{minutes} minute" else: s_minutes = f"{minutes} minutes" if hours + minutes == 0: s_minutes = "~ No time" os.remove(PICKLE_PATH) return f"{s_hours}{sep}{s_minutes}" def get_event_from_api(): creds = None SCOPES = ["https://www.googleapis.com/auth/calendar.readonly"] if os.path.exists(TOKEN_PATH): creds = Credentials.from_authorized_user_file(TOKEN_PATH, SCOPES) if not creds or not creds.valid: if creds and creds.expired and creds.refresh_token: creds.refresh(Request()) else: flow = InstalledAppFlow.from_client_secrets_file(CREDENTIALS_PATH, SCOPES) creds = flow.run_local_server(port=0) # Save the credentials for the next run with open(TOKEN_PATH, "w") as token: token.write(creds.to_json()) try: service = build("calendar", "v3", credentials=creds) now = datetime.datetime.utcnow().isoformat() + "Z" # 'Z' indicates UTC time in_24_h = (datetime.datetime.utcnow() + datetime.timedelta(days=1)).isoformat() + "Z" calendar_list = service.calendarList().list().execute() calendars = calendar_list.get("items", []) # List events from all calendars all_events = [] for calendar in calendars: calendar_id = calendar["id"] events_result = service.events().list( calendarId=calendar_id, timeMin=now, timeMax=in_24_h, singleEvents=True, orderBy="startTime", ).execute() events = events_result.get("items", []) all_events.extend(events) # Filter out all-day events all_events = [event for event in all_events if "dateTime" in event["start"]] # Find the earliest event earliest_event = None for event in all_events: event_start = event["start"]["dateTime"] if not earliest_event or event_start < earliest_event["start"]["dateTime"]: earliest_event = event # Now earliest_event contains the event that starts earliest return earliest_event except HttpError as error: print("An error occurred: %s" % error) exit(1) def dump_dict_to_file(event, now): if not event: event = {} event['nxWriteTime'] = now with open(PICKLE_PATH, 'wb') as f: pickle.dump(event, f) def load_dict_from_file(now): with open(PICKLE_PATH, 'rb') as f: event = pickle.load(f) # recheck all 15 minutes if (now - event['nxWriteTime']).seconds > 900: event = get_event_from_api() os.remove(PICKLE_PATH) return event def lookup(): # set now (timezone CEST) now = datetime.datetime.now(datetime.timezone(datetime.timedelta(hours=2))) no_event_string = "~ Zen ~" if os.path.exists(PICKLE_PATH): event = load_dict_from_file(now) else: event = get_event_from_api() if not event: print(no_event_string) dump_dict_to_file(event, now) exit(0) dump_dict_to_file(event, now) try: # when the saved even was empty (there was no event) end = datetime.datetime.strptime(event["end"]["dateTime"], "%Y-%m-%dT%H:%M:%S%z") start = datetime.datetime.strptime(event["start"]["dateTime"], "%Y-%m-%dT%H:%M:%S%z") except: if (now - event['nxWriteTime']).seconds > 900: event = get_event_from_api() os.remove(PICKLE_PATH) else: print(no_event_string) exit(0) # set mode, remaining if start.day != now.day: # event start tomorrow print(no_event_string) exit(0) elif (start - now).days < 0: # today, started alredy remaining = end - now mode = " remaining in " else: # today, not started yet remaining = start - now mode = " until the start of " name = escape(event['summary']) print(f"󱙬${sep}{sec_to_nice_string(remaining.seconds)}{mode}\'{name}\'") exit(0) def print_help(): print("Usage: nx_gcal_event [lookup|force-lookup|reauthenicate|help]") def forece_lookup(): try: os.remove(PICKLE_PATH) os.system('notify-send --app-name="nx_gcal_event" "Saved event deleted!"') except: os.system('notify-send --app-name="nx_gcal_event" "No saved event found!"') finally: lookup() def reauthenicate(): try: os.remove(PICKLE_PATH) os.remove(TOKEN_PATH) os.system('notify-send --app-name="nx_gcal_event" "Deleted Token"') exept: lookup() def print_help(): print("Usage: nx_gcal_event [lookup|force-lookup|reauthenicate|help]") if __name__ == "__main__": if len(sys.argv) != 2: print("Incorrect number of arguments.") print_help() else: arg = sys.argv[1] if arg == "lookup": lookup() elif arg == "force-lookup": forece_lookup() elif arg == "reauthenticate": reauthenicate() elif arg == "help": print_help() else: print_help() '') ]; }; }