Skip to content

Commit 35ed361

Browse files
tylertylerdayclaude
andcommitted
feat(calendar): add Google Calendar widget
Surface the next upcoming event(s) from one or more Google Calendars in the bar, with a left-click that joins Meet/Zoom/Teams meetings or falls back to the calendar event page. Tooltip lists the next N events (default 3) merged across all configured calendars. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 350f5b0 commit 35ed361

5 files changed

Lines changed: 623 additions & 0 deletions

File tree

docs/_Sidebar.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
- [Battery](./(Widget)-Battery)
1616
- [Bluetooth](./(Widget)-Bluetooth)
1717
- [Brightness](./(Widget)-Brightness)
18+
- [Calendar](./(Widget)-Calendar)
1819
- [Cava](./(Widget)-Cava)
1920
- [Copilot](./(Widget)-Copilot)
2021
- [CPU](./(Widget)-CPU)

docs/widgets/(Widget)-Calendar.md

Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
1+
# Calendar Widget Options
2+
3+
Shows the next upcoming Google Calendar event in the bar. Left-click joins the meeting (Google Meet, Zoom, or Microsoft Teams) by opening the join URL in the default browser.
4+
5+
| Option | Type | Default | Description |
6+
|-------------------------|---------|----------------------------------------------------------------------|-------------|
7+
| `label` | string | `'{icon} {title} {countdown}'` | Format string for the bar label. Tokens: `{icon}`, `{title}`, `{start_time}`, `{countdown}`, `{status}`, `{meeting_kind}`. |
8+
| `label_alt` | string | `'{icon} {title} at {start_time}'` | Alternative label, swapped via `toggle_label`. |
9+
| `class_name` | string | `''` | Extra CSS class appended to the widget frame. |
10+
| `update_interval` | integer | `60` | Seconds between Google Calendar API polls. Range 15–3600. |
11+
| `tick_interval` | integer | `1000` | Milliseconds between countdown re-renders (no API call). Range 250–60000. |
12+
| `calendar_ids` | list of strings | `['primary']` | Calendar IDs to read. Events from all listed calendars are merged and sorted by start time. Use `primary`, another email, or any calendar ID from your Google Calendar settings. |
13+
| `credentials_path` | string | `'~/.config/yasb/calendar/credentials.json'` | Path to the OAuth client JSON downloaded from Google Cloud Console. |
14+
| `token_path` | string | `'~/.config/yasb/calendar/token.json'` | Where the refresh token is cached after first authorisation. |
15+
| `look_ahead_minutes` | integer | `0` | If > 0, only show events starting within this many minutes. `0` = always show the next event regardless of how far away. |
16+
| `grace_period_minutes` | integer | `5` | Keep showing an in-progress event until this many minutes after its start. Range 0–120. |
17+
| `skip_all_day` | boolean | `true` | Skip all-day events when picking the "next" event. |
18+
| `max_title_length` | integer | `30` | Truncate event titles longer than this. |
19+
| `tooltip_event_count` | integer | `3` | Number of upcoming events to show in the hover tooltip. The bar label always shows just the next one. Range 1–10. |
20+
| `hide_when_empty` | boolean | `true` | Hide the widget when there are no upcoming events; otherwise show `empty_label`. |
21+
| `empty_label` | string | `'No upcoming events'` | Shown when there is no upcoming event (only if `hide_when_empty` is false). |
22+
| `auth_label` | string | `'Calendar: setup needed'` | Shown when `credentials.json` is missing. Click to open the setup docs. |
23+
| `setup_url` | string | `'https://github.com/amnweb/yasb/blob/main/docs/widgets/calendar.md'` | URL opened by `open_setup`. |
24+
| `tooltip` | boolean | `true` | Show a tooltip on hover with full title, time range, and location. |
25+
| `icons` | dict | `{'meet': '', 'zoom': '', 'teams': '', 'other': '', 'none': '', 'calendar': ''}` | Per-platform icon glyphs. Set to whichever Nerd Font codepoints you prefer. |
26+
| `callbacks` | dict | `{'on_left': 'join_meeting', 'on_middle': 'open_event', 'on_right': 'toggle_label'}` | Mouse callbacks. See *Callbacks* below. |
27+
28+
## Example Configuration
29+
30+
```yaml
31+
calendar:
32+
type: "yasb.calendar.CalendarWidget"
33+
options:
34+
label: "<span class=\"icon\">{icon}</span> {title} {countdown}"
35+
label_alt: "<span class=\"icon\">{icon}</span> {title} at {start_time}"
36+
update_interval: 60
37+
tick_interval: 1000
38+
calendar_ids:
39+
- "primary"
40+
- "you@example.com"
41+
look_ahead_minutes: 120
42+
grace_period_minutes: 5
43+
skip_all_day: true
44+
max_title_length: 30
45+
tooltip_event_count: 3
46+
hide_when_empty: true
47+
icons:
48+
meet: "󰼺"
49+
zoom: "󰹅"
50+
teams: "󰁳"
51+
other: ""
52+
none: ""
53+
calendar: ""
54+
callbacks:
55+
on_left: "join_meeting"
56+
on_middle: "open_event"
57+
on_right: "toggle_label"
58+
```
59+
60+
## One-time Google Calendar Setup
61+
62+
The widget reads your calendar via the Google Calendar API. You only have to do this once.
63+
64+
1. Open the [Google Cloud Console](https://console.cloud.google.com/) and create (or pick) a project.
65+
2. Enable the **Google Calendar API** for that project (APIs & Services → Library).
66+
3. Configure the OAuth consent screen as **External**, add your own Google account as a test user, and set scope `https://www.googleapis.com/auth/calendar.readonly`.
67+
4. Create credentials → **OAuth client ID** → application type **Desktop app**. Download the JSON file.
68+
5. Save it as `%USERPROFILE%\.config\yasb\calendar\credentials.json` (or set `credentials_path` to wherever you put it).
69+
6. Start YASB. The first time the widget runs it opens a browser tab asking you to authorise read-only access to your calendar. After you accept, a refresh token is cached at `token_path` and no further prompts are needed.
70+
71+
The token only grants read access. To revoke it, delete `token.json` and remove the app from <https://myaccount.google.com/permissions>.
72+
73+
## Tokens
74+
75+
Tokens you can use in `label` / `label_alt`:
76+
77+
- `{icon}` — picked from `icons` based on the meeting platform (`meet`/`zoom`/`teams`/`other`/`none`).
78+
- `{title}` — event title, truncated to `max_title_length`.
79+
- `{start_time}` — local time of the event start in `HH:MM`.
80+
- `{countdown}` — `in 12m`, `in 1h 20m`, `now`, `started 3m ago`.
81+
- `{status}` — `upcoming`, `live`, `ended` (also applied as a CSS class).
82+
- `{meeting_kind}` — `meet`, `zoom`, `teams`, `other`, or `none`.
83+
84+
## Callbacks
85+
86+
| Name | Behaviour |
87+
|----------------|-----------|
88+
| `join_meeting` | Open the meeting join URL in the default browser. Falls back to the calendar event page if no URL is found. If credentials are missing, opens `setup_url` instead. |
89+
| `open_event` | Open the event's `htmlLink` (Google Calendar web view). |
90+
| `toggle_label` | Swap between `label` and `label_alt`. |
91+
| `refresh` | Force a re-poll of the API (skipped if a poll is already in flight). |
92+
| `open_setup` | Open `setup_url`. |
93+
94+
## How the meeting URL is detected
95+
96+
In priority order:
97+
98+
1. `event.hangoutLink` — Google Meet links auto-attached to the event.
99+
2. `event.conferenceData.entryPoints[]` — first entry with `entryPointType: video`. Classified by host (`zoom.us`, `teams.microsoft.com`, etc.).
100+
3. Regex over the event's `location` and `description` for `https://*.zoom.us/...`, `https://teams.microsoft.com/l/meetup-join/...`, or `https://meet.google.com/xxx-xxxx-xxx`.
101+
102+
If nothing matches, `join_meeting` falls back to opening the event in Google Calendar.
103+
104+
## Style example
105+
106+
The widget frame gets state classes you can target from `styles.css`:
107+
108+
```css
109+
.calendar-widget {
110+
padding: 0 8px;
111+
}
112+
.calendar-widget.live {
113+
color: #f5a;
114+
font-weight: 600;
115+
}
116+
.calendar-widget.upcoming.meet {
117+
color: #00897b;
118+
}
119+
.calendar-widget.zoom { color: #2d8cff; }
120+
.calendar-widget.teams { color: #6264a7; }
121+
.calendar-widget.setup,
122+
.calendar-widget.error {
123+
color: #ff8a65;
124+
}
125+
```
126+
127+
State classes added to the frame: one of `loading`, `ok`, `empty`, `setup`, `error`, plus when state is `ok`: the meeting kind (`meet`/`zoom`/`teams`/`other`/`none`) and the status (`upcoming`/`live`/`ended`).

pyproject.toml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,9 @@ dependencies = [
4646
"winrt.windows.devices.wifi==3.2.1",
4747
"winrt.windows.security.credentials==3.2.1",
4848
"qt-css-engine @ git+https://github.com/Video-Nomad/qt-css-engine@dev",
49+
"google-api-python-client>=2.130",
50+
"google-auth-oauthlib>=1.2",
51+
"google-auth-httplib2>=0.2",
4952
]
5053

5154
[project.urls]
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
from pydantic import Field
2+
3+
from core.validation.widgets.base_model import (
4+
CallbacksConfig,
5+
CustomBaseModel,
6+
KeybindingConfig,
7+
)
8+
9+
10+
class CalendarIconsConfig(CustomBaseModel):
11+
meet: str = ""
12+
zoom: str = ""
13+
teams: str = ""
14+
other: str = ""
15+
none: str = ""
16+
calendar: str = ""
17+
18+
19+
class CalendarCallbacksConfig(CallbacksConfig):
20+
on_left: str = "join_meeting"
21+
on_middle: str = "open_event"
22+
on_right: str = "toggle_label"
23+
24+
25+
class CalendarConfig(CustomBaseModel):
26+
label: str = "{icon} {title} {countdown}"
27+
label_alt: str = "{icon} {title} at {start_time}"
28+
class_name: str = ""
29+
update_interval: int = Field(default=60, ge=15, le=3600)
30+
tick_interval: int = Field(default=1000, ge=250, le=60000)
31+
calendar_ids: list[str] = Field(default_factory=lambda: ["primary"])
32+
credentials_path: str = "~/.config/yasb/calendar/credentials.json"
33+
token_path: str = "~/.config/yasb/calendar/token.json"
34+
look_ahead_minutes: int = Field(default=0, ge=0, le=10080)
35+
grace_period_minutes: int = Field(default=5, ge=0, le=120)
36+
skip_all_day: bool = True
37+
max_title_length: int = Field(default=30, ge=5, le=200)
38+
tooltip_event_count: int = Field(default=3, ge=1, le=10)
39+
hide_when_empty: bool = True
40+
empty_label: str = "No upcoming events"
41+
auth_label: str = "Calendar: setup needed"
42+
setup_url: str = "https://github.com/amnweb/yasb/blob/main/docs/widgets/calendar.md"
43+
tooltip: bool = True
44+
icons: CalendarIconsConfig = CalendarIconsConfig()
45+
keybindings: list[KeybindingConfig] = []
46+
callbacks: CalendarCallbacksConfig = CalendarCallbacksConfig()

0 commit comments

Comments
 (0)