From 18c64907d44695e4a357f857780e92cf7406c74b Mon Sep 17 00:00:00 2001 From: Ryan Chen Date: Fri, 5 Sep 2025 23:37:46 -0400 Subject: [PATCH] catlendar --- .python-version | 1 + README.md | 7 +++++ caltui.py | 34 ++++++++++++++++++++ main.py | 82 +++++++++++++++++++++++++++++++++++++++++++++++++ pyproject.toml | 7 +++++ token.json | 1 + uv.lock | 8 +++++ 7 files changed, 140 insertions(+) create mode 100644 .python-version create mode 100644 README.md create mode 100644 caltui.py create mode 100644 main.py create mode 100644 pyproject.toml create mode 100644 token.json create mode 100644 uv.lock diff --git a/.python-version b/.python-version new file mode 100644 index 0000000..24ee5b1 --- /dev/null +++ b/.python-version @@ -0,0 +1 @@ +3.13 diff --git a/README.md b/README.md new file mode 100644 index 0000000..73ee672 --- /dev/null +++ b/README.md @@ -0,0 +1,7 @@ +# catlendar + +A funny little tui calendar for Google Calendar. + +Fetching logic ripped out of the [Google Calendar Python quickstart](https://developers.google.com/workspace/drive/api/quickstart/python). Requires the `credentials.json` file mentioned in the quickstart. + +TUI built using Textual. diff --git a/caltui.py b/caltui.py new file mode 100644 index 0000000..e833272 --- /dev/null +++ b/caltui.py @@ -0,0 +1,34 @@ +from textual.app import App, ComposeResult +from textual.binding import Binding +from textual.widgets import Footer, Header, Label, ListItem, ListView + +from main import get_next_ten + + +class Catlendar(App): + """A Textual app to manage stopwatches.""" + + BINDINGS = [ + ("d", "toggle_dark", "Toggle dark mode"), + Binding(key="q", action="quit", description="Quit the app"), + ] + + def compose(self) -> ComposeResult: + """Create child widgets for the app.""" + next_ten = get_next_ten() + yield Header() + yield ListView( + *[ListItem(Label(x)) for x in next_ten] + ) + yield Footer() + + def action_toggle_dark(self) -> None: + """An action to toggle dark mode.""" + self.theme = ( + "textual-dark" if self.theme == "textual-light" else "textual-light" + ) + + +if __name__ == "__main__": + app = Catlendar() + app.run() diff --git a/main.py b/main.py new file mode 100644 index 0000000..abd234d --- /dev/null +++ b/main.py @@ -0,0 +1,82 @@ +import datetime +import os.path + +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 + +# If modifying these scopes, delete the file token.json. +SCOPES = ["https://www.googleapis.com/auth/calendar.readonly"] + + +def get_next_ten(): + """Shows basic usage of the Google Calendar API. + Prints the start and name of the next 10 events on the user's calendar. + """ + creds = None + # The file token.json stores the user's access and refresh tokens, and is + # created automatically when the authorization flow completes for the first + # time. + if os.path.exists("token.json"): + creds = Credentials.from_authorized_user_file("token.json", SCOPES) + # If there are no (valid) credentials available, let the user log in. + 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.json", SCOPES + ) + creds = flow.run_local_server(port=0) + # Save the credentials for the next run + with open("token.json", "w") as token: + token.write(creds.to_json()) + + try: + service = build("calendar", "v3", credentials=creds) + + # Call the Calendar API + now = datetime.datetime.now(tz=datetime.timezone.utc).isoformat() + print("Getting the upcoming 10 events") + events_result = ( + service.events() + .list( + calendarId="primary", + timeMin=now, + maxResults=10, + singleEvents=True, + orderBy="startTime", + ) + .execute() + ) + events = events_result.get("items", []) + + if not events: + print("No upcoming events found.") + return + + # Prints the start and name of the next 10 eventsa + next_ten = [] + for event in events: + is_datetime = event["start"].get("dateTime") is not None + if is_datetime: + event_start = datetime.datetime.fromisoformat(event["start"].get("dateTime", event["start"].get("date"))) + event_end = datetime.datetime.fromisoformat(event["end"].get("dateTime", event["end"].get("date"))) + formatted_start_time = event_start.strftime("%Y-%m-%d %H:%M") + formatted_end_time = event_end.strftime("%H:%M") + formatted_event_time = f"{formatted_start_time} to {formatted_end_time}" + else: + formatted_event_time = event["start"].get("date") + next_ten.append(f"{formatted_event_time}: {event['summary']}") + print(formatted_event_time, event["summary"]) + + return next_ten + + except HttpError as error: + print(f"An error occurred: {error}") + + +if __name__ == "__main__": + get_next_ten() diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..3832267 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,7 @@ +[project] +name = "calendar-tui" +version = "0.1.0" +description = "Add your description here" +readme = "README.md" +requires-python = ">=3.13" +dependencies = [] diff --git a/token.json b/token.json new file mode 100644 index 0000000..5f3aee6 --- /dev/null +++ b/token.json @@ -0,0 +1 @@ +{"token": "ya29.A0AS3H6NxqYM1wQCVeSueNRARhrQg_YXSjJc-5VHH3DYohEyHsrFnfMPKS_DVpaA80SBrla9rGGesZ5wtKM2P73A6zpSkoiof7rZ4DKEeG9DakAnwj3eoUYMt7QWdGaDSK3D2QDbSXW78ToEwxMzCOCbdsJgv9FHAHe-rPtVTwY7bsRmAYGN3jSXH6EsLDl_QzFuUzeuoaCgYKAYwSARMSFQHGX2MiVhRWnGiAewPQEfOwhHhYDg0206", "refresh_token": "1//05yfcxi3BjL_RCgYIARAAGAUSNwF-L9Ir6099ieylxwsUT9j_n4BSAFtJYnmEBACko_OEupTZvPU0ux84ZW9WQyPMflIJR-bXna4", "token_uri": "https://oauth2.googleapis.com/token", "client_id": "139832928960-cbqfg2hpippe078jukp91o5qgmeubn26.apps.googleusercontent.com", "client_secret": "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", "scopes": ["https://www.googleapis.com/auth/calendar.readonly"], "universe_domain": "googleapis.com", "account": "", "expiry": "2025-09-06T04:30:45Z"} \ No newline at end of file diff --git a/uv.lock b/uv.lock new file mode 100644 index 0000000..5cecb95 --- /dev/null +++ b/uv.lock @@ -0,0 +1,8 @@ +version = 1 +revision = 2 +requires-python = ">=3.13" + +[[package]] +name = "calendar-tui" +version = "0.1.0" +source = { virtual = "." }