BUSY.Tedt.org BUSY Bar Apps
← All Apps

GCal Glance

Ambient Google Calendar display for BUSY Bar — at-a-glance meeting countdowns, alerts, and live schedule awareness.

Updated Sep 6, 2026 · Added Aug 18, 2026 · ↓ 0 package download starts

By Kilo59 · ▲ 0 · Rate and Discuss

How It Works

GCal Glance: Google Calendar on Aero Horizon for BUSY Bar

A dedicated BUSY Bar application engineered as a high-density, near-field desktop flight deck for Google Calendar on a 72×16 RGB LED matrix. Designed to solve temporal blindness and deliver continuous situational awareness through dynamic radar horizons, procedural micro-glyphs, and multi-phase meeting lifecycle management.

GCal Glance Preview


🛫 Aero Horizon Design System

Aero Horizon divides the 72×16 canvas into 3 functional vertical tiers:

+------------------------------------------------------------------------+ y=0
| 10:48 AM   42m Focus Runway                      [NEXT 11:30A]         | Tier 1 (y=0..7): Telemetry Deck
+------------------------------------------------------------------------+ y=7/8
| [📹] 11:30 AM - Sprint Planning - Room 3B - Then: 1:00 PM 1-on-1       | Tier 2 (y=8..13): Marquee Stream (65px)
+------------------------------------------------------------------------+ y=13/14
| NOW ● ▓▓▓▓▓ (11:30-12:00) ░░░░░░░░░ ▓▓▓▓▓▓ (13:00-13:45)               | Tier 3 (y=14..15): Dual-Mode Horizon
+------------------------------------------------------------------------+ y=15
  x=0                                                               x=71

1. Tier 1: Near-Field Telemetry Deck (y=0..7)

  • Idle Mode: 12h Wall Clock (10:48 AM), Dynamic Focus Runway (42m Focus Runway), and Compact Temporal Bookmark Pill:
    • Today's upcoming event: [NEXT 11:30A]
    • Tomorrow's first event: [TMRW 5:51A]
    • Multi-day future event: [WED 8/19 10A]
    • Clear schedule: [FREE]
  • Milestone Alerts: High-contrast amber badge ([⚡ IN 5 MIN] / [⚡ STARTING]), scheduled start time, and live seconds countdown.
  • Active Mode: Wall clock / start time, state badge ([● LIVE CALL], [● LIVE], [⚡ WRAP UP], [+04m OVER]), and live remaining time.

2. Tier 2: Micro-Glyph & Wide Marquee Stream (y=8..13)

  • 100% Procedural 5×5 Micro-Glyphs: Zero binary assets. Composed dynamically using 3 rectangle primitives:
    • Video Call (video): 4x5 bezel + 1x3 optical lens + 2x3 cutout
    • Coffee / Break (coffee): 4x4 mug + 2x1 vapor plume + 1x2 handle
    • Studio Headphones (focus): 5x3 earcups + 3x2 headband arch + 1x4 inner gap
    • Cruiser Airplane (travel): 1x5 fuselage + 5x1 wingspan + 3x1 tail stabilizer
    • Activity Dumbbell (fitness): 5x3 bar + 3x5 weight plates + cutout
    • Celebration Star (celebrate): 1x5 vertical ray + 5x1 horizontal ray + 3x3 diamond burst
    • Overtime Hourglass (overtime): 5x5 hourglass vessel + waist pinch
    • Calendar Grid (calendar): 5x5 frame + top header cutout + day pin
  • Wide 65px Text Viewport: Marquee scrolling for primary event title, location/platform, and configurable downstream schedule lookahead (default: 3 events).

3. Tier 3: Dual-Mode Dynamic Horizon (y=14..15)

  • Idle Proximity Radar: 6-hour rolling window at $5\text{ min/px}$ (default, configurable via --radar-window-minutes). Visualizes current time anchor (NOW ● beacon at x=0..1) and proportional upcoming event blocks with accurate temporal spacing.
  • Precision Elapsed Horizon: 72px active meeting gauge with 25%, 50%, and 75% tick marks (#303030FF) and a bright 1px white playhead needle (#FFFFFFFF).
  • Tight-Turn Pip (▌): Illuminates amber at x=70 when the downstream meeting buffer is $<5\text{ minutes}$.

🔄 Multi-Phase Meeting Lifecycle

stateDiagram-v2
    [*] --> Idle: No Active Meeting
    Idle --> Alert: T-15m / T-5m / T-2m / T-0m Checkpoint
    Alert --> Idle: 15s Timeout Dismissal
    Idle --> Phase1: Event Starts (T-0s)
    Alert --> Phase1: Event Starts (T-0s)

    state "Active Meeting Lifecycle" as Active {
        Phase1: Phase 1 — Title Verification (0..15s)\n[● LIVE CALL] - Location - Room
        Phase2: Phase 2 — Focus Flight Deck\n[● LIVE] - 25/50/75% Ticks - Playhead
        Phase3: Phase 3 — Wrap-Up Cue (Last 3m)\n[⚡ WRAP UP] - Tight-Turn Pip
        Overtime: Phase 4 — Overtime Count-Up\n[+04m OVER] - Crimson Flash

        Phase1 --> Phase2: After 15s
        Phase2 --> Phase3: Rem <= 3m
        Phase3 --> Overtime: Rem < 0s
    }

    Active --> InterruptedAlert: Incoming Event Alert Overlay\n(Top Alert Banner + Preserved Horizon Bar)
    InterruptedAlert --> Active: 15s Alert Timeout

    Active --> Breather: Call Ends & Gap >= 10m\n[☕ 15m BREAK]
    Active --> Idle: Call Ends & Back to Runway
    Breather --> Idle: Next Call Approaches

🎮 Hardware Controls & Tactile Navigation

GCal Glance integrates directly with the BUSY Bar's physical rotary encoder dial and push buttons for responsive, tactile desktop interaction:

Input Control Hardware Action Operational Behavior
Rotary Dial (Encoder) Rotate Left / Right Interactive Agenda Peek: Scrubs through upcoming calendar events one-by-one (1/N, 2/N, TMRW 3/N, etc.) across the lookahead queue. Displays exact start time, duration, room/video link, and location in real-time. Automatically dismisses back to the live flight deck after 8 seconds of inactivity.
OK Button Press Instant Manual Sync: Triggers an immediate re-fetch and re-parse of your Google Calendar feed, bypassing the normal 120-second polling interval. A bright blue LED confirmation pulse acknowledges the sync request.
BACK Button Press Exit & Dismiss: Immediately exits Agenda Peek view back to the active flight deck, or dismisses an active approaching alert banner early.
START Button Press Dismiss Alert: Clears an active milestone alert banner early to return directly to the standard runway or active meeting view.

⚙️ Configuration & Options

All configuration options can be set via command-line arguments or .env environment variables (prefixed with GCAL_GLANCE_, with GCAL_ and CALSYNC_ supported as fallbacks):

Setting CLI Flag Env Variable Default Description
Radar Window --radar-window-minutes GCAL_GLANCE_RADAR_WINDOW_MINUTES 360 Duration of idle rolling radar window in minutes (60–1440). 360 = 6h window ($5\text{ min/px}$).
Include All-Day --include-all-day GCAL_GLANCE_INCLUDE_ALL_DAY false Include all-day calendar events (VALUE=DATE).
Lookahead Count --lookahead-count GCAL_GLANCE_LOOKAHEAD_COUNT 8 Number of upcoming events to cycle in the idle marquee stream & dial peek (2–20).
iCal Feed URL --ical-url GCAL_GLANCE_ICAL_URL None Google Calendar secret address in iCal format (.ics).
Device Host --host GCAL_GLANCE_HOST 10.0.4.20 BUSY Bar host IP or hostname (USB is always 10.0.4.20).
Demo Mode --demo GCAL_GLANCE_DEMO false Run simulated multi-event schedule without a live calendar feed.
Sound --sound
--no-sound
GCAL_GLANCE_SOUND true Enable audible alert chimes on approaching checkpoints and event start.
Volume --volume GCAL_GLANCE_VOLUME None Speaker volume level (0–100). Default None keeps device's current volume.
Poll Interval — GCAL_GLANCE_CALENDAR_POLL_INTERVAL_SECONDS 120 Frequency in seconds to re-fetch and parse the iCal feed.
Alert Sequence — GCAL_GLANCE_UPCOMING_ALERT_SEQUENCE_MINUTES 15,5,2,0 Alert checkpoint intervals before event start.
Alert Duration — GCAL_GLANCE_ALERT_BANNER_DURATION_SECONDS 15 Seconds the alert banner overlay stays on screen.

🚀 Getting Started

Prerequisites

  • Python 3.10+
  • uv (recommended) or standard Python virtual environment

1. Normal Usage (Direct to Hardware)

The app is completely self-contained and drives the BUSY Bar directly over HTTP. Over USB, the physical bar is always reachable at 10.0.4.20.

  1. Open Google Calendar Settings $\rightarrow$ Select your calendar $\rightarrow$ Scroll to Integrate calendar.
  2. Copy your Secret address in iCal format (https://calendar.google.com/calendar/ical/.../basic.ics).
  3. Run the app:
uv run python app.py --ical-url "https://calendar.google.com/calendar/ical/.../basic.ics"

(Tip: To run this continuously as a background service, check out the community busybar-manager.)

2. Local Development & Hot-Reloading

For quick design iteration without relying on live calendar events or physical hardware, you can use Demo Mode and the local emulator.

Demo Mode: Run with a rich simulated multi-event schedule to preview all Aero Horizon phases and transitions:

Hot-Reloading (Dev Loop): For instant feedback as you edit app.py, use watchfiles to automatically restart the script whenever you save a change. You can target either the physical device or the local emulator:

# Option A: Hot-reload directly to the physical BUSY Bar
uv run watchfiles "uv run python app.py --demo" app.py

# Option B: Hot-reload to the local busybar-emulator
uv run watchfiles "uv run python app.py --demo --host 127.0.0.1:8080" app.py

🏷️ Procedural 5×5 Micro-Glyphs & Emoji Engine

The Aero Horizon engine analyzes incoming calendar entries (emojis, keyword summaries, bracketed tags, and colors) and dynamically renders an illuminated 5×5 procedural micro-glyph (x=0..4, y=8..12) using 3 lightweight rectangle primitives with zero external assets.

Micro-Glyph Visual Matrix Reference

Micro-Glyph 5×5 LED Pixel Matrix Category & Keywords Supported UTF-8 Emojis Theme Color
Video Call
video
█ █ █ █ ·
█ · · █ █
█ · · █ █
█ · · █ █
█ █ █ █ ·
meet, zoom, teams, sync, 1:1, standup, call 📹, 🎥, 📞, 📱, 🎙️, 💬, 👥, 👔 Blue
#4285F4
Coffee / Break
coffee
· █ █ · ·
█ █ █ █ █
█ █ █ █ █
█ █ █ █ ·
█ █ █ █ ·
coffee, break, lunch, tea, hydrate, decompress ☕, 🍵, 🧋, 🍺, 🍕, 🍔, 😎, 🌴, 🧘 Green
#34A853
Deep Focus
focus
(Headphones)
· █ █ █ ·
· █ · █ ·
█ █ · █ █
█ █ · █ █
█ █ · █ █
focus, deep work, coding, study, dnd, reading 🎯, 🎧, 🧠, 💡, 💻, ⌨️, 📝, 📚, 🔬 Purple
#A142F4
Transit / Travel
travel
(Airplane)
· · █ · ·
· · █ · ·
█ █ █ █ █
· · █ · ·
· █ █ █ ·
travel, flight, transit, train, bus, hotel, trip ✈️, 🛫, 🛬, 🚌, 🚍, 🚗, 🚆, 🚇, 🧳 Amber
#FBBC05
Fitness / Health
fitness
(Dumbbell)
· █ █ █ ·
█ █ █ █ █
█ · · · █
█ · · · █
· █ █ █ ·
run, gym, workout, fitness, walk, swim, cycle 🏃, 🏋️, 🚴, 🏊, ⚽, 🏀, 🥊, ❤️, 🩺 Green
#34A853
Celebration
celebrate
(Sparkle Star)
· · █ · ·
· █ █ █ ·
█ █ █ █ █
· █ █ █ ·
· · █ · ·
party, birthday, celebration, anniversary, happy hour 🎉, 🎈, 🎂, 🥳, 🎁, 🍾, ✨, ⭐, 🌟 Crimson
#EA4335
Overtime
overtime
(Hourglass)
█ █ █ █ █
█ · · · █
█ · █ · █
█ · · · █
█ █ █ █ █
Meeting overrun ($>0\text{m}$) ⏳, ⌛ Coral Red
#EA4335
Calendar Grid
calendar
█ █ █ █ █
█ · · · █
█ █ █ █ █
█ █ █ █ █
█ █ █ █ █
General scheduled task / default fallback 📅, 🗓️, 📆, ⏰, ⏱️ Blue
#4285F4

🎨 Semantic Color Palette System

Aero Horizon employs a functional, aviation-inspired multi-tone semantic color palette designed for high contrast, zero color bleeding, and effortless peripheral recognition on the 72×16 RGB LED matrix:

Color Swatch Hex Code Semantic Role Meaning & UI Context
Ice Blue / Google Blue #8AB4F8
#4285F4
#AECBFA
Calm Idle & Meetings Telemetry wall clock during idle states, standard scheduled meetings, virtual video calls, upcoming event radar blocks, soft sky blue marquee text, and default calendar indicators.
Royal Purple & Lavender #A142F4
#7B1FA2
#D7AEFB
Deep Focus & Concentration Deep work sessions, solo concentration blocks, coding/study tags, studio headphones micro-glyph, soft lavender marquee text, and [FOCUS] status badge.
Emerald & Sage Green #34A853
#81C995
Rest, Meals & Fitness Post-call breather mode ([15m BREAK]), lunch/coffee intervals, fitness and wellness workouts (fitness), soft mint green marquee text, and restful downtime.
Warning Amber #FBBC05
#FDD663
Urgency, Transit & Marquee Approaching milestone alerts ([⚡ IN 5 MIN], [⚡ STARTING]), flight & transit events (travel), soft golden amber marquee stream, wrap-up warning cues ([⚡ WRAP UP]), and tight-turn buffer pip.
Signal Red & Coral #EA4335
#F28B82
Active Calls & Overtime Active live meetings ([● LIVE CALL], [● LIVE]), Do Not Disturb status ([DO NOT DISTURB]), celebration events, soft coral marquee stream, and pulsing overtime overrun alerts ([+04m OVER]).
Crisp Chalk White #FFFFFF Primary Foci Top telemetry badge text (badge_txt), playhead needle (playhead), countdown timer digits, and high-priority indicators.
Slate Gray #9AA0A6
#303030
Subtext & Telemetry Secondary metadata, physical room locations, and 25%, 50%, 75% milestone horizon track tick marks.
Deep Space Obsidian #141414 Base Track & Contrast Deep dark base horizon rail (radar_bg) providing true black contrast behind the rolling radar blocks and progress bars.

🧭 Hardware Indicators & Horizon Symbols Reference

Beyond the 5×5 micro-glyphs, Aero Horizon utilizes dedicated hardware symbols and telemetry indicators across all 3 tiers:

Indicator / Symbol Location Visual Element Purpose & Behavior
Current Time Beacon Tier 3 Left NOW ● (x=0..1) Anchors the start of the rolling proximity radar window (default: 6h at $5\text{ min/px}$) at the current timestamp.
Active Playhead Needle Tier 3 Rail │ (x=0..71) 1px crisp white playhead needle (#FFFFFF) tracking real-time linear progress through an active meeting.
Quarter Milestones Tier 3 Rail 25% · 50% · 75% Subtle slate tick marks (#303030) at x=18, x=36, and x=54 providing instant pacing awareness without reading numbers.
Tight-Turn Pip Tier 3 Far Right ▌ (x=70..71) Flashing warning pip that illuminates in Amber when downstream meeting buffer is $<5\text{ minutes}$, alerting you to prepare for an immediate next call.
Context Status Pill Tier 1 Right [NEXT 11:30A] / [TMRW 5:51A] Inverted badge displaying operational state or immediate next start time on the telemetry deck.
Milestone Alert Badge Tier 1 Left [⚡ IN 5 MIN] High-contrast amber pill displayed during the 15-second pre-meeting urgency alert window.
Live Call Badge Tier 1 Left [● LIVE CALL] High-contrast crimson pill verifying that a meeting is currently in session.
Wrap-Up Cue Badge Tier 1 Left [⚡ WRAP UP] High-contrast amber badge triggered in the final 3 minutes of a meeting to signal prompt wrap-up.
Overtime Pill Tier 1 Left [+04m OVER] Flashing crimson alert badge counting up exact minutes elapsed past scheduled meeting end time.
Breather Badge Tier 1 Left [☕ 15m BREAK] Soothing emerald pill celebrating a buffer window $\ge 10\text{ minutes}$ after a call ends.

🧪 Testing & Verification

Run the test suite and type checker:

# Unit test suite
uv run pytest apps/gcal-glance/tests -v

# Lint & style check
uv run ruff check apps/gcal-glance
uv run ruff format --check apps/gcal-glance

Declared Dependencies

# Note: All dependencies below (httpx, pydantic, pydantic-settings) are already
# sub-dependencies of busylib, which is pre-approved according to project conventions.
busylib>=1.3.0
httpx>=0.28.0
pydantic>=2.11
pydantic-settings>=2.13

Source Files

These are the mirrored source bytes. The complete package also includes every asset and supporting file.

.pre-commit-config.yaml
files: ^apps/gcal-glance/
repos:
  - repo: https://github.com/pre-commit/pre-commit-hooks
    rev: v5.0.0
    hooks:
      - id: trailing-whitespace
      - id: end-of-file-fixer
      - id: mixed-line-ending
        args: [--fix=lf]
      - id: check-yaml
      - id: check-toml
README.md
# GCal Glance: Google Calendar on Aero Horizon for BUSY Bar

A dedicated BUSY Bar application engineered as a high-density, near-field desktop flight deck for Google Calendar on a 72×16 RGB LED matrix. Designed to solve temporal blindness and deliver continuous situational awareness through dynamic radar horizons, procedural micro-glyphs, and multi-phase meeting lifecycle management.

![GCal Glance Preview](./preview.gif)

---

## 🛫 Aero Horizon Design System

Aero Horizon divides the 72×16 canvas into 3 functional vertical tiers:

```
+------------------------------------------------------------------------+ y=0
| 10:48 AM   42m Focus Runway                      [NEXT 11:30A]         | Tier 1 (y=0..7): Telemetry Deck
+------------------------------------------------------------------------+ y=7/8
| [📹] 11:30 AM - Sprint Planning - Room 3B - Then: 1:00 PM 1-on-1       | Tier 2 (y=8..13): Marquee Stream (65px)
+------------------------------------------------------------------------+ y=13/14
| NOW ● ▓▓▓▓▓ (11:30-12:00) ░░░░░░░░░ ▓▓▓▓▓▓ (13:00-13:45)               | Tier 3 (y=14..15): Dual-Mode Horizon
+------------------------------------------------------------------------+ y=15
  x=0                                                               x=71
```

### 1. Tier 1: Near-Field Telemetry Deck (`y=0..7`)
- **Idle Mode**: 12h Wall Clock (`10:48 AM`), Dynamic Focus Runway (`42m Focus Runway`), and Compact Temporal Bookmark Pill:
  - Today's upcoming event: `[NEXT 11:30A]`
  - Tomorrow's first event: `[TMRW 5:51A]`
  - Multi-day future event: `[WED 8/19 10A]`
  - Clear schedule: `[FREE]`
- **Milestone Alerts**: High-contrast amber badge (`[⚡ IN 5 MIN]` / `[⚡ STARTING]`), scheduled start time, and live seconds countdown.
- **Active Mode**: Wall clock / start time, state badge (`[● LIVE CALL]`, `[● LIVE]`, `[⚡ WRAP UP]`, `[+04m OVER]`), and live remaining time.

### 2. Tier 2: Micro-Glyph & Wide Marquee Stream (`y=8..13`)
- **100% Procedural 5×5 Micro-Glyphs**: Zero binary assets. Composed dynamically using 3 rectangle primitives:
  - Video Call (`video`): 4x5 bezel + 1x3 optical lens + 2x3 cutout
  - Coffee / Break (`coffee`): 4x4 mug + 2x1 vapor plume + 1x2 handle
  - Studio Headphones (`focus`): 5x3 earcups + 3x2 headband arch + 1x4 inner gap
  - Cruiser Airplane (`travel`): 1x5 fuselage + 5x1 wingspan + 3x1 tail stabilizer
  - Activity Dumbbell (`fitness`): 5x3 bar + 3x5 weight plates + cutout
  - Celebration Star (`celebrate`): 1x5 vertical ray + 5x1 horizontal ray + 3x3 diamond burst
  - Overtime Hourglass (`overtime`): 5x5 hourglass vessel + waist pinch
  - Calendar Grid (`calendar`): 5x5 frame + top header cutout + day pin
- **Wide 65px Text Viewport**: Marquee scrolling for primary event title, location/platform, and configurable downstream schedule lookahead (default: 3 events).

### 3. Tier 3: Dual-Mode Dynamic Horizon (`y=14..15`)
- **Idle Proximity Radar**: 6-hour rolling window at $5\text{ min/px}$ (default, configurable via `--radar-window-minutes`). Visualizes current time anchor (`NOW ●` beacon at `x=0..1`) and proportional upcoming event blocks with accurate temporal spacing.
- **Precision Elapsed Horizon**: 72px active meeting gauge with 25%, 50%, and 75% tick marks (`#303030FF`) and a bright 1px white playhead needle (`#FFFFFFFF`).
- **Tight-Turn Pip (`▌`)**: Illuminates amber at `x=70` when the downstream meeting buffer is $<5\text{ minutes}$.

---

## 🔄 Multi-Phase Meeting Lifecycle

```mermaid
stateDiagram-v2
    [*] --> Idle: No Active Meeting
    Idle --> Alert: T-15m / T-5m / T-2m / T-0m Checkpoint
    Alert --> Idle: 15s Timeout Dismissal
    Idle --> Phase1: Event Starts (T-0s)
    Alert --> Phase1: Event Starts (T-0s)

    state "Active Meeting Lifecycle" as Active {
        Phase1: Phase 1 — Title Verification (0..15s)\n[● LIVE CALL] - Location - Room
        Phase2: Phase 2 — Focus Flight Deck\n[● LIVE] - 25/50/75% Ticks - Playhead
        Phase3: Phase 3 — Wrap-Up Cue (Last 3m)\n[⚡ WRAP UP] - Tight-Turn Pip
        Overtime: Phase 4 — Overtime Count-Up\n[+04m OVER] - Crimson Flash

        Phase1 --> Phase2: After 15s
        Phase2 --> Phase3: Rem <= 3m
        Phase3 --> Overtime: Rem < 0s
    }

    Active --> InterruptedAlert: Incoming Event Alert Overlay\n(Top Alert Banner + Preserved Horizon Bar)
    InterruptedAlert --> Active: 15s Alert Timeout

    Active --> Breather: Call Ends & Gap >= 10m\n[☕ 15m BREAK]
    Active --> Idle: Call Ends & Back to Runway
    Breather --> Idle: Next Call Approaches
```

---

## 🎮 Hardware Controls & Tactile Navigation

GCal Glance integrates directly with the BUSY Bar's physical rotary encoder dial and push buttons for responsive, tactile desktop interaction:

| Input Control | Hardware Action | Operational Behavior |
| :--- | :---: | :--- |
| **Rotary Dial (Encoder)** | **Rotate Left / Right** | **Interactive Agenda Peek**: Scrubs through upcoming calendar events one-by-one (`1/N`, `2/N`, `TMRW 3/N`, etc.) across the lookahead queue. Displays exact start time, duration, room/video link, and location in real-time. Automatically dismisses back to the live flight deck after 8 seconds of inactivity. |
| **`OK` Button** | **Press** | **Instant Manual Sync**: Triggers an immediate re-fetch and re-parse of your Google Calendar feed, bypassing the normal 120-second polling interval. A bright blue LED confirmation pulse acknowledges the sync request. |
| **`BACK` Button** | **Press** | **Exit & Dismiss**: Immediately exits Agenda Peek view back to the active flight deck, or dismisses an active approaching alert banner early. |
| **`START` Button** | **Press** | **Dismiss Alert**: Clears an active milestone alert banner early to return directly to the standard runway or active meeting view. |

---

## ⚙️ Configuration & Options

All configuration options can be set via command-line arguments or `.env` environment variables (prefixed with `GCAL_GLANCE_`, with `GCAL_` and `CALSYNC_` supported as fallbacks):

| Setting | CLI Flag | Env Variable | Default | Description |
| :--- | :--- | :--- | :---: | :--- |
| **Radar Window** | `--radar-window-minutes` | `GCAL_GLANCE_RADAR_WINDOW_MINUTES` | `360` | Duration of idle rolling radar window in minutes (60–1440). `360` = 6h window ($5\text{ min/px}$). |
| **Include All-Day** | `--include-all-day` | `GCAL_GLANCE_INCLUDE_ALL_DAY` | `false` | Include all-day calendar events (`VALUE=DATE`). |
| **Lookahead Count** | `--lookahead-count` | `GCAL_GLANCE_LOOKAHEAD_COUNT` | `8` | Number of upcoming events to cycle in the idle marquee stream & dial peek (2–20). |
| **iCal Feed URL** | `--ical-url` | `GCAL_GLANCE_ICAL_URL` | `None` | Google Calendar secret address in iCal format (`.ics`). |
| **Device Host** | `--host` | `GCAL_GLANCE_HOST` | `10.0.4.20` | BUSY Bar host IP or hostname (USB is always `10.0.4.20`). |
| **Demo Mode** | `--demo` | `GCAL_GLANCE_DEMO` | `false` | Run simulated multi-event schedule without a live calendar feed. |
| **Sound** | `--sound`<br>`--no-sound` | `GCAL_GLANCE_SOUND` | `true` | Enable audible alert chimes on approaching checkpoints and event start. |
| **Volume** | `--volume` | `GCAL_GLANCE_VOLUME` | `None` | Speaker volume level (0–100). Default `None` keeps device's current volume. |
| **Poll Interval** | — | `GCAL_GLANCE_CALENDAR_POLL_INTERVAL_SECONDS` | `120` | Frequency in seconds to re-fetch and parse the iCal feed. |
| **Alert Sequence** | — | `GCAL_GLANCE_UPCOMING_ALERT_SEQUENCE_MINUTES` | `15,5,2,0` | Alert checkpoint intervals before event start. |
| **Alert Duration** | — | `GCAL_GLANCE_ALERT_BANNER_DURATION_SECONDS` | `15` | Seconds the alert banner overlay stays on screen. |

---

## 🚀 Getting Started

### Prerequisites
- Python 3.10+
- [uv](https://github.com/astral-sh/uv) (recommended) or standard Python virtual environment

### 1. Normal Usage (Direct to Hardware)

The app is completely self-contained and drives the BUSY Bar directly over HTTP. Over USB, the physical bar is always reachable at `10.0.4.20`.

1. Open Google Calendar Settings $\rightarrow$ Select your calendar $\rightarrow$ Scroll to **Integrate calendar**.
2. Copy your **Secret address in iCal format** (`https://calendar.google.com/calendar/ical/.../basic.ics`).
3. Run the app:
```bash
uv run python app.py --ical-url "https://calendar.google.com/calendar/ical/.../basic.ics"
```
*(Tip: To run this continuously as a background service, check out the community [busybar-manager](https://github.com/maxswinkels/busybar-manager).)*

### 2. Local Development & Hot-Reloading

For quick design iteration without relying on live calendar events or physical hardware, you can use Demo Mode and the local emulator.

**Demo Mode:**
Run with a rich simulated multi-event schedule to preview all Aero Horizon phases and transitions:

**Hot-Reloading (Dev Loop):**
For instant feedback as you edit `app.py`, use `watchfiles` to automatically restart the script whenever you save a change. You can target either the physical device or the local emulator:

```bash
# Option A: Hot-reload directly to the physical BUSY Bar
uv run watchfiles "uv run python app.py --demo" app.py

# Option B: Hot-reload to the local busybar-emulator
uv run watchfiles "uv run python app.py --demo --host 127.0.0.1:8080" app.py
```

---

## 🏷️ Procedural 5×5 Micro-Glyphs & Emoji Engine

The Aero Horizon engine analyzes incoming calendar entries (emojis, keyword summaries, bracketed tags, and colors) and dynamically renders an illuminated 5×5 procedural micro-glyph (`x=0..4, y=8..12`) using 3 lightweight rectangle primitives with zero external assets.

### Micro-Glyph Visual Matrix Reference

| Micro-Glyph | 5×5 LED Pixel Matrix | Category & Keywords | Supported UTF-8 Emojis | Theme Color |
| :--- | :---: | :--- | :--- | :--- |
| **Video Call**<br>`video` | `█ █ █ █ ·`<br>`█ · · █ █`<br>`█ · · █ █`<br>`█ · · █ █`<br>`█ █ █ █ ·` | `meet`, `zoom`, `teams`, `sync`, `1:1`, `standup`, `call` | `📹`, `🎥`, `📞`, `📱`, `🎙️`, `💬`, `👥`, `👔` | Blue<br>`#4285F4` |
| **Coffee / Break**<br>`coffee` | `· █ █ · ·`<br>`█ █ █ █ █`<br>`█ █ █ █ █`<br>`█ █ █ █ ·`<br>`█ █ █ █ ·` | `coffee`, `break`, `lunch`, `tea`, `hydrate`, `decompress` | `☕`, `🍵`, `🧋`, `🍺`, `🍕`, `🍔`, `😎`, `🌴`, `🧘` | Green<br>`#34A853` |
| **Deep Focus**<br>`focus`<br>*(Headphones)* | `· █ █ █ ·`<br>`· █ · █ ·`<br>`█ █ · █ █`<br>`█ █ · █ █`<br>`█ █ · █ █` | `focus`, `deep work`, `coding`, `study`, `dnd`, `reading` | `🎯`, `🎧`, `🧠`, `💡`, `💻`, `⌨️`, `📝`, `📚`, `🔬` | Purple<br>`#A142F4` |
| **Transit / Travel**<br>`travel`<br>*(Airplane)* | `· · █ · ·`<br>`· · █ · ·`<br>`█ █ █ █ █`<br>`· · █ · ·`<br>`· █ █ █ ·` | `travel`, `flight`, `transit`, `train`, `bus`, `hotel`, `trip` | `✈️`, `🛫`, `🛬`, `🚌`, `🚍`, `🚗`, `🚆`, `🚇`, `🧳` | Amber<br>`#FBBC05` |
| **Fitness / Health**<br>`fitness`<br>*(Dumbbell)* | `· █ █ █ ·`<br>`█ █ █ █ █`<br>`█ · · · █`<br>`█ · · · █`<br>`· █ █ █ ·` | `run`, `gym`, `workout`, `fitness`, `walk`, `swim`, `cycle` | `🏃`, `🏋️`, `🚴`, `🏊`, `⚽`, `🏀`, `🥊`, `❤️`, `🩺` | Green<br>`#34A853` |
| **Celebration**<br>`celebrate`<br>*(Sparkle Star)* | `· · █ · ·`<br>`· █ █ █ ·`<br>`█ █ █ █ █`<br>`· █ █ █ ·`<br>`· · █ · ·` | `party`, `birthday`, `celebration`, `anniversary`, `happy hour` | `🎉`, `🎈`, `🎂`, `🥳`, `🎁`, `🍾`, `✨`, `⭐`, `🌟` | Crimson<br>`#EA4335` |
| **Overtime**<br>`overtime`<br>*(Hourglass)* | `█ █ █ █ █`<br>`█ · · · █`<br>`█ · █ · █`<br>`█ · · · █`<br>`█ █ █ █ █` | Meeting overrun ($>0\text{m}$) | `⏳`, `⌛` | Coral Red<br>`#EA4335` |
| **Calendar Grid**<br>`calendar` | `█ █ █ █ █`<br>`█ · · · █`<br>`█ █ █ █ █`<br>`█ █ █ █ █`<br>`█ █ █ █ █` | General scheduled task / default fallback | `📅`, `🗓️`, `📆`, `⏰`, `⏱️` | Blue<br>`#4285F4` |

---

## 🎨 Semantic Color Palette System

Aero Horizon employs a functional, aviation-inspired multi-tone semantic color palette designed for high contrast, zero color bleeding, and effortless peripheral recognition on the 72×16 RGB LED matrix:

| Color Swatch | Hex Code | Semantic Role | Meaning & UI Context |
| :--- | :--- | :--- | :--- |
| **Ice Blue / Google Blue** | `#8AB4F8`<br>`#4285F4`<br>`#AECBFA` | **Calm Idle & Meetings** | Telemetry wall clock during idle states, standard scheduled meetings, virtual video calls, upcoming event radar blocks, soft sky blue marquee text, and default calendar indicators. |
| **Royal Purple & Lavender** | `#A142F4`<br>`#7B1FA2`<br>`#D7AEFB` | **Deep Focus & Concentration** | Deep work sessions, solo concentration blocks, coding/study tags, studio headphones micro-glyph, soft lavender marquee text, and `[FOCUS]` status badge. |
| **Emerald & Sage Green** | `#34A853`<br>`#81C995` | **Rest, Meals & Fitness** | Post-call breather mode (`[15m BREAK]`), lunch/coffee intervals, fitness and wellness workouts (`fitness`), soft mint green marquee text, and restful downtime. |
| **Warning Amber** | `#FBBC05`<br>`#FDD663` | **Urgency, Transit & Marquee** | Approaching milestone alerts (`[⚡ IN 5 MIN]`, `[⚡ STARTING]`), flight & transit events (`travel`), soft golden amber marquee stream, wrap-up warning cues (`[⚡ WRAP UP]`), and tight-turn buffer pip. |
| **Signal Red & Coral** | `#EA4335`<br>`#F28B82` | **Active Calls & Overtime** | Active live meetings (`[● LIVE CALL]`, `[● LIVE]`), Do Not Disturb status (`[DO NOT DISTURB]`), celebration events, soft coral marquee stream, and pulsing overtime overrun alerts (`[+04m OVER]`). |
| **Crisp Chalk White** | `#FFFFFF` | **Primary Foci** | Top telemetry badge text (`badge_txt`), playhead needle (`playhead`), countdown timer digits, and high-priority indicators. |
| **Slate Gray** | `#9AA0A6`<br>`#303030` | **Subtext & Telemetry** | Secondary metadata, physical room locations, and 25%, 50%, 75% milestone horizon track tick marks. |
| **Deep Space Obsidian** | `#141414` | **Base Track & Contrast** | Deep dark base horizon rail (`radar_bg`) providing true black contrast behind the rolling radar blocks and progress bars. |

---

## 🧭 Hardware Indicators & Horizon Symbols Reference

Beyond the 5×5 micro-glyphs, Aero Horizon utilizes dedicated hardware symbols and telemetry indicators across all 3 tiers:

| Indicator / Symbol | Location | Visual Element | Purpose & Behavior |
| :--- | :---: | :---: | :--- |
| **Current Time Beacon** | Tier 3 Left | `NOW ●` (`x=0..1`) | Anchors the start of the rolling proximity radar window (default: 6h at $5\text{ min/px}$) at the current timestamp. |
| **Active Playhead Needle** | Tier 3 Rail | `│` (`x=0..71`) | 1px crisp white playhead needle (`#FFFFFF`) tracking real-time linear progress through an active meeting. |
| **Quarter Milestones** | Tier 3 Rail | `25%` · `50%` · `75%` | Subtle slate tick marks (`#303030`) at `x=18`, `x=36`, and `x=54` providing instant pacing awareness without reading numbers. |
| **Tight-Turn Pip** | Tier 3 Far Right | `▌` (`x=70..71`) | Flashing warning pip that illuminates in Amber when downstream meeting buffer is $<5\text{ minutes}$, alerting you to prepare for an immediate next call. |
| **Context Status Pill** | Tier 1 Right | `[NEXT 11:30A]` / `[TMRW 5:51A]` | Inverted badge displaying operational state or immediate next start time on the telemetry deck. |
| **Milestone Alert Badge** | Tier 1 Left | `[⚡ IN 5 MIN]` | High-contrast amber pill displayed during the 15-second pre-meeting urgency alert window. |
| **Live Call Badge** | Tier 1 Left | `[● LIVE CALL]` | High-contrast crimson pill verifying that a meeting is currently in session. |
| **Wrap-Up Cue Badge** | Tier 1 Left | `[⚡ WRAP UP]` | High-contrast amber badge triggered in the final 3 minutes of a meeting to signal prompt wrap-up. |
| **Overtime Pill** | Tier 1 Left | `[+04m OVER]` | Flashing crimson alert badge counting up exact minutes elapsed past scheduled meeting end time. |
| **Breather Badge** | Tier 1 Left | `[☕ 15m BREAK]` | Soothing emerald pill celebrating a buffer window $\ge 10\text{ minutes}$ after a call ends. |

---

## 🧪 Testing & Verification

Run the test suite and type checker:
```bash
# Unit test suite
uv run pytest apps/gcal-glance/tests -v

# Lint & style check
uv run ruff check apps/gcal-glance
uv run ruff format --check apps/gcal-glance
```
app.py
#!/usr/bin/env python3
"""GCal Glance: Google Calendar Flight Deck & Dynamic Runway (Aero Horizon).

python app.py                        # BUSY Bar over USB (always 10.0.4.20)
python app.py --host 127.0.0.1:8080  # emulator or a Wi-Fi bar
python app.py --demo                 # test with rich simulated schedule
python app.py --ical-url <URL>       # Google Calendar secret iCal URL
"""

import argparse
import asyncio
import calendar
import json
import re
import sys
from collections.abc import Generator
from dataclasses import dataclass
from datetime import date, datetime, timedelta, timezone
from pathlib import Path
from typing import Any, ClassVar, Final, Literal, TypedDict

try:
    from zoneinfo import ZoneInfo
except ImportError:
    ZoneInfo = None  # type: ignore

import httpx
from pydantic import AliasChoices, AnyHttpUrl, Field, field_validator
from pydantic_settings import BaseSettings, SettingsConfigDict

APP = "gcal-glance"
GRID_WIDTH = 72
RADAR_WINDOW_MIN = 360  # 6-Hour rolling action window (1px = 5 min)

# --- Type Definitions -------------------------------------------------------

ElementType = Literal["rectangle", "text"]
GlyphType = Literal[
    "video",
    "coffee",
    "focus",
    "travel",
    "fitness",
    "celebrate",
    "overtime",
    "calendar",
]
FontType = Literal["tiny", "small"]
AlignType = Literal["top_left", "top_right"]
FillStyle = Literal["solid", "none"]


class LabelMapping(TypedDict):
    """Configuration mapping for an event category."""

    color: str
    icon: GlyphType
    label: str


class RectangleElement(TypedDict, total=False):
    """BUSY Bar rectangle display primitive."""

    id: str
    type: Literal["rectangle"]
    x: int
    y: int
    width: int
    height: int
    fill: FillStyle
    fill_colors: list[str]
    border_width: int
    border_color: str


class TextElement(TypedDict, total=False):
    """BUSY Bar text display primitive."""

    id: str
    type: Literal["text"]
    x: int
    y: int
    font: FontType
    color: str
    text: str
    align: AlignType
    width: int
    scroll_rate: int
    scroll_start_delay: int


DisplayElement = RectangleElement | TextElement


class DisplayExtra(TypedDict):
    """Auxiliary display control metadata (priority, LED notification color)."""

    priority: int
    led_notification_color: str | None


# --- Settings (pydantic-settings) -------------------------------------------
# All fields are readable from environment variables with the GCAL_GLANCE_ prefix
# (or GCAL_ / CALSYNC_ fallbacks).
# Example: GCAL_GLANCE_ALERT_BANNER_DURATION_SECONDS=30


class AppConfig(BaseSettings):
    model_config: ClassVar[SettingsConfigDict] = {
        "env_prefix": "GCAL_GLANCE_",
        "env_file": (Path(__file__).resolve().parent / ".env", ".env"),
        "extra": "ignore",
    }

    # BUSY Bar host (overridable by --host CLI arg too)
    host: str = Field(
        default="10.0.4.20",
        validation_alias=AliasChoices(
            "GCAL_GLANCE_HOST", "GCAL_HOST", "CALSYNC_HOST", "host"
        ),
    )

    # Google Calendar secret iCal feed URL
    ical_url: AnyHttpUrl | None = Field(
        default=None,
        validation_alias=AliasChoices(
            "GCAL_GLANCE_ICAL_URL", "GCAL_ICAL_URL", "CALSYNC_ICAL_URL", "ical_url"
        ),
    )

    # Run with a simulated demo schedule instead of a live calendar
    demo: bool = Field(
        default=False,
        validation_alias=AliasChoices(
            "GCAL_GLANCE_DEMO", "GCAL_DEMO", "CALSYNC_DEMO", "demo"
        ),
    )

    # Alert interval sequence before event start (minutes, descending order)
    upcoming_alert_sequence_minutes: list[int] = Field(
        default=[15, 5, 2, 0],
        validation_alias=AliasChoices(
            "GCAL_GLANCE_UPCOMING_ALERT_SEQUENCE_MINUTES",
            "GCAL_UPCOMING_ALERT_SEQUENCE_MINUTES",
            "CALSYNC_UPCOMING_ALERT_SEQUENCE_MINUTES",
            "upcoming_alert_sequence_minutes",
        ),
    )

    # How long (seconds) each upcoming-event alert banner stays on screen
    alert_banner_duration_seconds: int = Field(
        default=15,
        validation_alias=AliasChoices(
            "GCAL_GLANCE_ALERT_BANNER_DURATION_SECONDS",
            "GCAL_ALERT_BANNER_DURATION_SECONDS",
            "CALSYNC_ALERT_BANNER_DURATION_SECONDS",
            "alert_banner_duration_seconds",
        ),
    )

    # How often (seconds) to re-fetch the calendar feed
    calendar_poll_interval_seconds: int = Field(
        default=120,
        validation_alias=AliasChoices(
            "GCAL_GLANCE_CALENDAR_POLL_INTERVAL_SECONDS",
            "GCAL_CALENDAR_POLL_INTERVAL_SECONDS",
            "CALSYNC_CALENDAR_POLL_INTERVAL_SECONDS",
            "calendar_poll_interval_seconds",
        ),
    )

    # Rolling proximity radar window in minutes (default: 360 = 6h = 5 min/px)
    radar_window_minutes: int = Field(
        default=360,
        ge=60,
        le=1440,
        validation_alias=AliasChoices(
            "GCAL_GLANCE_RADAR_WINDOW_MINUTES",
            "GCAL_RADAR_WINDOW_MINUTES",
            "CALSYNC_RADAR_WINDOW_MINUTES",
            "radar_window_minutes",
        ),
    )

    # Include all-day calendar events (default: False)
    include_all_day: bool = Field(
        default=False,
        validation_alias=AliasChoices(
            "GCAL_GLANCE_INCLUDE_ALL_DAY",
            "GCAL_INCLUDE_ALL_DAY",
            "CALSYNC_INCLUDE_ALL_DAY",
            "include_all_day",
        ),
    )

    # Number of upcoming events to cycle in idle marquee & dial peek (2-20, default: 8)
    lookahead_count: int = Field(
        default=8,
        ge=2,
        le=20,
        validation_alias=AliasChoices(
            "GCAL_GLANCE_LOOKAHEAD_COUNT",
            "GCAL_LOOKAHEAD_COUNT",
            "CALSYNC_LOOKAHEAD_COUNT",
            "lookahead_count",
        ),
    )

    # Enable audible alert chimes (default: True)
    sound: bool = Field(
        default=True,
        validation_alias=AliasChoices(
            "GCAL_GLANCE_SOUND",
            "GCAL_SOUND",
            "CALSYNC_SOUND",
            "sound",
        ),
    )

    # Optional speaker volume level 0-100 (default: None to preserve hardware volume)
    volume: int | None = Field(
        default=None,
        ge=0,
        le=100,
        validation_alias=AliasChoices(
            "GCAL_GLANCE_VOLUME",
            "GCAL_VOLUME",
            "CALSYNC_VOLUME",
            "volume",
        ),
    )

    @field_validator("upcoming_alert_sequence_minutes", mode="before")
    @classmethod
    def _parse_sequence(cls, v: object) -> object:
        """Allow GCAL_GLANCE_UPCOMING_ALERT_SEQUENCE_MINUTES='15,5,2,0' from env."""
        if isinstance(v, str):
            return [int(x.strip()) for x in v.split(",") if x.strip()]
        return v


_CONFIG: Final[AppConfig] = AppConfig()


def get_config() -> AppConfig:
    return _CONFIG


def set_config(**cli_overrides: Any) -> None:
    global _CONFIG
    _CONFIG = AppConfig(**cli_overrides)  # type: ignore[misc] # only allowed here


# User-configurable event label/color mappings (structured — not env-driven)
LABEL_ICON_MAPPINGS: Final[dict[str, LabelMapping]] = {
    "meeting": {"color": "#4285F4FF", "icon": "video", "label": "MEETING"},
    "dnd": {"color": "#EA4335FF", "icon": "focus", "label": "DO NOT DISTURB"},
    "focus": {"color": "#A142F4FF", "icon": "focus", "label": "DEEP WORK"},
    "break": {"color": "#34A853FF", "icon": "coffee", "label": "ON BREAK"},
    "coffee": {"color": "#34A853FF", "icon": "coffee", "label": "ON BREAK"},
    "lunch": {"color": "#34A853FF", "icon": "coffee", "label": "LUNCH"},
    "travel": {"color": "#FBBC05FF", "icon": "travel", "label": "TRAVEL"},
    "fitness": {"color": "#34A853FF", "icon": "fitness", "label": "FITNESS"},
    "celebrate": {"color": "#EA4335FF", "icon": "celebrate", "label": "EVENT"},
    "overtime": {"color": "#EA4335FF", "icon": "overtime", "label": "OVERTIME"},
    "calendar": {"color": "#4285F4FF", "icon": "calendar", "label": "CALENDAR"},
}

# fmt: off
# Common UTF-8 Emoji to 5x5 Micro-Glyph Mapping Table
EMOJI_GLYPH_MAPPINGS: Final[dict[str, GlyphType]] = {
    # Transit & Travel
    "🚌": "travel", "🚍": "travel", "🚎": "travel", "🚐": "travel", "🚑": "travel",
    "🚒": "travel", "🚓": "travel", "🚕": "travel", "🚗": "travel", "🚙": "travel",
    "🚚": "travel", "🚛": "travel", "🚜": "travel", "🏎": "travel", "🏍": "travel",
    "🛵": "travel", "🚲": "travel", "🛴": "travel", "🚊": "travel", "🚝": "travel",
    "🚄": "travel", "🚅": "travel", "🚆": "travel", "🚇": "travel", "🚈": "travel",
    "🚉": "travel", "🚂": "travel", "✈": "travel", "🛫": "travel", "🛬": "travel",
    "🛩": "travel", "🚀": "travel", "🛸": "travel", "🚁": "travel", "🛶": "travel",
    "⛵": "travel", "🚤": "travel", "🛳": "travel", "🚢": "travel", "⚓": "travel",
    "🧳": "travel", "🗺": "travel",
    # Coffee, Food, Drinks, Break & Leisure
    "☕": "coffee", "🍵": "coffee", "🧋": "coffee", "🥤": "coffee", "🍺": "coffee",
    "🍻": "coffee", "🍷": "coffee", "🍸": "coffee", "🍹": "coffee", "🥂": "coffee",
    "🍽": "coffee", "🍴": "coffee", "🥄": "coffee", "🍕": "coffee", "🍔": "coffee",
    "🍟": "coffee", "🌭": "coffee", "🍿": "coffee", "🥪": "coffee", "🌮": "coffee",
    "🌯": "coffee", "🥗": "coffee", "🍜": "coffee", "🍲": "coffee", "🍛": "coffee",
    "🍣": "coffee", "🍱": "coffee", "🥟": "coffee", "🍦": "coffee", "🍧": "coffee",
    "🍨": "coffee", "🍩": "coffee", "🍪": "coffee", "🎂": "celebrate", "🍰": "coffee",
    "🍫": "coffee", "🍬": "coffee", "🍭": "coffee", "😎": "coffee", "🌴": "coffee",
    "🏖": "coffee", "☀️": "coffee", "🧘": "coffee", "💆": "coffee", "🛋": "coffee",
    "🎮": "coffee", "🎧": "coffee", "🎵": "coffee",
    # Focus, Deep Work, Coding, Reading, Thinking
    "🎯": "focus", "🧠": "focus", "💡": "focus", "💻": "focus", "🖥": "focus",
    "⌨": "focus", "🖱": "focus", "🖨": "focus", "📝": "focus", "✏": "focus",
    "🖊": "focus", "🖋": "focus", "📚": "focus", "📖": "focus", "📕": "focus",
    "📗": "focus", "📘": "focus", "📙": "focus", "📓": "focus", "📒": "focus",
    "🔬": "focus", "🔭": "focus", "🧪": "focus", "📐": "focus", "📏": "focus",
    "🛠": "focus", "🔧": "focus", "🔨": "focus", "⚙": "focus", "⚡": "focus",
    "🔥": "focus", "🔍": "focus", "🔎": "focus", "📊": "focus", "📈": "focus",
    # Video & Calls
    "📹": "video", "🎥": "video", "🎬": "video", "📺": "video", "📞": "video",
    "📱": "video", "🎙": "video", "🎤": "video", "💬": "video", "🗣": "video",
    "👥": "video", "👤": "video", "🤝": "video", "👔": "video",
    # Celebration & Social
    "🎉": "celebrate", "🎊": "celebrate", "🎈": "celebrate", "🥳": "celebrate",
    "🎁": "celebrate", "🍾": "celebrate", "✨": "celebrate", "⭐": "celebrate",
    "🌟": "celebrate", "🎆": "celebrate", "🎇": "celebrate",
    # Fitness & Health
    "🏃": "fitness", "🏋": "fitness", "🚴": "fitness", "🏊": "fitness", "⚽": "fitness",
    "🏀": "fitness", "🏈": "fitness", "⚾": "fitness", "🎾": "fitness", "🏐": "fitness",
    "🥊": "fitness", "🧗": "fitness", "🥋": "fitness", "❤️": "fitness", "🩺": "fitness",
    "💊": "fitness",
    # Calendar, Clock, Time
    "📅": "calendar", "🗓": "calendar", "📆": "calendar", "⏰": "calendar",
    "⏱": "calendar", "⏲": "calendar", "⏳": "overtime", "⌛": "overtime",
}
# fmt: on

# Display theme colours (#RRGGBBAA) - Multi-Tone Semantic Palette
COLOR_CLOCK = "#8AB4F8FF"  # Soft Ice Blue (calm wall time)
COLOR_CLOCK_ALERT = "#FDD663FF"  # Warm Amber (alert wall time)
COLOR_CLOCK_OVER = "#F28B82FF"  # Coral Red (overtime wall time)
COLOR_CLOCK_BREAK = "#81C995FF"  # Sage Green (break wall time)

COLOR_WHITE = "#FFFFFFFF"  # Crisp Chalk White (main titles & playhead needle)
COLOR_MUTED = "#9AA0A6FF"  # Slate Gray (subtext & metadata)
COLOR_SKY = "#AECBFAFF"  # Muted Sky Blue (runway metadata)

COLOR_BLUE = "#4285F4FF"  # Google Blue (meetings)
COLOR_BLUE_DARK = "#1A73E8FF"  # Inverted Blue Plate
COLOR_AMBER = "#FBBC05FF"  # Warning Amber (alerts & wrap-up)
COLOR_AMBER_DARK = "#7A4B00FF"  # Inverted Amber Plate
COLOR_RED = "#EA4335FF"  # Crimson Red (active calls & DND)
COLOR_RED_DARK = "#D93025FF"  # Inverted Red Plate
COLOR_RED_DIM = "#5C130CFF"  # Dim red pulse
COLOR_GREEN = "#34A853FF"  # Emerald Green (breaks & lunches)
COLOR_GREEN_DARK = "#1E8E3EFF"  # Inverted Green Plate
COLOR_PURPLE = "#A142F4FF"  # Royal Purple (deep focus)
COLOR_PURPLE_DARK = "#9334E6FF"  # Inverted Purple Plate

COLOR_RADAR_BG = "#141414FF"  # Deep Obsidian Base Rail (high contrast)
COLOR_TICK = "#303030FF"  # Dark Slate Milestone Pips

# Dynamic Category Marquee Soft-Tint Palette
GLYPH_TINT_COLORS: Final[dict[GlyphType, str]] = {
    "travel": "#FDD663FF",  # Soft Warm Amber
    "video": "#AECBFAFF",  # Soft Sky Blue
    "focus": "#D7AEFBFF",  # Soft Lavender
    "coffee": "#81C995FF",  # Soft Mint Green
    "fitness": "#81C995FF",  # Soft Mint Green
    "celebrate": "#F28B82FF",  # Soft Coral
    "overtime": "#F28B82FF",  # Soft Coral Red
    "calendar": "#AECBFAFF",  # Soft Sky Blue
}


def resolve_glyph_tint_color(glyph_type: GlyphType) -> str:
    """Resolve a soft illuminated text color for the marquee stream."""
    return GLYPH_TINT_COLORS.get(glyph_type, COLOR_SKY)


# --- Phase 2 Tactile Controls & LED Halo Settings ---------------------------

PEEK_INACTIVITY_TIMEOUT_SECONDS: Final[int] = 6
SYNC_ACK_FLASH_DURATION_SECONDS: Final[float] = 0.5

# Top-surface LED Notification Halo Colors (#RRGGBBAA)
LED_COLOR_ALERT: Final[str] = "#FBBC05FF"  # Amber pulse during alert banner
LED_COLOR_MEETING: Final[str] = "#4285F4FF"  # Solid Blue during active meeting
LED_COLOR_DND: Final[str] = "#EA4335FF"  # Solid Red during DND
LED_COLOR_SYNC_ACK: Final[str] = "#34A853FF"  # Green flash on manual sync


# --- BUSY Bar HTTP API (Async) ----------------------------------------------


def _parse_args() -> argparse.Namespace:
    p = argparse.ArgumentParser(
        description="GCal Glance: Google Calendar for BUSY Bar (Aero Horizon)"
    )
    cfg = get_config()
    p.add_argument("--host", default=cfg.host)
    p.add_argument(
        "--ical-url",
        default=cfg.ical_url,
        help="Google Calendar secret iCal feed URL (or set GCAL_GLANCE_ICAL_URL)",
    )
    p.add_argument(
        "--demo",
        action="store_true",
        default=cfg.demo,
        help=(
            "use fake events for testing without a calendar feed (or set"
            " GCAL_GLANCE_DEMO=true)"
        ),
    )
    p.add_argument(
        "--lookahead-count",
        type=int,
        default=cfg.lookahead_count,
        help=(
            "Number of upcoming events to cycle in idle marquee & dial peek"
            " (2-20, default: 8)"
        ),
    )
    p.add_argument(
        "--radar-window-minutes",
        type=int,
        default=cfg.radar_window_minutes,
        help=(
            "Duration of rolling proximity radar window in minutes (60-1440,"
            " default: 360 = 5 min/px)"
        ),
    )
    p.add_argument(
        "--include-all-day",
        action="store_true",
        default=cfg.include_all_day,
        help=(
            "Include all-day calendar events (or set GCAL_GLANCE_INCLUDE_ALL_DAY=true)"
        ),
    )
    p.add_argument(
        "--sound",
        action=argparse.BooleanOptionalAction,
        default=cfg.sound,
        help="Enable audible alert chimes (default: True, use --no-sound to mute)",
    )
    p.add_argument(
        "--volume",
        type=int,
        default=cfg.volume,
        help="Speaker volume 0-100 (default: keep device volume)",
    )
    args, _ = p.parse_known_args()
    set_config(
        host=args.host,
        ical_url=args.ical_url,
        demo=args.demo,
        lookahead_count=args.lookahead_count,
        radar_window_minutes=args.radar_window_minutes,
        include_all_day=args.include_all_day,
        sound=args.sound,
        volume=args.volume,
    )
    return args


ARGS: Final[argparse.Namespace] = _parse_args()
BASE: Final[str] = "http://" + get_config().host.replace("http://", "").rstrip("/")


async def draw(
    client: httpx.AsyncClient, elements: list[DisplayElement], **extra: Any
) -> None:
    extra = {k: v for k, v in extra.items() if v is not None}
    body: dict[str, Any] = {"application_name": APP, "elements": elements, **extra}
    resp = await client.post(f"{BASE}/api/display/draw", json=body, timeout=5.0)
    if resp.status_code == 400:
        print(f"[draw] Bad payload: {json.dumps(body, indent=2)}", file=sys.stderr)
    resp.raise_for_status()


STOCK_SOUND_EVENT_START: Final[str] = "calendar_event_starts"
STOCK_SOUND_REMINDER: Final[str] = "calendar_reminder_ends"
_pending_audio_chimes: list[str] = []
_background_tasks: set[asyncio.Task[Any]] = set()


async def play_sound(
    client: httpx.AsyncClient,
    stock_path: str,
    volume: int | None = None,
) -> None:
    """Play a stock audio chime on the BUSY Bar speaker."""
    try:
        if volume is not None:
            try:
                await client.post(
                    f"{BASE}/api/audio/volume",
                    json={"volume": volume},
                    timeout=2.0,
                )
            except Exception:
                pass
        await client.post(
            f"{BASE}/api/audio/play",
            json={"application_name": APP, "stock_path": stock_path},
            timeout=3.0,
        )
    except Exception as e:
        print(f"[audio] playback error: {e}", file=sys.stderr, flush=True)


# --- Data Models ------------------------------------------------------------


@dataclass
class CalendarEvent:
    """A single calendar event with start/end times and metadata."""

    uid: str  # Unique event identifier (from iCal UID)
    summary: str  # Event title / summary
    start: datetime  # Start time (timezone-aware)
    end: datetime  # End time (timezone-aware)
    color: str = ""  # Google Calendar color label (if available)
    description: str = ""  # Event description body
    location: str = ""  # Room, meeting URL, or location


@dataclass
class AlertState:
    """Tracks a currently-active alert banner on screen."""

    event: CalendarEvent  # The event this alert is for
    checkpoint_minutes: int  # Which alert milestone triggered (e.g. 15, 5, 2, 0)
    expires_at: datetime  # Wall-clock time when this banner should dismiss

    def is_expired(self, now: datetime) -> bool:
        return now >= self.expires_at


# Fired checkpoint tracker: set of (event_uid, checkpoint_minutes)
fired_checkpoints: set[tuple[str, int]] = set()

# Currently displayed alert banner (or None)
active_alert: AlertState | None = None


@dataclass
class AgendaPeekState:
    """Active Agenda Peek state for interactive rotary dial scrubbing."""

    queue: list[CalendarEvent]
    selected_index: int  # 0-indexed
    expires_at: datetime  # Inactivity expiration wall-clock time

    def is_expired(self, now: datetime) -> bool:
        return now >= self.expires_at


# Currently active agenda peek overlay (or None)
active_peek: AgendaPeekState | None = None

# Manual sync feedback flash tracker & async trigger event
manual_sync_flash_until: datetime | None = None
_manual_sync_event: asyncio.Event = asyncio.Event()

# --- Calendar Fetcher -------------------------------------------------------


def _unfold_ical(text_content: str) -> list[str]:
    """Unfold lines in iCal according to RFC 5545."""
    lines: list[str] = []
    for line in text_content.splitlines():
        if line.startswith((" ", "\t")) and lines:
            lines[-1] += line[1:]
        else:
            lines.append(line)
    return lines


def _unescape_ical(text_val: str) -> str:
    """Unescape special characters in iCal text fields."""
    return (
        text_val.replace(r"\n", "\n")
        .replace(r"\,", ",")
        .replace(r"\;", ";")
        .replace(r"\\", "\\")
    )


def _parse_ical_datetime(
    val: str, params: str = "", as_utc: bool = True
) -> datetime | None:
    """Parse an iCal DTSTART/DTEND string into a timezone-aware datetime."""
    val = val.strip()
    is_all_day = "VALUE=DATE" in params or len(val) == 8
    if is_all_day:
        if not get_config().include_all_day:
            return None
        try:
            # All-day event YYYYMMDD -> start of day in local timezone (or UTC)
            dt = datetime.strptime(val, "%Y%m%d")
            local_dt = dt.astimezone()
            return local_dt if not as_utc else local_dt.astimezone(timezone.utc)
        except Exception:
            return None
    try:
        if val.endswith("Z"):
            dt = datetime.strptime(val, "%Y%m%dT%H%M%SZ").replace(tzinfo=timezone.utc)
            return dt if not as_utc else dt.astimezone(timezone.utc)
        if "T" in val:
            dt = datetime.strptime(val, "%Y%m%dT%H%M%S")
            tzid_match = re.search(r"TZID=([^;:]+)", params, re.IGNORECASE)
            if tzid_match and ZoneInfo is not None:
                tz_name = tzid_match.group(1).strip()
                try:
                    tz_dt = dt.replace(tzinfo=ZoneInfo(tz_name))
                    return tz_dt if not as_utc else tz_dt.astimezone(timezone.utc)
                except Exception:
                    pass
            # Fallback to local system timezone -> UTC
            local_dt = dt.astimezone()
            return local_dt if not as_utc else local_dt.astimezone(timezone.utc)
    except Exception:
        pass
    return None


_DAY_NAMES: Final[dict[str, int]] = {
    "MO": 0,
    "TU": 1,
    "WE": 2,
    "TH": 3,
    "FR": 4,
    "SA": 5,
    "SU": 6,
}


def _get_nth_weekday_of_month(
    year: int, month: int, weekday: int, n: int
) -> date | None:
    """Find nth weekday of month (e.g. 3rd Friday: n=3; last Friday: n=-1)."""
    cal = calendar.Calendar(firstweekday=0)
    month_days = [
        d
        for d in cal.itermonthdates(year, month)
        if d.month == month and d.weekday() == weekday
    ]
    if not month_days:
        return None
    if n > 0 and n <= len(month_days):
        return month_days[n - 1]
    elif n < 0 and abs(n) <= len(month_days):
        return month_days[n]
    return None


def _parse_byday(byday_str: str) -> list[tuple[int | None, int]]:
    """Parse BYDAY string (e.g. '3FR', 'MO,TU,WE') into (pos, weekday_int) list."""
    results: list[tuple[int | None, int]] = []
    for item in byday_str.split(","):
        item = item.strip().upper()
        m = re.match(r"^([+-]?\d+)?(MO|TU|WE|TH|FR|SA|SU)$", item)
        if m:
            pos = int(m.group(1)) if m.group(1) else None
            wd = _DAY_NAMES[m.group(2)]
            results.append((pos, wd))
    return results


def _expand_rrule(
    dtstart: datetime,
    rrule_str: str,
    window_start: datetime,
    window_end: datetime,
    max_occurrences: int = 100,
) -> list[datetime]:
    """Pure stdlib RFC 5545 recurrence expansion preserving timezone across DST."""
    props: dict[str, str] = {}
    for part in rrule_str.split(";"):
        if "=" in part:
            k, v = part.split("=", 1)
            props[k.strip().upper()] = v.strip()

    freq = props.get("FREQ", "DAILY").upper()
    interval = max(1, int(props.get("INTERVAL", "1")))
    count = int(props["COUNT"]) if "COUNT" in props else None

    until_dt: datetime | None = None
    if "UNTIL" in props:
        u_val = props["UNTIL"]
        if len(u_val) == 8:
            u_y, u_m, u_d = int(u_val[:4]), int(u_val[4:6]), int(u_val[6:8])
            until_dt = datetime(u_y, u_m, u_d, 23, 59, 59, tzinfo=timezone.utc)
        elif "T" in u_val:
            u_clean = u_val.rstrip("Z")
            u_y, u_m, u_d = int(u_clean[:4]), int(u_clean[4:6]), int(u_clean[6:8])
            hh, mm = int(u_clean[9:11]), int(u_clean[11:13])
            ss = int(u_clean[13:15]) if len(u_clean) >= 15 else 0
            until_dt = datetime(u_y, u_m, u_d, hh, mm, ss, tzinfo=timezone.utc)

    byday = _parse_byday(props["BYDAY"]) if "BYDAY" in props else []
    bymonthday = (
        [int(x) for x in props["BYMONTHDAY"].split(",") if x.isdigit()]
        if "BYMONTHDAY" in props
        else []
    )

    tz = dtstart.tzinfo or timezone.utc
    start_date = dtstart.date()
    results: list[datetime] = []
    seen_count = 0

    def check_and_add(cand_date: date) -> bool:
        nonlocal seen_count
        if cand_date < start_date:
            return True
        try:
            cand_dt = datetime.combine(cand_date, dtstart.time(), tzinfo=tz)
        except Exception:
            return True
        cand_utc = cand_dt.astimezone(timezone.utc)
        if until_dt is not None and cand_utc > until_dt:
            return False
        seen_count += 1
        if count is not None and seen_count > count:
            return False
        if cand_utc >= window_start:
            if cand_utc <= window_end:
                results.append(cand_utc)
            elif cand_utc > window_end:
                return False
        return len(results) < max_occurrences

    if freq == "DAILY":
        curr = start_date
        win_start_local = window_start.astimezone(tz).date()
        if count is None and curr < win_start_local:
            days_behind = (win_start_local - curr).days
            steps = days_behind // interval
            curr += timedelta(days=steps * interval)
        while curr <= window_end.astimezone(tz).date() + timedelta(days=1):
            if not check_and_add(curr):
                break
            curr += timedelta(days=interval)

    elif freq == "WEEKLY":
        curr_week_start = start_date - timedelta(days=start_date.weekday())
        win_start_local = window_start.astimezone(tz).date()
        if count is None and curr_week_start < win_start_local - timedelta(days=7):
            weeks_behind = (win_start_local - curr_week_start).days // 7
            steps = weeks_behind // interval
            curr_week_start += timedelta(weeks=steps * interval)

        target_days = [wd for (_, wd) in byday] if byday else [dtstart.weekday()]
        target_days.sort()
        stop = False
        while not stop:
            for wd in target_days:
                cand_date = curr_week_start + timedelta(days=wd)
                if cand_date < start_date:
                    continue
                if not check_and_add(cand_date):
                    stop = True
                    break
            curr_week_start += timedelta(weeks=interval)
            if curr_week_start > window_end.astimezone(tz).date() + timedelta(days=7):
                break

    elif freq == "MONTHLY":
        curr_year = start_date.year
        curr_month = start_date.month
        win_start_local = window_start.astimezone(tz).date()
        if count is None:
            months_diff = (win_start_local.year - curr_year) * 12 + (
                win_start_local.month - curr_month
            )
            if months_diff > 1:
                steps = months_diff // interval
                total_months = (curr_year * 12 + curr_month - 1) + steps * interval
                curr_year = total_months // 12
                curr_month = (total_months % 12) + 1

        stop = False
        while not stop:
            if byday:
                for pos, wd in byday:
                    if pos is not None:
                        d_pos = _get_nth_weekday_of_month(
                            curr_year, curr_month, wd, pos
                        )
                        if d_pos and not check_and_add(d_pos):
                            stop = True
                            break
                    else:
                        cal = calendar.Calendar(firstweekday=0)
                        for d_day in cal.itermonthdates(curr_year, curr_month):
                            if d_day.month == curr_month and d_day.weekday() == wd:
                                if not check_and_add(d_day):
                                    stop = True
                                    break
            elif bymonthday:
                for bmd in bymonthday:
                    try:
                        d_bmd = date(curr_year, curr_month, bmd)
                        if not check_and_add(d_bmd):
                            stop = True
                            break
                    except ValueError:
                        pass
            else:
                try:
                    d_simple = date(curr_year, curr_month, start_date.day)
                    if not check_and_add(d_simple):
                        stop = True
                        break
                except ValueError:
                    pass
            total_m = (curr_year * 12 + curr_month - 1) + interval
            curr_year = total_m // 12
            curr_month = (total_m % 12) + 1
            if date(curr_year, curr_month, 1) > window_end.astimezone(
                tz
            ).date() + timedelta(days=32):
                break

    elif freq == "YEARLY":
        curr_year = start_date.year
        win_start_local = window_start.astimezone(tz).date()
        if count is None and curr_year < win_start_local.year - 1:
            years_diff = win_start_local.year - curr_year
            steps = years_diff // interval
            curr_year += steps * interval
        while True:
            try:
                d_yr = date(curr_year, start_date.month, start_date.day)
                if not check_and_add(d_yr):
                    break
            except ValueError:
                pass
            curr_year += interval
            if curr_year > window_end.astimezone(tz).date().year + 1:
                break

    results.sort()
    return results


async def fetch_events(
    client: httpx.AsyncClient, ical_url: AnyHttpUrl | str
) -> list[CalendarEvent] | None:
    """Fetch an iCal .ics URL and return future CalendarEvents sorted by start time."""
    try:
        resp = await client.get(
            str(ical_url),
            headers={"User-Agent": "BUSYBar-CalendarSync/1.0"},
            timeout=15.0,
        )
        resp.raise_for_status()
        content = resp.text
    except Exception as e:
        print(
            f"[fetch] warning: failed to fetch ical feed: {e}",
            file=sys.stderr,
            flush=True,
        )
        return None

    try:
        lines = _unfold_ical(content)
        raw_events: list[dict[str, Any]] = []
        in_vevent = False
        event_data: dict[str, Any] = {}
        now = datetime.now(timezone.utc)
        window_end = now + timedelta(days=60)

        for line in lines:
            line_str = line.strip()
            if line_str == "BEGIN:VEVENT":
                in_vevent = True
                event_data = {"EXDATES": []}
            elif line_str == "END:VEVENT":
                if in_vevent:
                    in_vevent = False
                    raw_events.append(dict(event_data))
            elif in_vevent:
                if ":" in line:
                    key_part, val = line.split(":", 1)
                    if ";" in key_part:
                        key, params = key_part.split(";", 1)
                    else:
                        key, params = key_part, ""
                    key_upper = key.upper()
                    if key_upper == "EXDATE":
                        event_data.setdefault("EXDATES", []).append((val, params))
                    elif key_upper == "RECURRENCE-ID":
                        event_data["RECURRENCE_ID_VAL"] = val
                        event_data["RECURRENCE_ID_PARAMS"] = params
                    elif key_upper in (
                        "UID",
                        "SUMMARY",
                        "DESCRIPTION",
                        "LOCATION",
                        "COLOR",
                        "X-APPLE-CALENDAR-COLOR",
                        "RRULE",
                        "STATUS",
                    ):
                        event_data[key_upper] = val
                    elif key_upper == "DTSTART":
                        event_data["DTSTART_VAL"] = val
                        event_data["DTSTART_PARAMS"] = params
                    elif key_upper == "DTEND":
                        event_data["DTEND_VAL"] = val
                        event_data["DTEND_PARAMS"] = params

        # 1. Collect overridden recurrence instances to suppress base RRULE occurrences
        overridden_recs: set[tuple[str, datetime]] = set()
        for ed in raw_events:
            uid = ed.get("UID", "")
            rec_val = ed.get("RECURRENCE_ID_VAL")
            rec_params = ed.get("RECURRENCE_ID_PARAMS", "")
            if rec_val and uid:
                rec_dt = _parse_ical_datetime(rec_val, rec_params, as_utc=True)
                if rec_dt is not None:
                    overridden_recs.add((uid, rec_dt))

        events: list[CalendarEvent] = []
        for ed in raw_events:
            uid = ed.get("UID", "")
            status = ed.get("STATUS", "").upper()
            if status == "CANCELLED":
                continue

            summary = _unescape_ical(ed.get("SUMMARY", "(No title)"))
            description = _unescape_ical(ed.get("DESCRIPTION", ""))
            location = _unescape_ical(ed.get("LOCATION", ""))
            color = ed.get("COLOR", "") or ed.get("X-APPLE-CALENDAR-COLOR", "")

            dtstart_val = ed.get("DTSTART_VAL", "")
            dtstart_params = ed.get("DTSTART_PARAMS", "")
            dtend_val = ed.get("DTEND_VAL", "")
            dtend_params = ed.get("DTEND_PARAMS", "")

            start_dt_raw = _parse_ical_datetime(
                dtstart_val, dtstart_params, as_utc=False
            )
            if start_dt_raw is None:
                continue
            end_dt_raw = (
                _parse_ical_datetime(dtend_val, dtend_params, as_utc=False)
                if dtend_val
                else None
            )
            is_all_day_evt = (
                "VALUE=DATE" in dtstart_params or len(dtstart_val.strip()) == 8
            )
            if end_dt_raw is None:
                end_dt_raw = start_dt_raw + (
                    timedelta(days=1) if is_all_day_evt else timedelta(hours=1)
                )
            duration = max(timedelta(seconds=0), end_dt_raw - start_dt_raw)

            start_dt = start_dt_raw.astimezone(timezone.utc)
            end_dt = end_dt_raw.astimezone(timezone.utc)

            rrule_str = ed.get("RRULE")
            if rrule_str:
                exdates: set[datetime] = set()
                for ex_val, ex_params in ed.get("EXDATES", []):
                    for part in ex_val.split(","):
                        ex_dt = _parse_ical_datetime(
                            part.strip(), ex_params, as_utc=True
                        )
                        if ex_dt is not None:
                            exdates.add(ex_dt)

                try:
                    for occ_start in _expand_rrule(
                        start_dt_raw,
                        rrule_str,
                        now - timedelta(hours=1),
                        window_end,
                    ):
                        if occ_start in exdates or (uid, occ_start) in overridden_recs:
                            continue
                        occ_end = occ_start + duration
                        if occ_end > now:
                            events.append(
                                CalendarEvent(
                                    uid=f"{uid}_{occ_start.isoformat()}",
                                    summary=summary,
                                    start=occ_start,
                                    end=occ_end,
                                    color=color,
                                    description=description,
                                    location=location,
                                )
                            )
                except Exception:
                    if end_dt > now:
                        events.append(
                            CalendarEvent(
                                uid=uid,
                                summary=summary,
                                start=start_dt,
                                end=end_dt,
                                color=color,
                                description=description,
                                location=location,
                            )
                        )
            else:
                if end_dt > now:
                    events.append(
                        CalendarEvent(
                            uid=uid,
                            summary=summary,
                            start=start_dt,
                            end=end_dt,
                            color=color,
                            description=description,
                            location=location,
                        )
                    )

        events.sort(key=lambda e: e.start)
        return events
    except Exception as e:
        print(
            f"[fetch] warning: error parsing ical feed: {e}",
            file=sys.stderr,
            flush=True,
        )
        return None


def demo_events() -> list[CalendarEvent]:
    """Return realistic fake events for testing Aero Horizon states."""
    now = datetime.now(timezone.utc)
    return [
        CalendarEvent(
            "demo-1",
            "Sprint Planning",
            now + timedelta(minutes=4),
            now + timedelta(minutes=34),
            location="Room 3B",
        ),
        CalendarEvent(
            "demo-2",
            "[focus] Deep Work / Architecture",
            now + timedelta(minutes=36),
            now + timedelta(minutes=90),
            location="Desk",
        ),
        CalendarEvent(
            "demo-3",
            "[coffee] 1-on-1 Sync",
            now + timedelta(minutes=105),
            now + timedelta(minutes=135),
            location="Cafeteria",
        ),
    ]


# --- Invariant Element & Helper Functions ------------------------------------


def make_rect(
    elem_id: str,
    x: int,
    y: int,
    width: int,
    height: int,
    fill: str,
    border_width: int = 0,
    border_color: str | None = None,
) -> RectangleElement:
    """Create a fully-specified rectangle with fill and explicit border width."""
    elem: RectangleElement = {
        "id": elem_id,
        "type": "rectangle",
        "x": x,
        "y": y,
        "width": max(1, width),
        "height": max(1, height),
        "fill": "none" if fill.endswith("00") else "solid",
        "fill_colors": [fill],
        "border_width": border_width,
    }
    if border_color:
        elem["border_color"] = border_color
    return elem


def park_rect(elem_id: str) -> RectangleElement:
    """Park an inactive rectangle safely off-screen."""
    return make_rect(elem_id, -100, -100, 1, 1, "#00000000")


def park_text(elem_id: str, text: str = "") -> TextElement:
    """Park an inactive text element safely off-screen."""
    return {
        "id": elem_id,
        "type": "text",
        "x": -100,
        "y": -100,
        "font": "tiny",
        "color": "#00000000",
        "text": text,
    }


def fmt_12h(dt: datetime, include_ampm: bool = True) -> str:
    """Format a timezone-aware datetime in 12-hour format without leading zero."""
    local_dt = dt.astimezone()
    hour = local_dt.hour % 12
    if hour == 0:
        hour = 12
    ampm = "AM" if local_dt.hour < 12 else "PM"
    if include_ampm:
        return f"{hour}:{local_dt.minute:02d} {ampm}"
    return f"{hour}:{local_dt.minute:02d}"


def fmt_compact_12h(dt: datetime) -> str:
    """Format datetime into compact 12h representation for status pills.

    Examples: '5:51A', '10A', '1:30P'.
    """
    local_dt = dt.astimezone()
    hour = local_dt.hour % 12
    if hour == 0:
        hour = 12
    ampm = "A" if local_dt.hour < 12 else "P"
    if local_dt.minute == 0:
        return f"{hour}{ampm}"
    return f"{hour}:{local_dt.minute:02d}{ampm}"


def fmt_countdown(total_seconds: float) -> str:
    """Format seconds into compact mm:ss or XhXXm."""
    sec = max(0, int(total_seconds))
    if sec < 3600:
        m, s = divmod(sec, 60)
        return f"{m:02d}:{s:02d}"
    h, m = divmod(sec // 60, 60)
    return f"{h}h{m:02d}m"


def fmt_remaining(delta: timedelta) -> str:
    """Format a timedelta as a compact remaining-time string."""
    total_minutes = max(0, int(delta.total_seconds()) // 60)
    if total_minutes >= 60:
        h, m = divmod(total_minutes, 60)
        return f"{h}h{m:02d}m"
    return f"{total_minutes}m"


def resolve_label(event: CalendarEvent) -> str:
    """Resolve an event to a display label using LABEL_ICON_MAPPINGS."""
    title = event.summary.lower()
    for key, mapping in LABEL_ICON_MAPPINGS.items():
        if f"[{key}]" in title:
            return mapping["label"]
    for mapping in LABEL_ICON_MAPPINGS.values():
        if event.color and event.color.upper().startswith(mapping["color"][:7].upper()):
            return mapping["label"]
    return "IN MEETING"


def clean_location(location: str, description: str = "") -> str:
    """Sanitize and extract a clean room name or meeting platform label."""
    raw = location.strip() if location else ""
    desc = description.strip() if description else ""

    # If location field is populated
    if raw:
        if "http://" in raw or "https://" in raw:
            parts = re.split(r"https?://\S+", raw)
            clean_part = parts[0].strip(" -·,;/")
            if clean_part:
                return clean_part[:30]
            # Location only contained a URL
            raw_lower = raw.lower()
            if "meet.google.com" in raw_lower:
                return "Google Meet"
            if "zoom.us" in raw_lower or "zoomgov.com" in raw_lower:
                return "Zoom"
            if "teams.microsoft.com" in raw_lower or "teams.live.com" in raw_lower:
                return "Teams"
            if "webex.com" in raw_lower:
                return "Webex"
            if "slack.com" in raw_lower:
                return "Slack Huddle"
            return "Video Link"
        return raw[:30]

    # If location is empty, check description for platform links
    if desc:
        desc_lower = desc.lower()
        if "meet.google.com" in desc_lower:
            return "Google Meet"
        if "zoom.us" in desc_lower or "zoomgov.com" in desc_lower:
            return "Zoom"
        if "teams.microsoft.com" in desc_lower or "teams.live.com" in desc_lower:
            return "Teams"
        if "webex.com" in desc_lower:
            return "Webex"
        if "slack.com" in desc_lower:
            return "Slack Huddle"

    return ""


def sanitize_display_text(text: str) -> str:
    """Sanitize text to printable ASCII to prevent missing glyph boxes on hardware."""
    if not text:
        return ""
    # Map common unicode punctuation/symbols to clean ASCII equivalents
    text = (
        text.replace("\u00b7", " - ")  # Middle dot
        .replace("\u2022", " - ")  # Bullet
        .replace("\u2014", " - ")  # Em dash
        .replace("\u2013", " - ")  # En dash
        .replace("\u201c", '"')  # Left double quote
        .replace("\u201d", '"')  # Right double quote
        .replace("\u2018", "'")  # Left single quote
        .replace("\u2019", "'")  # Right single quote
        .replace("\u2026", "...")  # Horizontal ellipsis
    )
    # Strip any remaining non-ASCII characters (e.g. emojis, symbols)
    cleaned = "".join(c for c in text if 32 <= ord(c) <= 126)
    # Normalize multiple whitespace
    cleaned = re.sub(r"\s+", " ", cleaned)
    # Normalize multiple dashes
    cleaned = re.sub(r"(\s+-\s+)+", " - ", cleaned)
    return cleaned.strip(" -")


def format_idle_badge(now: datetime, upcoming_events: list[CalendarEvent]) -> str:
    """Format a compact temporal bookmark pill for Tier 1 top-right."""
    if not upcoming_events:
        return "FREE"
    next_evt = upcoming_events[0]
    now_local = now.astimezone()
    next_start_local = next_evt.start.astimezone()
    compact_time = fmt_compact_12h(next_evt.start)

    delta_days = (next_start_local.date() - now_local.date()).days

    if delta_days <= 0:
        return f"NEXT {compact_time}"
    elif delta_days == 1:
        return f"TMRW {compact_time}"
    elif delta_days < 7:
        day_name = next_start_local.strftime("%a").upper()
        return f"{day_name} {compact_time}"
    else:
        day_name = next_start_local.strftime("%a").upper()
        return (
            f"{day_name} {next_start_local.month}/{next_start_local.day} {compact_time}"
        )


def format_idle_marquee(
    now: datetime,
    upcoming_events: list[CalendarEvent],
    lookahead_count: int,
) -> str:
    """Build a rich marquee stream with configurable multi-event lookahead."""
    if not upcoming_events:
        return "Schedule clear"

    now_local = now.astimezone()
    next_evt = upcoming_events[0]
    next_start_local = next_evt.start.astimezone()

    first_summary = sanitize_display_text(next_evt.summary) or "Event"
    first_loc = sanitize_display_text(
        clean_location(next_evt.location, next_evt.description)
    )
    first_loc_part = f" - {first_loc}" if first_loc else ""

    # If first event is today, include start time (e.g. '5:51 PM - Travel')
    # If on a future day, its start time and day are in the pill [TMRW 5:51A],
    # so we start directly with the event title and details!
    if next_start_local.date() == now_local.date():
        time_str = fmt_12h(next_evt.start, include_ampm=True)
        parts = [f"{time_str} - {first_summary}{first_loc_part}"]
    else:
        parts = [f"{first_summary}{first_loc_part}"]

    # Downstream lookahead stream (up to lookahead_count items)
    prev_date = next_start_local.date()
    for evt in upcoming_events[1 : max(1, lookahead_count)]:
        evt_local = evt.start.astimezone()
        evt_summary = sanitize_display_text(evt.summary) or "Event"
        evt_loc = sanitize_display_text(clean_location(evt.location, evt.description))
        evt_loc_part = f" ({evt_loc})" if evt_loc else ""

        if evt_local.date() == prev_date:
            time_str = fmt_12h(evt.start, include_ampm=True)
            parts.append(f"[THEN {time_str}] {evt_summary}{evt_loc_part}")
        else:
            delta_days = (evt_local.date() - now_local.date()).days
            time_str = fmt_12h(evt.start, include_ampm=True)
            day_name = evt_local.strftime("%a")
            if delta_days < 7:
                parts.append(
                    f"[THEN {day_name} {time_str}] {evt_summary}{evt_loc_part}"
                )
            else:
                date_str = f"{evt_local.month}/{evt_local.day}"
                tag = f"[THEN {day_name} {date_str} {time_str}]"
                parts.append(f"{tag} {evt_summary}{evt_loc_part}")
            prev_date = evt_local.date()

    return " - ".join(parts)


def build_peek_queue(
    now: datetime,
    events: list[CalendarEvent],
    lookahead_count: int,
) -> list[CalendarEvent]:
    """Compose the lookahead peek queue for rotary dial navigation."""
    if not events:
        return []
    now_local = now.astimezone()
    today_date = now_local.date()

    today_events = [e for e in events if e.start.astimezone().date() == today_date]
    today_events.sort(key=lambda e: e.start)

    future_events = [e for e in events if e.start.astimezone().date() > today_date]
    future_events.sort(key=lambda e: e.start)

    if today_events:
        return list(today_events) + future_events[: max(1, lookahead_count)]
    return future_events[: max(1, lookahead_count)]


def format_peek_bookmark(
    now: datetime,
    event: CalendarEvent,
    index: int,
    total: int,
) -> str:
    """Format the index bookmark pill for dial peek (e.g. 1/4, TMRW 2/4, WED 3/4)."""
    now_local = now.astimezone()
    evt_start_local = event.start.astimezone()
    delta_days = (evt_start_local.date() - now_local.date()).days

    if delta_days <= 0:
        return f"{index}/{total}"
    elif delta_days == 1:
        return f"TMRW {index}/{total}"
    elif delta_days < 7:
        day_name = evt_start_local.strftime("%a").upper()
        return f"{day_name} {index}/{total}"
    else:
        day_name = evt_start_local.strftime("%a").upper()
        return (
            f"{day_name} {evt_start_local.month}/{evt_start_local.day} {index}/{total}"
        )


def format_relative_delta(now: datetime, event: CalendarEvent) -> str:
    """Format relative delta badge for peek view (e.g. 'in 42m', 'LIVE')."""
    if event.start <= now < event.end:
        return "LIVE"
    if now >= event.end:
        ended_min = max(0, int((now - event.end).total_seconds() / 60.0))
        if ended_min < 60:
            return f"ended {ended_min}m"
        ended_hrs = ended_min // 60
        return f"ended {ended_hrs}h"

    delta_sec = (event.start - now).total_seconds()
    delta_min = max(0, int(delta_sec / 60.0))
    if delta_min < 60:
        return f"in {delta_min}m"
    delta_hrs = delta_min // 60
    if delta_hrs < 24:
        return f"in {delta_hrs}h"
    delta_days = (event.start.astimezone().date() - now.astimezone().date()).days
    return f"in {delta_days}d"


def resolve_glyph_type(event: CalendarEvent, is_overtime: bool = False) -> GlyphType:
    """Determine the 5x5 procedural micro-glyph type for an event."""
    if is_overtime:
        return "overtime"

    # 1. UTF-8 Emoji scanning across summary and description
    content = f"{event.summary} {event.description}"
    for emoji_char, glyph in EMOJI_GLYPH_MAPPINGS.items():
        if emoji_char in content:
            return glyph

    # 2. Tag brackets (e.g. [focus], [video], [travel])
    title = event.summary.lower()
    for key, mapping in LABEL_ICON_MAPPINGS.items():
        if f"[{key}]" in title:
            return mapping["icon"]

    # 3. Keyword semantic analysis
    if re.search(
        r"\b(travel|flight|transit|airport|train|bus|drive|commute|trip|hotel|reservation)\b",
        title,
    ):
        return "travel"
    if re.search(
        r"\b(run|running|gym|workout|fitness|exercise|walk|swim|cycle|biking)\b",
        title,
    ):
        return "fitness"
    if re.search(
        r"\b(party|birthday|celebration|anniversary|happy hour|ceremony)\b",
        title,
    ):
        return "celebrate"
    if re.search(r"\b(coffee|break|lunch|hydrate|tea|decompress|relax)\b", title):
        return "coffee"
    if re.search(
        r"\b(focus|deep\s+work|dnd|study|coding|code|deepwork|reading)\b",
        title,
    ):
        return "focus"
    if re.search(
        r"\b(zoom|meet|teams|sync|standup|call|meeting|1:1|1-on-1)\b",
        title,
    ):
        return "video"

    # 4. Location meeting URLs
    loc_desc = f"{event.location} {event.description}".lower()
    if any(
        k in loc_desc
        for k in ("meet.google.com", "zoom.us", "teams.microsoft.com", "webex.com")
    ):
        return "video"

    # 5. Calendar color hints
    if event.color:
        c = event.color.upper()
        if c.startswith("#34A853"):
            return "coffee"
        if c.startswith("#A142F4"):
            return "focus"
        if c.startswith("#FBBC05"):
            return "travel"

    return "calendar"


def make_glyph_elements(glyph_type: GlyphType, color: str) -> list[RectangleElement]:
    """Generate 3 procedural rectangle primitives for a 5x5 micro-glyph."""
    if glyph_type == "video":
        # 5x5 Camera Body with Right Lens
        return [
            make_rect("glyph_0", 0, 8, 4, 5, color),
            make_rect("glyph_1", 1, 9, 2, 3, "#000000FF"),
            make_rect("glyph_2", 4, 9, 1, 3, color),
        ]
    elif glyph_type == "coffee":
        # 5x5 Coffee Mug with Handle and Steam Plume
        return [
            make_rect("glyph_0", 0, 9, 4, 4, color),
            make_rect("glyph_1", 1, 8, 2, 1, color),
            make_rect("glyph_2", 4, 9, 1, 2, color),
        ]
    elif glyph_type == "focus":
        # 5x5 Studio Headphones (Overhead arch + Cushioned earcups)
        return [
            make_rect("glyph_0", 0, 10, 5, 3, color),
            make_rect("glyph_1", 1, 8, 3, 2, color),
            make_rect("glyph_2", 2, 9, 1, 4, "#000000FF"),
        ]
    elif glyph_type == "travel":
        # 5x5 Airplane (Fuselage + 5px Wingspan + 3px Tail Stabilizer)
        return [
            make_rect("glyph_0", 2, 8, 1, 5, color),
            make_rect("glyph_1", 0, 10, 5, 1, color),
            make_rect("glyph_2", 1, 12, 3, 1, color),
        ]
    elif glyph_type == "fitness":
        # 5x5 Dumbbell / Activity profile
        return [
            make_rect("glyph_0", 0, 9, 5, 3, color),
            make_rect("glyph_1", 1, 8, 3, 5, color),
            make_rect("glyph_2", 1, 10, 3, 2, "#000000FF"),
        ]
    elif glyph_type == "celebrate":
        # 5x5 Sparkle / Celebration star
        return [
            make_rect("glyph_0", 2, 8, 1, 5, color),
            make_rect("glyph_1", 0, 10, 5, 1, color),
            make_rect("glyph_2", 1, 9, 3, 3, color),
        ]
    elif glyph_type == "overtime":
        # 5x5 Hourglass Vessel
        return [
            make_rect("glyph_0", 0, 8, 5, 5, color),
            make_rect("glyph_1", 1, 9, 3, 3, "#000000FF"),
            make_rect("glyph_2", 2, 10, 1, 1, color),
        ]
    else:
        # Default 5x5 Calendar Frame with Header Cutout
        return [
            make_rect("glyph_0", 0, 8, 5, 5, color),
            make_rect("glyph_1", 1, 9, 3, 1, "#000000FF"),
            make_rect("glyph_2", 2, 11, 1, 1, COLOR_WHITE),
        ]


# --- Exact Font Measurement Engine (Firmware True) ---------------------------

# fmt: off
TINY_FONT_ADVANCES: Final[dict[str, int]] = {
    "0": 4, "1": 4, "2": 4, "3": 4, "4": 4, "5": 4, "6": 4, "7": 4, "8": 4, "9": 4,
    " ": 3, "!": 2, '"': 4, "#": 6, "$": 4, "%": 5, "&": 5, "'": 2, "(": 3, ")": 3,
    "*": 4, "+": 4, ",": 2, "-": 4, ".": 2, "/": 5, ":": 2, ";": 2, "<": 4, "=": 4,
    ">": 4, "?": 4, "@": 4, "[": 3, "\\": 5, "]": 3, "^": 4, "_": 4, "`": 3, "{": 4,
    "|": 2, "}": 4, "~": 5,
    "A": 4, "B": 4, "C": 4, "D": 4, "E": 4, "F": 4, "G": 4, "H": 4, "I": 4, "J": 4,
    "K": 5, "L": 4, "M": 6, "N": 5, "O": 5, "P": 4, "Q": 5, "R": 4, "S": 4, "T": 4,
    "U": 5, "V": 6, "W": 6, "X": 4, "Y": 6, "Z": 5,
    "a": 4, "b": 4, "c": 4, "d": 4, "e": 4, "f": 3, "g": 3, "h": 4, "i": 2, "j": 2,
    "k": 4, "l": 2, "m": 6, "n": 4, "o": 4, "p": 4, "q": 4, "r": 3, "s": 4, "t": 4,
    "u": 4, "v": 4, "w": 6, "x": 4, "y": 4, "z": 4,
}

TINY_FONT_WIDTHS: Final[dict[str, int]] = {
    "0": 3, "1": 3, "2": 3, "3": 3, "4": 3, "5": 3, "6": 3, "7": 3, "8": 3, "9": 3,
    " ": 0, "!": 1, '"': 3, "#": 5, "$": 3, "%": 4, "&": 4, "'": 1, "(": 2, ")": 2,
    "*": 3, "+": 3, ",": 1, "-": 3, ".": 1, "/": 4, ":": 1, ";": 1, "<": 3, "=": 3,
    ">": 3, "?": 3, "@": 3, "[": 2, "\\": 4, "]": 2, "^": 3, "_": 3, "`": 2, "{": 3,
    "|": 1, "}": 3, "~": 4,
    "A": 3, "B": 3, "C": 3, "D": 3, "E": 3, "F": 3, "G": 3, "H": 3, "I": 3, "J": 3,
    "K": 4, "L": 3, "M": 5, "N": 4, "O": 4, "P": 3, "Q": 4, "R": 3, "S": 3, "T": 3,
    "U": 4, "V": 5, "W": 5, "X": 3, "Y": 5, "Z": 4,
    "a": 3, "b": 3, "c": 3, "d": 3, "e": 3, "f": 2, "g": 2, "h": 3, "i": 1, "j": 1,
    "k": 3, "l": 1, "m": 5, "n": 3, "o": 3, "p": 3, "q": 3, "r": 2, "s": 3, "t": 3,
    "u": 3, "v": 3, "w": 5, "x": 3, "y": 3, "z": 3,
}
# fmt: on


def measure_tiny_text(text: str) -> int:
    """Measure exact visual pixel width (lit bounds) rendered by BUSY Bar tiny font."""
    pen = 0
    max_right = 0
    for ch in text:
        w = TINY_FONT_WIDTHS.get(ch, 3)
        adv = TINY_FONT_ADVANCES.get(ch, 4)
        max_right = max(max_right, pen + w)
        pen += adv
    return max(1, max_right)


def measure_badge_width(text: str, left_pad: int = 1, right_pad: int = 1) -> int:
    """Calculate exact width of the background plate so text NEVER overflows."""
    return measure_tiny_text(text) + left_pad + right_pad


def make_pill(
    pill_id: str,
    text: str,
    x: int = 0,
    y: int = 0,
    bg_color: str = COLOR_BLUE_DARK,
    text_color: str = COLOR_WHITE,
    font: FontType = "tiny",
    margin_x: int = 1,
    margin_y: int = 0,
    height: int = 7,
    align: AlignType = "top_left",
) -> tuple[RectangleElement, TextElement]:
    """Generate an encapsulated status pill component (bg rectangle + text).

    Automatically computes the exact rendered pixel width of `text` based on
    firmware font glyph metrics and wraps it with `margin_x` on both sides.
    Returns (bg_rectangle_element, text_element).
    """
    text_w = measure_tiny_text(text)
    plate_w = text_w + (margin_x * 2)

    if align == "top_right":
        rect_x = x - plate_w + 1
    else:
        rect_x = x

    text_x = rect_x + margin_x
    rect_elem = make_rect(f"{pill_id}_bg", rect_x, y, plate_w, height, bg_color)
    text_elem: TextElement = {
        "id": f"{pill_id}_txt",
        "type": "text",
        "x": text_x,
        "y": y + margin_y,
        "font": font,
        "color": text_color,
        "text": text,
        "align": "top_left",
    }
    return rect_elem, text_elem


# --- Aero Horizon Render Functions ------------------------------------------


def render_aero_idle(
    now: datetime, upcoming_events: list[CalendarEvent]
) -> tuple[list[DisplayElement], DisplayExtra]:
    """Render Aero Horizon State 1: Idle Focus Runway with Rolling 3h Radar."""
    next_evt = upcoming_events[0] if upcoming_events else None
    clock_str = fmt_12h(now, include_ampm=True)

    if next_evt:
        now_local = now.astimezone()
        next_start_local = next_evt.start.astimezone()
        if next_start_local.date() == now_local.date():
            free_min = max(0, int((next_evt.start - now).total_seconds() / 60.0))
            runway_str = f"{free_min}m Focus Runway"
        elif next_start_local.date() == now_local.date() + timedelta(days=1):
            runway_str = "Rest of Day Free"
        else:
            runway_str = "Schedule Clear"
        next_pill = format_idle_badge(now, upcoming_events)
        lookahead = get_config().lookahead_count
        title_str = format_idle_marquee(now, upcoming_events, lookahead_count=lookahead)
        glyph_type = resolve_glyph_type(next_evt)
        badge_bg = COLOR_BLUE_DARK
        title_color = resolve_glyph_tint_color(glyph_type)
    else:
        runway_str = "Schedule Clear"
        next_pill = "FREE"
        title_str = "Schedule clear"
        glyph_type = "calendar"
        badge_bg = "#253342FF"
        title_color = COLOR_SKY

    badge_bg_elem, badge_txt_elem = make_pill(
        "badge", next_pill, x=71, y=0, bg_color=badge_bg, align="top_right"
    )

    elements: list[DisplayElement] = [
        # Tier 1: Clean 2-Point Telemetry (Left Clock + Right Status Badge)
        {
            "id": "clock_txt",
            "type": "text",
            "x": 0,
            "y": 0,
            "font": "small",
            "color": COLOR_CLOCK,
            "text": clock_str,
        },
        park_text("mid_txt", text=runway_str),
        badge_bg_elem,
        badge_txt_elem,
        park_text("tele_txt"),
    ]

    # Tier 2: Micro-glyph + Marquee Stream
    elements.extend(make_glyph_elements(glyph_type, badge_bg))
    elements.append(
        {
            "id": "title_txt",
            "type": "text",
            "x": 6,
            "y": 8,
            "width": 66,
            "font": "tiny",
            "color": title_color,
            "text": title_str,
            "scroll_rate": 500,
            "scroll_start_delay": 1200,
        }
    )

    # Tier 3: Rolling 3-Hour Proximity Runway
    elements.extend(
        [
            make_rect("radar_bg", 0, 14, 72, 2, COLOR_RADAR_BG),
            park_rect("tick_25"),
            park_rect("tick_50"),
            park_rect("tick_75"),
            make_rect("bar_fill", 0, 14, 2, 2, COLOR_CLOCK),  # Origin Beacon (NOW)
            park_rect("playhead"),
            park_rect("tight_pip"),
        ]
    )

    # Render upcoming meeting blocks in the rolling runway window
    radar_win_min = get_config().radar_window_minutes
    for i in range(3):
        blk_id = f"radar_blk_{i}"
        if i < len(upcoming_events):
            evt = upcoming_events[i]
            offset_start_sec = (evt.start - now).total_seconds()
            offset_end_sec = (evt.end - now).total_seconds()
            window_sec = radar_win_min * 60.0

            if 0 <= offset_start_sec < window_sec:
                x_start = min(
                    71, max(0, int((offset_start_sec / window_sec) * GRID_WIDTH))
                )
                x_end = min(
                    72,
                    max(
                        x_start + 1,
                        int((offset_end_sec / window_sec) * GRID_WIDTH),
                    ),
                )
                width = max(1, x_end - x_start)
                evt_type = resolve_glyph_type(evt)
                mapping = LABEL_ICON_MAPPINGS.get(evt_type)
                evt_color = mapping["color"] if mapping else COLOR_BLUE
                elements.append(make_rect(blk_id, x_start, 14, width, 2, evt_color))
            else:
                elements.append(park_rect(blk_id))
        else:
            elements.append(park_rect(blk_id))

    extra: DisplayExtra = {"priority": 30, "led_notification_color": None}
    return elements, extra


def render_aero_alert(
    now: datetime, alert: AlertState, upcoming_events: list[CalendarEvent]
) -> tuple[list[DisplayElement], DisplayExtra]:
    """Render Aero Horizon State 2: Approaching Milestone Alert."""
    delta_sec = (alert.event.start - now).total_seconds()
    cd_str = f"{fmt_countdown(delta_sec)} left"

    if alert.checkpoint_minutes == 0:
        badge_str = "STARTING"
    else:
        badge_str = f"IN {alert.checkpoint_minutes} MIN"

    start_str = f"Starts at {fmt_12h(alert.event.start, False)}"
    loc = sanitize_display_text(
        clean_location(alert.event.location, alert.event.description)
    )
    loc_part = f" - {loc}" if loc else ""
    start_fmt = fmt_12h(alert.event.start, True)
    summary = sanitize_display_text(alert.event.summary) or "Event"
    title_str = f"{summary}{loc_part} - [STARTS {start_fmt}]"
    glyph_type = resolve_glyph_type(alert.event)
    title_color = resolve_glyph_tint_color(glyph_type)

    checkpoint_total_sec = max(60.0, float(alert.checkpoint_minutes * 60))
    approach_pct = max(
        0.0, min(1.0, 1.0 - (max(0.0, delta_sec) / checkpoint_total_sec))
    )
    fill_w = max(2, int(approach_pct * GRID_WIDTH))

    badge_bg_elem, badge_txt_elem = make_pill(
        "badge", badge_str, x=0, y=0, bg_color=COLOR_AMBER, text_color="#000000FF"
    )

    elements: list[DisplayElement] = [
        # Tier 1: Alert Banner (Left Amber Plate + Right Urgency Countdown)
        park_text("clock_txt"),
        badge_bg_elem,
        badge_txt_elem,
        park_text("mid_txt", text=start_str),
        {
            "id": "tele_txt",
            "type": "text",
            "x": 71,
            "y": 0,
            "font": "small",
            "align": "top_right",
            "color": COLOR_CLOCK_ALERT,
            "text": cd_str,
        },
    ]

    # Tier 2: Micro-glyph + Marquee
    elements.extend(make_glyph_elements(glyph_type, COLOR_AMBER))
    elements.append(
        {
            "id": "title_txt",
            "type": "text",
            "x": 6,
            "y": 8,
            "width": 66,
            "font": "tiny",
            "color": title_color,
            "text": title_str,
            "scroll_rate": 500,
            "scroll_start_delay": 1200,
        }
    )

    # Tier 3: Approach Rail
    elements.extend(
        [
            make_rect("radar_bg", 0, 14, 72, 2, COLOR_RADAR_BG),
            park_rect("tick_25"),
            park_rect("tick_50"),
            park_rect("tick_75"),
            make_rect("bar_fill", 0, 14, fill_w, 2, COLOR_AMBER),
            make_rect("playhead", min(71, fill_w), 14, 1, 2, COLOR_WHITE),
            park_rect("tight_pip"),
            park_rect("radar_blk_0"),
            park_rect("radar_blk_1"),
            park_rect("radar_blk_2"),
        ]
    )

    extra: DisplayExtra = {"priority": 45, "led_notification_color": COLOR_AMBER}
    return elements, extra


def render_aero_active(
    now: datetime,
    event: CalendarEvent,
    upcoming_events: list[CalendarEvent],
    active_alert: AlertState | None = None,
) -> tuple[list[DisplayElement], DisplayExtra]:
    """Render Aero Horizon Active Meeting lifecycle phases (States 3, 4, 5, 6, 7)."""
    elapsed_sec = (now - event.start).total_seconds()
    total_sec = max(1.0, (event.end - event.start).total_seconds())
    rem_sec = total_sec - elapsed_sec
    is_overtime = rem_sec < 0
    is_wrapup = 0 <= rem_sec <= 180

    # Downstream buffer calculations
    next_evt = next((e for e in upcoming_events if e.start >= event.end), None)
    buffer_min = (
        ((next_evt.start - event.end).total_seconds() / 60.0) if next_evt else 999.0
    )
    is_tight_turn = buffer_min < 5.0

    # Progress bar calculation
    progress_ratio = max(0.0, min(1.0, elapsed_sec / total_sec))
    fill_w = (
        GRID_WIDTH if is_overtime else min(GRID_WIDTH, int(progress_ratio * GRID_WIDTH))
    )
    playhead_x = min(71, fill_w)

    evt_type = resolve_glyph_type(event)
    mapping = LABEL_ICON_MAPPINGS.get(evt_type)
    evt_color = mapping["color"] if mapping else COLOR_RED
    title_color = resolve_glyph_tint_color(evt_type)

    summary = sanitize_display_text(event.summary) or "Event"
    loc = sanitize_display_text(clean_location(event.location, event.description))
    loc_part = f" - {loc}" if loc else ""

    # -------------------------------------------------------------------------
    # STATE 7: Interrupted Alert Overlay (Horizon & Progress Preserved)
    # -------------------------------------------------------------------------
    if active_alert and not active_alert.is_expired(now):
        badge_bg_col = COLOR_AMBER
        badge_txt_col = "#000000FF"
        badge_txt_val = (
            "STARTING"
            if active_alert.checkpoint_minutes == 0
            else f"IN {active_alert.checkpoint_minutes} MIN"
        )
        tele_txt_val = f"{max(1, round(rem_sec / 60.0))}m on call"
        tele_col = COLOR_RED
        alert_loc = sanitize_display_text(
            clean_location(active_alert.event.location, active_alert.event.description)
        )
        alert_loc_part = f" - {alert_loc}" if alert_loc else ""
        alert_summary = sanitize_display_text(active_alert.event.summary) or "Event"
        title_str = (
            f"[NEXT] {alert_summary} starts at"
            f" {fmt_12h(active_alert.event.start, True)}{alert_loc_part}"
        )
        glyph_type = resolve_glyph_type(active_alert.event)
        title_color = resolve_glyph_tint_color(glyph_type)
        bar_col = evt_color
        led_color = COLOR_AMBER
        priority = 60

        badge_bg_elem, badge_txt_elem = make_pill(
            "badge",
            badge_txt_val,
            x=0,
            y=0,
            bg_color=badge_bg_col,
            text_color=badge_txt_col,
        )

        elements: list[DisplayElement] = [
            park_text("clock_txt"),
            badge_bg_elem,
            badge_txt_elem,
            park_text("mid_txt", text=f"Next: {alert_summary[:10]}"),
            {
                "id": "tele_txt",
                "type": "text",
                "x": 71,
                "y": 0,
                "font": "small",
                "align": "top_right",
                "color": tele_col,
                "text": tele_txt_val,
            },
        ]

    # -------------------------------------------------------------------------
    # STATE 6: Dedicated Overtime Phase
    # -------------------------------------------------------------------------
    elif is_overtime:
        overtime_min = int(abs(rem_sec) // 60)
        badge_bg_col = COLOR_RED_DARK
        badge_txt_col = COLOR_WHITE
        badge_txt_val = f"+{overtime_min:02d}m OVER"
        tele_txt_val = f"End: {fmt_12h(event.end, False)}"
        tele_col = COLOR_CLOCK_OVER
        bar_col = COLOR_RED if int(now.timestamp()) % 2 == 0 else COLOR_RED_DIM
        next_gap = (
            int(max(0, (next_evt.start - now).total_seconds() / 60.0))
            if next_evt
            else 0
        )
        if next_evt:
            next_sum = sanitize_display_text(next_evt.summary) or "Event"
            title_str = (
                f"{summary} Overrun (+{overtime_min}m) - Next in"
                f" {next_gap}m: {next_sum}"
            )
        else:
            title_str = (
                f"{summary} Overrun (+{overtime_min}m) - Scheduled end was"
                f" {fmt_12h(event.end, True)}"
            )
        glyph_type = "overtime"
        led_color = COLOR_RED
        priority = 60

        badge_bg_elem, badge_txt_elem = make_pill(
            "badge",
            badge_txt_val,
            x=0,
            y=0,
            bg_color=badge_bg_col,
            text_color=badge_txt_col,
        )

        elements = [
            park_text("clock_txt", text=fmt_12h(now, False)),
            badge_bg_elem,
            badge_txt_elem,
            park_text("mid_txt"),
            {
                "id": "tele_txt",
                "type": "text",
                "x": 71,
                "y": 0,
                "font": "small",
                "align": "top_right",
                "color": tele_col,
                "text": tele_txt_val,
            },
        ]

    # -------------------------------------------------------------------------
    # STATE 5: Active Meeting Phase 3 (Wrap-Up Cue - Last 3 Minutes)
    # -------------------------------------------------------------------------
    elif is_wrapup:
        badge_bg_col = COLOR_AMBER
        badge_txt_col = "#000000FF"
        badge_txt_val = "WRAP UP"
        tele_txt_val = f"{int(rem_sec // 60):02d}:{int(rem_sec % 60):02d} rem"
        tele_col = COLOR_AMBER
        bar_col = COLOR_AMBER
        if is_tight_turn and next_evt:
            next_loc = sanitize_display_text(
                clean_location(next_evt.location, next_evt.description)
            )
            next_loc_part = f" - {next_loc}" if next_loc else ""
            next_sum = sanitize_display_text(next_evt.summary) or "Event"
            title_str = (
                f"[TIGHT TURN] {next_sum} starts in {int(buffer_min)}m!{next_loc_part}"
            )
        elif next_evt:
            next_start_fmt = fmt_12h(next_evt.start, True)
            next_sum = sanitize_display_text(next_evt.summary) or "Event"
            title_str = f"Wrap up soon - [NEXT {next_start_fmt}] {next_sum}"
        else:
            title_str = f"Wrap up soon - [ENDS {fmt_12h(event.end, True)}] {summary}"
        glyph_type = resolve_glyph_type(event)
        title_color = COLOR_AMBER
        led_color = COLOR_AMBER
        priority = 60

        badge_bg_elem, badge_txt_elem = make_pill(
            "badge",
            badge_txt_val,
            x=0,
            y=0,
            bg_color=badge_bg_col,
            text_color=badge_txt_col,
        )

        elements = [
            park_text("clock_txt", text=fmt_12h(now, False)),
            badge_bg_elem,
            badge_txt_elem,
            park_text("mid_txt"),
            {
                "id": "tele_txt",
                "type": "text",
                "x": 71,
                "y": 0,
                "font": "small",
                "align": "top_right",
                "color": tele_col,
                "text": tele_txt_val,
            },
        ]

    # -------------------------------------------------------------------------
    # STATE 3: Active Meeting Phase 1 (First 0..15 Seconds: Title Verification)
    # -------------------------------------------------------------------------
    elif elapsed_sec <= 15:
        badge_bg_col = COLOR_RED_DARK
        badge_txt_col = COLOR_WHITE
        badge_txt_val = "LIVE CALL"
        tele_txt_val = f"{round(total_sec / 60.0)}m call"
        tele_col = COLOR_RED
        bar_col = evt_color
        title_str = f"{summary}{loc_part}"
        glyph_type = resolve_glyph_type(event)
        led_color = COLOR_RED
        priority = 60

        badge_bg_elem, badge_txt_elem = make_pill(
            "badge",
            badge_txt_val,
            x=0,
            y=0,
            bg_color=badge_bg_col,
            text_color=badge_txt_col,
        )

        elements = [
            park_text("clock_txt", text=fmt_12h(event.start, False)),
            badge_bg_elem,
            badge_txt_elem,
            park_text("mid_txt"),
            {
                "id": "tele_txt",
                "type": "text",
                "x": 71,
                "y": 0,
                "font": "small",
                "align": "top_right",
                "color": tele_col,
                "text": tele_txt_val,
            },
        ]

    # -------------------------------------------------------------------------
    # STATE 4: Active Meeting Phase 2 (Underway: Focus Flight Deck & Lookahead)
    # -------------------------------------------------------------------------
    else:
        if evt_type == "focus":
            badge_bg_col = "#7B1FA2FF"
            badge_txt_val = "FOCUS"
            tele_col = COLOR_PURPLE
        elif evt_type == "coffee":
            badge_bg_col = COLOR_GREEN_DARK
            badge_txt_val = "BREAK"
            tele_col = COLOR_CLOCK_BREAK
        else:
            badge_bg_col = COLOR_RED_DARK
            badge_txt_val = "LIVE"
            tele_col = COLOR_RED
        badge_txt_col = COLOR_WHITE
        tele_txt_val = f"{max(1, round(rem_sec / 60.0))}m left"
        bar_col = evt_color
        if next_evt:
            next_loc = sanitize_display_text(
                clean_location(next_evt.location, next_evt.description)
            )
            next_loc_str = f" ({next_loc})" if next_loc else ""
            next_start_fmt = fmt_12h(next_evt.start, True)
            next_sum = sanitize_display_text(next_evt.summary) or "Event"
            title_str = (
                f"{summary}{loc_part} - [NEXT {next_start_fmt}] "
                f"{next_sum}{next_loc_str} (in {int(buffer_min)}m)"
            )
        else:
            title_str = f"{summary}{loc_part}"
        glyph_type = resolve_glyph_type(event)
        title_color = resolve_glyph_tint_color(glyph_type)
        led_color = evt_color
        priority = 60

        badge_bg_elem, badge_txt_elem = make_pill(
            "badge",
            badge_txt_val,
            x=0,
            y=0,
            bg_color=badge_bg_col,
            text_color=badge_txt_col,
        )

        elements = [
            park_text("clock_txt", text=fmt_12h(now, False)),
            badge_bg_elem,
            badge_txt_elem,
            park_text("mid_txt"),
            {
                "id": "tele_txt",
                "type": "text",
                "x": 71,
                "y": 0,
                "font": "small",
                "align": "top_right",
                "color": tele_col,
                "text": tele_txt_val,
            },
        ]

    # Tier 2: Micro-glyph + Marquee Stream
    elements.extend(make_glyph_elements(glyph_type, badge_bg_col))
    elements.append(
        {
            "id": "title_txt",
            "type": "text",
            "x": 6,
            "y": 8,
            "width": 66,
            "font": "tiny",
            "color": title_color,
            "text": title_str,
            "scroll_rate": 500,
            "scroll_start_delay": 1200,
        }
    )

    # Tier 3: Precision Progress Horizon (25%, 50%, 75% Ticks & Live Playhead)
    elements.extend(
        [
            make_rect("radar_bg", 0, 14, 72, 2, COLOR_RADAR_BG),
            make_rect("tick_25", 18, 14, 1, 2, COLOR_TICK),
            make_rect("tick_50", 36, 14, 1, 2, COLOR_TICK),
            make_rect("tick_75", 54, 14, 1, 2, COLOR_TICK),
            make_rect("bar_fill", 0, 14, fill_w, 2, bar_col),
            make_rect("playhead", playhead_x, 14, 1, 2, COLOR_WHITE),
            make_rect(
                "tight_pip",
                70 if (is_tight_turn and (is_wrapup or is_overtime)) else -100,
                14 if (is_tight_turn and (is_wrapup or is_overtime)) else -100,
                2 if (is_tight_turn and (is_wrapup or is_overtime)) else 0,
                2 if (is_tight_turn and (is_wrapup or is_overtime)) else 0,
                COLOR_AMBER,
            ),
            park_rect("radar_blk_0"),
            park_rect("radar_blk_1"),
            park_rect("radar_blk_2"),
        ]
    )

    extra: DisplayExtra = {"priority": priority, "led_notification_color": led_color}
    return elements, extra


def render_aero_breather(
    now: datetime, next_event: CalendarEvent
) -> tuple[list[DisplayElement], DisplayExtra]:
    """Render Aero Horizon State 8: Post-Meeting Breather / Buffer Window."""
    free_sec = (next_event.start - now).total_seconds()
    free_min = max(0, int(free_sec / 60.0))
    cd_str = fmt_countdown(free_sec)

    badge_str = f"{free_min}m BREAK"
    loc = sanitize_display_text(
        clean_location(next_event.location, next_event.description)
    )
    loc_part = f" - {loc}" if loc else ""
    next_sum = sanitize_display_text(next_event.summary) or "Event"
    title_str = (
        "Breather Window - Hydrate & Stretch - [NEXT"
        f" {fmt_12h(next_event.start, True)}] {next_sum}{loc_part}"
    )

    # Draining bar calculation (drains toward next meeting)
    drain_w = max(2, min(72, int((free_sec / (20 * 60.0)) * GRID_WIDTH)))

    badge_bg_elem, badge_txt_elem = make_pill(
        "badge", badge_str, x=0, y=0, bg_color=COLOR_GREEN_DARK, text_color=COLOR_WHITE
    )

    elements: list[DisplayElement] = [
        # Tier 1: Breather Banner (Left Green Plate + Right Countdown)
        park_text("clock_txt", text=fmt_12h(now, False)),
        badge_bg_elem,
        badge_txt_elem,
        park_text("mid_txt"),
        {
            "id": "tele_txt",
            "type": "text",
            "x": 71,
            "y": 0,
            "font": "small",
            "align": "top_right",
            "color": COLOR_CLOCK_BREAK,
            "text": cd_str,
        },
    ]

    # Tier 2: Micro-glyph + Marquee
    elements.extend(make_glyph_elements("coffee", COLOR_GREEN))
    elements.append(
        {
            "id": "title_txt",
            "type": "text",
            "x": 7,
            "y": 8,
            "width": 65,
            "font": "tiny",
            "color": COLOR_CLOCK_BREAK,
            "text": title_str,
            "scroll_rate": 500,
            "scroll_start_delay": 1200,
        }
    )

    # Tier 3: Breather Drain Horizon
    elements.extend(
        [
            make_rect("radar_bg", 0, 14, 72, 2, COLOR_RADAR_BG),
            park_rect("tick_25"),
            park_rect("tick_50"),
            park_rect("tick_75"),
            make_rect("bar_fill", 0, 14, drain_w, 2, COLOR_GREEN),
            make_rect("playhead", min(71, drain_w), 14, 1, 2, COLOR_WHITE),
            park_rect("tight_pip"),
            park_rect("radar_blk_0"),
            park_rect("radar_blk_1"),
            park_rect("radar_blk_2"),
        ]
    )

    extra: DisplayExtra = {"priority": 30, "led_notification_color": COLOR_GREEN}
    return elements, extra


# --- Aero Horizon State: Interactive Agenda Peek (Phase 2) ------------------


def render_aero_peek(
    now: datetime,
    peek: AgendaPeekState,
) -> tuple[list[DisplayElement], DisplayExtra]:
    """Render Aero Horizon State: Interactive Agenda Peek (Rotary Dial Navigation)."""
    if not peek.queue:
        return render_aero_idle(now, [])

    idx = max(0, min(len(peek.queue) - 1, peek.selected_index))
    evt = peek.queue[idx]
    total = len(peek.queue)
    curr_1idx = idx + 1

    pill_str = format_peek_bookmark(now, evt, curr_1idx, total)
    start_str = fmt_12h(evt.start, include_ampm=False)
    end_str = fmt_12h(evt.end, include_ampm=True)
    time_range_str = f"{start_str}-{end_str}"
    delta_str = format_relative_delta(now, evt)

    # Dynamic badge background
    if delta_str == "LIVE":
        badge_bg = COLOR_RED_DARK
        badge_color = COLOR_WHITE
        led_color: str | None = LED_COLOR_MEETING
    else:
        badge_bg = "#253342FF"
        badge_color = COLOR_SKY
        led_color = None

    badge_bg_elem, badge_txt_elem = make_pill(
        "badge",
        f"[{delta_str}]",
        x=71,
        y=0,
        bg_color=badge_bg,
        text_color=badge_color,
        align="top_right",
    )

    glyph_type = resolve_glyph_type(evt)
    title_color = resolve_glyph_tint_color(glyph_type)
    summary_str = sanitize_display_text(evt.summary) or "Event"
    loc_str = sanitize_display_text(clean_location(evt.location, evt.description))
    full_title = f"{summary_str} - {loc_str}" if loc_str else summary_str

    elements: list[DisplayElement] = [
        # Tier 1: Telemetry (Bookmark Index + Time Range on left/mid, Delta on right)
        {
            "id": "clock_txt",
            "type": "text",
            "x": 0,
            "y": 0,
            "font": "tiny",
            "color": COLOR_CLOCK,
            "text": f"[{pill_str}] {time_range_str}",
        },
        park_text("mid_txt"),
        badge_bg_elem,
        badge_txt_elem,
        park_text("tele_txt"),
    ]

    # Tier 2: 5x5 Micro-glyph + Title Marquee
    elements.extend(make_glyph_elements(glyph_type, badge_bg))
    elements.append(
        {
            "id": "title_txt",
            "type": "text",
            "x": 6,
            "y": 8,
            "width": 66,
            "font": "tiny",
            "color": title_color,
            "text": full_title,
            "scroll_rate": 500,
            "scroll_start_delay": 1200,
        }
    )

    # Tier 3: Proportional Stepper Rail
    elements.extend(
        [
            make_rect("radar_bg", 0, 14, 72, 2, COLOR_RADAR_BG),
            park_rect("tick_25"),
            park_rect("tick_50"),
            park_rect("tick_75"),
            park_rect("bar_fill"),
            park_rect("playhead"),
            park_rect("tight_pip"),
            park_rect("radar_blk_0"),
            park_rect("radar_blk_1"),
            park_rect("radar_blk_2"),
        ]
    )

    # Dynamic pips for total queue length
    for k in range(total):
        pip_id = f"stepper_{k}"
        if total > 1:
            x_pip = int(k * 66 / (total - 1)) + 3
        else:
            x_pip = 35

        if k == idx:
            elements.append(make_rect(pip_id, max(0, x_pip - 1), 14, 3, 2, COLOR_WHITE))
        else:
            elements.append(make_rect(pip_id, x_pip, 14, 1, 2, "#404040FF"))

    extra: DisplayExtra = {
        "priority": 50,
        "led_notification_color": led_color,
    }
    return elements, extra


# --- FSM Engine -------------------------------------------------------------


def evaluate_display_with_hardware(
    now: datetime, events: list[CalendarEvent]
) -> tuple[list[DisplayElement], DisplayExtra]:
    """Evaluate time against events, alerts, and tactile hardware state.

    Returns (elements, extra).
    """
    global active_alert, active_peek, fired_checkpoints, manual_sync_flash_until
    cfg = get_config()

    # 1. Prune fired checkpoints for events that have ended
    ended_uids = {e.uid for e in events if e.end <= now}
    fired_checkpoints = {
        (uid, cp) for (uid, cp) in fired_checkpoints if uid not in ended_uids
    }

    # 2. Check for active alert banner expiration
    if active_alert is not None and active_alert.is_expired(now):
        active_alert = None

    # 2.5 Check for active peek overlay expiration
    if active_peek is not None and active_peek.is_expired(now):
        active_peek = None

    # 3. If Agenda Peek is active, render peek display
    if active_peek is not None and not active_peek.is_expired(now):
        elements, extra = render_aero_peek(now, active_peek)
        if manual_sync_flash_until is not None and now < manual_sync_flash_until:
            extra["led_notification_color"] = LED_COLOR_SYNC_ACK
        return elements, extra

    # 4. Identify active event or overtime event
    current_event = None
    for evt in events:
        if evt.start <= now < evt.end:
            current_event = evt
            break
        elif evt.end <= now < evt.end + timedelta(minutes=15):
            # Overtime candidate: only if no new event has started
            if not any(e.start <= now < e.end for e in events):
                current_event = evt
                break

    if (
        current_event
        and current_event.start <= now < current_event.end
        and (current_event.uid, 0) not in fired_checkpoints
    ):
        fired_checkpoints.add((current_event.uid, 0))
        if (
            now - current_event.start
        ).total_seconds() < cfg.alert_banner_duration_seconds and cfg.sound:
            _pending_audio_chimes.append(STOCK_SOUND_EVENT_START)

    upcoming_events = [e for e in events if e.start > now and e != current_event]
    upcoming_events.sort(key=lambda e: e.start)

    # 5. Check for upcoming alert triggers
    checkpoints = sorted(cfg.upcoming_alert_sequence_minutes, reverse=True)
    candidate_upcoming = [e for e in events if e.end > now and e != current_event]
    candidate_upcoming.sort(key=lambda e: e.start)

    for evt in candidate_upcoming:
        delta_minutes = (evt.start - now).total_seconds() / 60.0

        eligible_cps = [cp for cp in checkpoints if delta_minutes <= cp]
        if eligible_cps:
            target_cp = min(eligible_cps)

            if evt.start <= now and target_cp != 0:
                continue

            if (
                target_cp == 0
                and (now - evt.start).total_seconds()
                >= cfg.alert_banner_duration_seconds
            ):
                fired_checkpoints.add((evt.uid, 0))
                continue

            for cp in checkpoints:
                if cp > target_cp:
                    fired_checkpoints.add((evt.uid, cp))

            if (evt.uid, target_cp) not in fired_checkpoints:
                fired_checkpoints.add((evt.uid, target_cp))
                active_alert = AlertState(
                    event=evt,
                    checkpoint_minutes=target_cp,
                    expires_at=now
                    + timedelta(seconds=cfg.alert_banner_duration_seconds),
                )
                if cfg.sound:
                    chime = (
                        STOCK_SOUND_EVENT_START
                        if target_cp == 0
                        else STOCK_SOUND_REMINDER
                    )
                    _pending_audio_chimes.append(chime)
                break
        break

    # 6. Render based on FSM State
    if current_event:
        elements, extra = render_aero_active(
            now, current_event, upcoming_events, active_alert
        )
    elif active_alert and not active_alert.is_expired(now):
        elements, extra = render_aero_alert(now, active_alert, upcoming_events)
    else:
        elements, extra = render_aero_idle(now, upcoming_events)

    if manual_sync_flash_until is not None and now < manual_sync_flash_until:
        extra["led_notification_color"] = LED_COLOR_SYNC_ACK

    return elements, extra


def evaluate_display(
    now: datetime, events: list[CalendarEvent]
) -> list[DisplayElement]:
    """Compatibility wrapper returning elements list."""
    elements, _ = evaluate_display_with_hardware(now, events)
    return elements


# --- Hardware Input Stream & Protobuf Wire Decoder --------------------------

BTN_OK: Final[int] = 0
BTN_BACK: Final[int] = 1
BTN_START: Final[int] = 2

ACTION_PRESS: Final[int] = 0
ACTION_RELEASE: Final[int] = 1


def _read_varint(buf: bytes, pos: int) -> tuple[int, int]:
    """Read a protobuf varint from buf at pos."""
    value = 0
    shift = 0
    while pos < len(buf):
        b = buf[pos]
        pos += 1
        value |= (b & 0x7F) << shift
        if not (b & 0x80):
            return value, pos
        shift += 7
        if shift > 63:
            raise ValueError("protobuf varint too long")
    raise ValueError("truncated protobuf varint")


def _iter_proto_fields(buf: bytes) -> Generator[tuple[int, int, Any], None, None]:
    """Yield (field_number, wire_type, value) for a protobuf buffer."""
    pos = 0
    while pos < len(buf):
        key, pos = _read_varint(buf, pos)
        field_no, wire = key >> 3, key & 7
        if wire == 0:
            value, pos = _read_varint(buf, pos)
            yield field_no, wire, value
        elif wire == 1:
            if pos + 8 > len(buf):
                return
            yield field_no, wire, buf[pos : pos + 8]
            pos += 8
        elif wire == 2:
            n, pos = _read_varint(buf, pos)
            end = pos + n
            if end > len(buf):
                return
            yield field_no, wire, buf[pos:end]
            pos = end
        elif wire == 5:
            if pos + 4 > len(buf):
                return
            yield field_no, wire, buf[pos : pos + 4]
            pos += 4
        else:
            return


def decode_input_events(frame: bytes) -> list[tuple[str, Any]]:
    """Decode binary status frame -> [('button', (id, act)), ('encoder', delta)]."""
    events: list[tuple[str, Any]] = []
    for field_no, wire, update in _iter_proto_fields(frame):
        if field_no != 2 or wire != 2:  # State.updates (StateUpdate)
            continue
        for uf, uw, uv in _iter_proto_fields(update):
            if uf != 11 or uw != 2:  # StateUpdate.input (InputEvent)
                continue
            for ef, ew, ev in _iter_proto_fields(uv):
                if ef == 1 and ew == 2:  # InputEvent.button_event
                    button = action = 0
                    for bf, bw, bv in _iter_proto_fields(ev):
                        if bw == 0 and bf == 1:
                            button = int(bv)
                        elif bw == 0 and bf == 2:
                            action = int(bv)
                    events.append(("button", (button, action)))
                elif ef == 2 and ew == 2:  # InputEvent.encoder_event
                    delta = 0
                    for enc_f, enc_w, enc_v in _iter_proto_fields(ev):
                        if enc_f == 1 and enc_w == 0:
                            val = int(enc_v)
                            if val > 0x7FFFFFFF:
                                val -= 0x100000000
                            delta = val
                    if delta != 0:
                        events.append(("encoder", delta))
    return events


def apply_hardware_input(
    now: datetime,
    event_type: str,
    value: Any,
    events: list[CalendarEvent],
) -> bool:
    """Process hardware input event. Returns True if display state changed."""
    global active_alert, active_peek, manual_sync_flash_until
    cfg = get_config()
    state_changed = False

    if event_type == "button":
        btn_id, action = value
        if action == ACTION_PRESS:
            # 1. Early alert dismissal (START or BACK)
            if btn_id in (BTN_START, BTN_BACK) and active_alert is not None:
                active_alert = None
                state_changed = True

            # 2. Exit peek view (BACK)
            if btn_id == BTN_BACK and active_peek is not None:
                active_peek = None
                state_changed = True

            # 3. Manual instant sync (OK)
            if btn_id == BTN_OK:
                manual_sync_flash_until = now + timedelta(
                    seconds=SYNC_ACK_FLASH_DURATION_SECONDS
                )
                _manual_sync_event.set()
                state_changed = True

    elif event_type == "encoder":
        delta = int(value)
        if delta != 0:
            timeout = timedelta(seconds=PEEK_INACTIVITY_TIMEOUT_SECONDS)
            if active_peek is None or active_peek.is_expired(now):
                queue = build_peek_queue(
                    now, events, lookahead_count=cfg.lookahead_count
                )
                if queue:
                    # Find initial index (active event or first upcoming event)
                    start_idx = 0
                    for idx, evt in enumerate(queue):
                        if evt.start <= now < evt.end:
                            start_idx = idx
                            break
                        elif evt.start > now:
                            start_idx = idx
                            break

                    target_idx = max(0, min(len(queue) - 1, start_idx))
                    active_peek = AgendaPeekState(
                        queue=queue,
                        selected_index=target_idx,
                        expires_at=now + timeout,
                    )
                    state_changed = True
            else:
                new_idx = max(
                    0,
                    min(len(active_peek.queue) - 1, active_peek.selected_index + delta),
                )
                active_peek.selected_index = new_idx
                active_peek.expires_at = now + timeout
                state_changed = True

    return state_changed


class InputListener:
    """Stream button/encoder events off a background asyncio task or busylib."""

    def __init__(self, host: str, token: str | None = None) -> None:
        self.host = host
        self.token = token
        self.queue: asyncio.Queue[tuple[str, Any]] = asyncio.Queue()
        self._stop = asyncio.Event()

    def stop(self) -> None:
        self._stop.set()

    async def run(self) -> None:
        """Connect to WebSocket and stream input events with reconnection backoff."""
        # Try busylib AsyncBusyBar first if available
        try:
            from busylib import AsyncBusyBar  # type: ignore[import-untyped]

            backoff = 1.0
            while not self._stop.is_set():
                kwargs: dict[str, Any] = {"token": self.token} if self.token else {}
                bb = AsyncBusyBar(self.host, **kwargs)
                try:
                    async for msg in bb.stream_status_ws():
                        if self._stop.is_set():
                            break
                        backoff = 1.0
                        if not isinstance(msg, dict):
                            continue
                        for upd in msg.get("updates", []):
                            inp = upd.get("input")
                            if not inp:
                                continue
                            if "button_event" in inp:
                                be = inp["button_event"]
                                btn_name = str(be.get("button", "OK")).upper()
                                btn_id = (
                                    BTN_START
                                    if btn_name == "START"
                                    else BTN_BACK
                                    if btn_name == "BACK"
                                    else BTN_OK
                                )
                                action_name = str(be.get("action", "PRESS")).upper()
                                action_id = (
                                    ACTION_RELEASE
                                    if action_name == "RELEASE"
                                    else ACTION_PRESS
                                )
                                self.queue.put_nowait(("button", (btn_id, action_id)))
                            if "encoder_event" in inp:
                                delta = int(inp["encoder_event"].get("delta", 0))
                                if delta != 0:
                                    self.queue.put_nowait(("encoder", delta))
                except asyncio.CancelledError:
                    break
                except Exception:
                    pass
                finally:
                    try:
                        await bb.aclose()
                    except Exception:
                        pass

                if self._stop.is_set():
                    break
                await asyncio.sleep(backoff)
                backoff = min(10.0, backoff * 1.5)
            return
        except ImportError:
            pass

        # Raw websockets client fallback
        try:
            import websockets
        except ImportError:
            print(
                "[input] neither busylib nor websockets available; hardware controls"
                " disabled",
                flush=True,
            )
            return

        raw_host = self.host.rstrip("/")
        if "://" not in raw_host:
            raw_host = "http://" + raw_host
        import urllib.parse

        parsed = urllib.parse.urlparse(raw_host)
        scheme = "wss" if parsed.scheme == "https" else "ws"
        path = parsed.path.rstrip("/") + "/api/status/ws"
        url = urllib.parse.urlunparse((scheme, parsed.netloc, path, "", "", ""))

        backoff = 1.0
        while not self._stop.is_set():
            try:
                async with websockets.connect(
                    url, open_timeout=5, ping_interval=None, ping_timeout=None
                ) as ws:
                    backoff = 1.0
                    async for msg in ws:
                        if self._stop.is_set():
                            break
                        if isinstance(msg, (bytes, bytearray)):
                            for evt in decode_input_events(bytes(msg)):
                                self.queue.put_nowait(evt)
            except asyncio.CancelledError:
                break
            except Exception:
                if self._stop.is_set():
                    break
                await asyncio.sleep(backoff)
                backoff = min(10.0, backoff * 1.5)


# --- Main Loop (AsyncIO) ----------------------------------------------------

_events: list[CalendarEvent] = []


def _update_events(new_events: list[CalendarEvent]) -> None:
    global _events
    _events = list(new_events)


def _get_events() -> list[CalendarEvent]:
    return list(_events)


async def _sync_worker(client: httpx.AsyncClient, ical_url: AnyHttpUrl) -> None:
    """Background coroutine: periodically fetch calendar and update shared store."""
    while True:
        try:
            events = await fetch_events(client, ical_url)
            if events is not None:
                _update_events(events)
                print(f"[sync] fetched {len(events)} event(s)", flush=True)
        except Exception as e:
            print(f"[sync] error: {e}", file=sys.stderr, flush=True)

        poll_interval = get_config().calendar_poll_interval_seconds
        try:
            await asyncio.wait_for(_manual_sync_event.wait(), timeout=poll_interval)
            _manual_sync_event.clear()
            print("[sync] manual sync requested via OK button", flush=True)
        except asyncio.TimeoutError:
            pass


_last_payload_str: str | None = None


async def _display_loop(
    client: httpx.AsyncClient, input_listener: InputListener | None = None
) -> None:
    """Display loop coroutine processing inputs and 1 Hz refreshes."""
    global _last_payload_str
    while True:
        now = datetime.now(timezone.utc)
        events = _get_events()

        if input_listener is not None:
            while not input_listener.queue.empty():
                evt_type, val = input_listener.queue.get_nowait()
                apply_hardware_input(now, evt_type, val, events)

        elements, extra = evaluate_display_with_hardware(now, events)

        # Dispatch any pending alert chimes asynchronously
        if _pending_audio_chimes:
            cfg = get_config()
            while _pending_audio_chimes:
                chime = _pending_audio_chimes.pop(0)
                task = asyncio.create_task(play_sound(client, chime, volume=cfg.volume))
                _background_tasks.add(task)
                task.add_done_callback(_background_tasks.discard)

        import json

        payload_str = json.dumps({"elements": elements, "extra": extra}, sort_keys=True)

        if payload_str != _last_payload_str:
            try:
                await draw(client, elements, **extra)
                _last_payload_str = payload_str
            except httpx.HTTPStatusError as e:
                if e.response.status_code == 409:
                    try:
                        await client.delete(f"{BASE}/api/display/draw", timeout=2.0)
                        await draw(client, elements, **extra)
                        _last_payload_str = payload_str
                    except Exception:
                        pass
                else:
                    print(
                        f"[draw] HTTP {e.response.status_code}: {e.response.text}",
                        file=sys.stderr,
                    )
            except httpx.HTTPError as e:
                print(f"[draw] network error: {e}", file=sys.stderr, flush=True)

        sleep_duration = (
            0.1
            if (active_peek or (input_listener and not input_listener.queue.empty()))
            else 0.5
        )
        await asyncio.sleep(sleep_duration)


async def async_main() -> None:
    cfg = get_config()
    input_listener = InputListener(cfg.host)
    input_task = asyncio.create_task(input_listener.run())

    async with httpx.AsyncClient(timeout=10.0) as client:
        try:
            if cfg.demo:
                _update_events(demo_events())
                print(
                    f"{APP} → {BASE}  [Aero Horizon demo mode]  (Ctrl-C to stop)",
                    flush=True,
                )
                await _display_loop(client, input_listener)
            elif cfg.ical_url:
                print(f"{APP} → {BASE}  (Ctrl-C to stop)", flush=True)
                try:
                    initial_events = await fetch_events(client, cfg.ical_url)
                    if initial_events is not None:
                        _update_events(initial_events)
                except Exception as e:
                    print(
                        f"[sync] initial fetch failed: {e}", file=sys.stderr, flush=True
                    )

                await asyncio.gather(
                    _display_loop(client, input_listener),
                    _sync_worker(client, cfg.ical_url),
                )
            else:
                print(
                    f"{APP} → {BASE}  [no calendar source, use --ical-url, --demo, or"
                    " GCAL_GLANCE_ICAL_URL]",
                    flush=True,
                )
                print("Showing Aero Horizon idle focus runway display.", flush=True)
                await _display_loop(client, input_listener)
        finally:
            input_listener.stop()
            input_task.cancel()
            try:
                await input_task
            except asyncio.CancelledError:
                pass


def main() -> None:
    try:
        asyncio.run(async_main())
    except KeyboardInterrupt:
        print("\nstopped.")


if __name__ == "__main__":
    main()
conftest.py
"""Pytest configuration and fixtures for GCal Glance."""

from collections.abc import Generator

import pytest
import respx

from app import BASE


@pytest.fixture
def mock_busy_bar_api() -> Generator[respx.MockRouter, None, None]:
    """Mock router scoped to the BUSY Bar BASE URL (no real API calls)."""
    with respx.mock(base_url=BASE, assert_all_mocked=True) as respx_mock:
        yield respx_mock
manifest.yaml
name: GCal Glance
author: Kilo59
description: Ambient Google Calendar display for BUSY Bar — at-a-glance meeting countdowns, alerts, and live schedule awareness.
tags:
  - google-calendar
  - calendar
  - productivity
  - info
preview: ./preview.gif
pyproject.toml
[project]
name = "gcal-glance"
version = "0.1.0"
description = "Google Calendar Glance (GCal Glance) for BUSY Bar"
readme = "README.md"
requires-python = ">=3.10"
dependencies = [
    "busylib>=1.3.0",
    "httpx>=0.28.0",
    "pydantic>=2.11",
    "pydantic-settings>=2.13",
]

[project.optional-dependencies]
dev = [
    "pytest>=9.0.0",
    "pytest-asyncio>=0.23.0",
    "respx>=0.22.0",
    "ruff>=0.16.3",
    "mypy>=2.3.1",
    "watchfiles>=0.21.0",
    "python-dateutil>=2.9.0",
    "types-python-dateutil>=2.9.0",
    "prek>=0.4.14",
]

[tool.pytest]
asyncio_mode = "auto"
asyncio_default_fixture_loop_scope = "function"

[tool.ruff]
target-version = "py310"

[tool.ruff.lint]
select = ["E", "F", "W", "I", "UP", "B", "RUF", "PERF"]

[tool.mypy]
python_version = "3.10"
strict = true
requirements.txt
# Note: All dependencies below (httpx, pydantic, pydantic-settings) are already
# sub-dependencies of busylib, which is pre-approved according to project conventions.
busylib>=1.3.0
httpx>=0.28.0
pydantic>=2.11
pydantic-settings>=2.13
tests/__init__.py
tests/test_app.py
"""Unit tests for GCal Glance application (Aero Horizon)."""

import json
from collections.abc import Generator
from datetime import datetime, timedelta, timezone
from typing import Any

import httpx
import pytest
import respx

import app
from app import (
    APP,
    GRID_WIDTH,
    AlertState,
    CalendarEvent,
    DisplayElement,
    GlyphType,
    _expand_rrule,
    _parse_ical_datetime,
    _unescape_ical,
    _unfold_ical,
    _update_events,
    clean_location,
    draw,
    evaluate_display,
    evaluate_display_with_hardware,
    fetch_events,
    fmt_12h,
    fmt_compact_12h,
    fmt_countdown,
    fmt_remaining,
    format_idle_badge,
    format_idle_marquee,
    make_glyph_elements,
    render_aero_active,
    render_aero_alert,
    render_aero_breather,
    resolve_glyph_tint_color,
    resolve_glyph_type,
    resolve_label,
    sanitize_display_text,
)


def make_el_map(elements: list[DisplayElement]) -> dict[str, dict[str, Any]]:
    return {str(el["id"]): dict(el) for el in elements}


@pytest.fixture(autouse=True)
def reset_module_state() -> Generator[None, None, None]:
    """Reset module-level mutable state before and after each test."""
    app.fired_checkpoints.clear()
    app.active_alert = None
    _update_events([])
    yield
    app.fired_checkpoints.clear()
    app.active_alert = None
    _update_events([])


# --- 1. iCal Parsing & Timezone Tests ---------------------------------------


def test_parse_ical_datetime_utc() -> None:
    dt = _parse_ical_datetime("20260815T140000Z")
    assert dt is not None
    assert dt.tzinfo == timezone.utc
    assert dt.year == 2026 and dt.month == 8 and dt.day == 15
    assert dt.hour == 14 and dt.minute == 0 and dt.second == 0


def test_parse_ical_datetime_tzid() -> None:
    # EDT is UTC-4 during August
    dt = _parse_ical_datetime("20260815T100000", "TZID=America/New_York")
    assert dt is not None
    assert dt.tzinfo == timezone.utc
    assert dt.hour == 14 and dt.minute == 0


def test_parse_ical_datetime_allday_events(monkeypatch: pytest.MonkeyPatch) -> None:
    # By default (include_all_day=False), all-day events return None
    assert _parse_ical_datetime("20260815", "VALUE=DATE") is None
    assert _parse_ical_datetime("20260815") is None

    # When include_all_day=True, all-day events are parsed into a valid datetime
    monkeypatch.setattr(app.get_config(), "include_all_day", True)
    dt = _parse_ical_datetime("20260815", "VALUE=DATE")
    assert dt is not None


def test_unfold_and_unescape_ical() -> None:
    raw = (
        "SUMMARY:Line 1\r\n  continued\r\n"
        "DESCRIPTION:Escaped\\, chars\\; and\\nnewline\r\n"
        "LOCATION:Room\\, 3B"
    )
    unfolded = _unfold_ical(raw)
    assert len(unfolded) == 3
    assert unfolded[0] == "SUMMARY:Line 1 continued"
    assert _unescape_ical(unfolded[1]) == "DESCRIPTION:Escaped, chars; and\nnewline"
    assert _unescape_ical(unfolded[2]) == "LOCATION:Room, 3B"


# --- 2. HTTP Interaction Tests (using mock_busy_bar_api fixture) -------------

SAMPLE_ICS = """BEGIN:VCALENDAR
VERSION:2.0
PRODID:-//Google Inc//Google Calendar 70.9054//EN
BEGIN:VEVENT
UID:event-1@google.com
SUMMARY:Sprint Standup
LOCATION:Room 2B
DTSTART:20300815T140000Z
DTEND:20300815T143000Z
DESCRIPTION:Daily sync
END:VEVENT
BEGIN:VEVENT
UID:event-2@google.com
SUMMARY:[dnd] Deep Focus Block
LOCATION:Desk
DTSTART:20300815T150000Z
DTEND:20300815T160000Z
END:VEVENT
END:VCALENDAR
"""


@pytest.mark.asyncio
async def test_fetch_events_success(mock_busy_bar_api: respx.MockRouter) -> None:
    ical_url = "https://calendar.google.com/calendar/ical/feed/basic.ics"
    mock_busy_bar_api.get(ical_url).respond(status_code=200, text=SAMPLE_ICS)

    async with httpx.AsyncClient() as client:
        events = await fetch_events(client, ical_url)

    assert events is not None
    assert len(events) == 2
    assert events[0].uid == "event-1@google.com"
    assert events[0].summary == "Sprint Standup"
    assert events[0].location == "Room 2B"
    assert events[1].uid == "event-2@google.com"
    assert events[1].summary == "[dnd] Deep Focus Block"
    assert events[1].location == "Desk"


@pytest.mark.asyncio
async def test_fetch_events_recurring_rrule_upcoming(
    mock_busy_bar_api: respx.MockRouter, monkeypatch: pytest.MonkeyPatch
) -> None:
    """A recurring event with past DTSTART should produce upcoming occurrences."""
    # Freeze datetime to 2026-08-21 14:00:00 UTC (10:00 AM EDT)
    fixed_now = datetime(2026, 8, 21, 14, 0, 0, tzinfo=timezone.utc)

    # 1. Weekly recurring Friday 18:00 UTC (2:00 PM EDT) started in Jan 2026
    # 2. Future single event on Saturday Aug 22, 2026
    recurring_ics = """BEGIN:VCALENDAR
VERSION:2.0
PRODID:-//Google Inc//Google Calendar 70.9054//EN
BEGIN:VEVENT
UID:recurring-weekly@google.com
SUMMARY:Weekly Team Sync
LOCATION:Meet
DTSTART:20260102T180000Z
DTEND:20260102T183000Z
RRULE:FREQ=WEEKLY;BYDAY=FR
END:VEVENT
BEGIN:VEVENT
UID:future-single@google.com
SUMMARY:Saturday Workshop
LOCATION:Office
DTSTART:20260822T130000Z
DTEND:20260822T140000Z
END:VEVENT
END:VCALENDAR
"""
    ical_url = "https://calendar.google.com/calendar/ical/feed/basic.ics"
    mock_busy_bar_api.get(ical_url).respond(status_code=200, text=recurring_ics)

    # Monkeypatch datetime.now in app module
    class FixedDateTime(datetime):
        @classmethod
        def now(cls, tz: Any = None) -> datetime:
            if tz is not None:
                return fixed_now.astimezone(tz)
            return fixed_now

    monkeypatch.setattr(app, "datetime", FixedDateTime)

    async with httpx.AsyncClient() as client:
        events = await fetch_events(client, ical_url)

    assert events is not None
    # The immediate next event MUST be the occurrence on 2026-08-21 18:00 UTC
    assert len(events) >= 2
    assert events[0].summary == "Weekly Team Sync"
    assert events[0].start == datetime(2026, 8, 21, 18, 0, 0, tzinfo=timezone.utc)
    assert events[0].end == datetime(2026, 8, 21, 18, 30, 0, tzinfo=timezone.utc)
    assert events[1].summary == "Saturday Workshop"


@pytest.mark.asyncio
async def test_fetch_events_recurring_exdate_and_recurrence_id(
    mock_busy_bar_api: respx.MockRouter, monkeypatch: pytest.MonkeyPatch
) -> None:
    """EXDATE skips cancelled occurrences; RECURRENCE-ID overrides instances."""
    fixed_now = datetime(2026, 8, 21, 14, 0, 0, tzinfo=timezone.utc)

    # Weekly Friday event at 18:00 UTC.
    # Aug 21 instance is overridden with RECURRENCE-ID and moved to 19:00 UTC.
    # Aug 28 instance is excluded via EXDATE.
    ics_content = """BEGIN:VCALENDAR
VERSION:2.0
PRODID:-//Google Inc//Google Calendar 70.9054//EN
BEGIN:VEVENT
UID:recurring-base@google.com
SUMMARY:Friday Coffee
DTSTART:20260102T180000Z
DTEND:20260102T183000Z
RRULE:FREQ=WEEKLY;BYDAY=FR
EXDATE:20260828T180000Z
END:VEVENT
BEGIN:VEVENT
UID:recurring-base@google.com
RECURRENCE-ID:20260821T180000Z
SUMMARY:Friday Coffee (Rescheduled)
DTSTART:20260821T190000Z
DTEND:20260821T193000Z
END:VEVENT
BEGIN:VEVENT
UID:cancelled-event@google.com
SUMMARY:Cancelled Meeting
STATUS:CANCELLED
DTSTART:20260821T200000Z
DTEND:20260821T210000Z
END:VEVENT
END:VCALENDAR
"""
    ical_url = "https://calendar.google.com/calendar/ical/feed/basic.ics"
    mock_busy_bar_api.get(ical_url).respond(status_code=200, text=ics_content)

    class FixedDateTime(datetime):
        @classmethod
        def now(cls, tz: Any = None) -> datetime:
            if tz is not None:
                return fixed_now.astimezone(tz)
            return fixed_now

    monkeypatch.setattr(app, "datetime", FixedDateTime)

    async with httpx.AsyncClient() as client:
        events = await fetch_events(client, ical_url)

    assert events is not None
    # Cancelled event must not appear
    assert not any(e.summary == "Cancelled Meeting" for e in events)
    # Aug 21 occurrence is overridden to 19:00
    assert events[0].summary == "Friday Coffee (Rescheduled)"
    assert events[0].start == datetime(2026, 8, 21, 19, 0, 0, tzinfo=timezone.utc)
    # Aug 28 occurrence was in EXDATE, so next base occurrence is Sep 04
    assert events[1].summary == "Friday Coffee"
    assert events[1].start == datetime(2026, 9, 4, 18, 0, 0, tzinfo=timezone.utc)


@pytest.mark.asyncio
async def test_fetch_events_monthly_rrule(
    mock_busy_bar_api: respx.MockRouter, monkeypatch: pytest.MonkeyPatch
) -> None:
    """Monthly BYDAY recurrence (e.g. 3rd Friday of month)."""
    fixed_now = datetime(2026, 8, 21, 14, 0, 0, tzinfo=timezone.utc)

    # 3rd Friday of every month at 14:00 America/New_York (18:00 UTC)
    # Aug 21, 2026 is 3rd Friday of August 2026
    monthly_ics = """BEGIN:VCALENDAR
VERSION:2.0
PRODID:-//Google Inc//Google Calendar 70.9054//EN
BEGIN:VEVENT
UID:monthly-3fr@google.com
SUMMARY:Bill + Gabriel
DTSTART;TZID=America/New_York:20251121T140000
DTEND;TZID=America/New_York:20251121T144000
RRULE:FREQ=MONTHLY;BYDAY=3FR
END:VEVENT
END:VCALENDAR
"""
    ical_url = "https://calendar.google.com/calendar/ical/feed/basic.ics"
    mock_busy_bar_api.get(ical_url).respond(status_code=200, text=monthly_ics)

    class FixedDateTime(datetime):
        @classmethod
        def now(cls, tz: Any = None) -> datetime:
            if tz is not None:
                return fixed_now.astimezone(tz)
            return fixed_now

    monkeypatch.setattr(app, "datetime", FixedDateTime)

    async with httpx.AsyncClient() as client:
        events = await fetch_events(client, ical_url)

    assert events is not None
    assert len(events) >= 1
    assert events[0].summary == "Bill + Gabriel"
    assert events[0].start == datetime(2026, 8, 21, 18, 0, 0, tzinfo=timezone.utc)
    assert events[0].end == datetime(2026, 8, 21, 18, 40, 0, tzinfo=timezone.utc)


def test_expand_rrule_differential_validation_against_dateutil() -> None:
    """Validate stdlib _expand_rrule directly against dateutil.rrule across rules."""
    import re
    from zoneinfo import ZoneInfo

    from dateutil import rrule

    def normalize_for_dateutil(rrule_str: str) -> str:
        rrule_str = re.sub(r"UNTIL=(\d{8})(?!T)", r"UNTIL=\1T235959Z", rrule_str)
        rrule_str = re.sub(r"UNTIL=(\d{8}T\d{6})(?!Z)", r"UNTIL=\1Z", rrule_str)
        return rrule_str

    tz_ny = ZoneInfo("America/New_York")
    test_cases = [
        (datetime(2025, 1, 1, 9, 0, tzinfo=tz_ny), "FREQ=DAILY;INTERVAL=3"),
        (
            datetime(2025, 1, 1, 10, 0, tzinfo=tz_ny),
            "FREQ=WEEKLY;INTERVAL=2;BYDAY=TU,TH",
        ),
        (
            datetime(2025, 1, 1, 11, 0, tzinfo=tz_ny),
            "FREQ=WEEKLY;COUNT=10;BYDAY=MO,WE,FR",
        ),
        (datetime(2025, 11, 21, 14, 0, tzinfo=tz_ny), "FREQ=MONTHLY;BYDAY=3FR"),
        (datetime(2025, 1, 15, 16, 30, tzinfo=tz_ny), "FREQ=MONTHLY;BYMONTHDAY=15"),
        (datetime(2024, 8, 15, 12, 0, tzinfo=tz_ny), "FREQ=YEARLY"),
    ]

    window_start = datetime(2026, 8, 1, 0, 0, tzinfo=timezone.utc)
    window_end = datetime(2026, 10, 1, 0, 0, tzinfo=timezone.utc)

    for dtstart, rule_str in test_cases:
        stdlib_res = _expand_rrule(dtstart, rule_str, window_start, window_end)

        norm_rule = normalize_for_dateutil(rule_str)
        du_rule = rrule.rrulestr(norm_rule, dtstart=dtstart)
        local_tz = dtstart.tzinfo or timezone.utc
        local_win_start = window_start.astimezone(local_tz)
        local_win_end = window_end.astimezone(local_tz)
        du_res = [
            occ.astimezone(timezone.utc)
            for occ in du_rule.between(local_win_start, local_win_end, inc=True)
        ]

        assert stdlib_res == du_res, f"Mismatch for {rule_str}"


@pytest.mark.asyncio
async def test_fetch_events_http_error_returns_none(
    mock_busy_bar_api: respx.MockRouter,
) -> None:
    ical_url = "https://calendar.google.com/calendar/ical/feed/basic.ics"
    mock_busy_bar_api.get(ical_url).respond(
        status_code=500, text="Internal Server Error"
    )

    async with httpx.AsyncClient() as client:
        events = await fetch_events(client, ical_url)

    assert events is None


@pytest.mark.asyncio
async def test_fetch_events_network_failure_returns_none(
    mock_busy_bar_api: respx.MockRouter,
) -> None:
    ical_url = "https://calendar.google.com/calendar/ical/feed/basic.ics"
    mock_busy_bar_api.get(ical_url).mock(
        side_effect=httpx.ConnectError("Connection refused")
    )

    async with httpx.AsyncClient() as client:
        events = await fetch_events(client, ical_url)

    assert events is None


@pytest.mark.asyncio
async def test_draw_dispatches_proper_payload(
    mock_busy_bar_api: respx.MockRouter,
) -> None:
    draw_route = mock_busy_bar_api.post("/api/display/draw").respond(
        status_code=200, json={"status": "ok"}
    )

    elements: list[DisplayElement] = [
        {"id": "clock_txt", "type": "text", "text": "10:48 AM"}
    ]
    async with httpx.AsyncClient() as client:
        await draw(client, elements, priority=45, led_notification_color="#FBBC05FF")

    assert draw_route.called
    req = draw_route.calls.last.request
    payload = json.loads(req.content)
    assert payload["application_name"] == APP
    assert payload["elements"] == elements
    assert payload["priority"] == 45
    assert payload["led_notification_color"] == "#FBBC05FF"


@pytest.mark.asyncio
async def test_draw_raises_on_http_error(mock_busy_bar_api: respx.MockRouter) -> None:
    mock_busy_bar_api.post("/api/display/draw").respond(
        status_code=409, text="Display busy"
    )

    elements: list[DisplayElement] = [
        {"id": "clock_txt", "type": "text", "text": "Conflict test"}
    ]
    async with httpx.AsyncClient() as client:
        with pytest.raises(httpx.HTTPStatusError) as exc_info:
            await draw(client, elements)
        assert exc_info.value.response.status_code == 409


# --- 3. Aero Horizon Invariant Element Map & Procedural Glyphs --------------


REQUIRED_INVARIANT_IDS = {
    "clock_txt",
    "badge_bg",
    "badge_txt",
    "mid_txt",
    "tele_txt",
    "glyph_0",
    "glyph_1",
    "glyph_2",
    "title_txt",
    "radar_bg",
    "tick_25",
    "tick_50",
    "tick_75",
    "bar_fill",
    "playhead",
    "tight_pip",
}


def test_invariant_element_ids_present_across_all_states() -> None:
    now = datetime.now(timezone.utc)
    future_evt = CalendarEvent(
        "e1", "Design Review", now + timedelta(minutes=40), now + timedelta(minutes=70)
    )
    active_evt = CalendarEvent(
        "e2", "Sprint Standup", now - timedelta(minutes=10), now + timedelta(minutes=20)
    )
    alert = AlertState(future_evt, 5, now + timedelta(seconds=15))

    states = [
        evaluate_display(now, []),
        evaluate_display(now, [future_evt]),
        evaluate_display(now, [active_evt]),
        render_aero_alert(now, alert, [future_evt])[0],
        render_aero_breather(now, future_evt)[0],
        render_aero_active(now, active_evt, [future_evt], alert)[0],
    ]

    for state_elements in states:
        element_ids = {el["id"] for el in state_elements}
        for req_id in REQUIRED_INVARIANT_IDS:
            assert req_id in element_ids, f"Missing required element ID {req_id}"


def test_procedural_micro_glyphs() -> None:
    glyph_types: list[GlyphType] = [
        "video",
        "coffee",
        "focus",
        "travel",
        "fitness",
        "celebrate",
        "overtime",
        "calendar",
    ]
    for gtype in glyph_types:
        elems = make_glyph_elements(gtype, "#4285F4FF")
        assert len(elems) == 3
        assert [e["id"] for e in elems] == ["glyph_0", "glyph_1", "glyph_2"]
        assert all(e["type"] == "rectangle" for e in elems)


def test_resolve_glyph_type() -> None:
    e_video = CalendarEvent(
        "1", "Team Zoom Sync", datetime.now(timezone.utc), datetime.now(timezone.utc)
    )
    assert resolve_glyph_type(e_video) == "video"

    e_coffee = CalendarEvent(
        "2",
        "Quick Coffee & Catchup",
        datetime.now(timezone.utc),
        datetime.now(timezone.utc),
    )
    assert resolve_glyph_type(e_coffee) == "coffee"

    e_focus = CalendarEvent(
        "3",
        "Deep Work [focus]",
        datetime.now(timezone.utc),
        datetime.now(timezone.utc),
    )
    assert resolve_glyph_type(e_focus) == "focus"

    e_overtime = CalendarEvent(
        "4", "Late Sync", datetime.now(timezone.utc), datetime.now(timezone.utc)
    )
    assert resolve_glyph_type(e_overtime, is_overtime=True) == "overtime"

    e_cal = CalendarEvent(
        "5", "General Task", datetime.now(timezone.utc), datetime.now(timezone.utc)
    )
    assert resolve_glyph_type(e_cal) == "calendar"

    # UTF-8 Emoji detection
    e_bus = CalendarEvent(
        "6", "🚌 Travel", datetime.now(timezone.utc), datetime.now(timezone.utc)
    )
    assert resolve_glyph_type(e_bus) == "travel"

    e_plane = CalendarEvent(
        "7", "Flight to ORD ✈️", datetime.now(timezone.utc), datetime.now(timezone.utc)
    )
    assert resolve_glyph_type(e_plane) == "travel"

    e_run = CalendarEvent(
        "8", "Morning Run 🏃", datetime.now(timezone.utc), datetime.now(timezone.utc)
    )
    assert resolve_glyph_type(e_run) == "fitness"

    e_party = CalendarEvent(
        "9", "Team Party 🎉", datetime.now(timezone.utc), datetime.now(timezone.utc)
    )
    assert resolve_glyph_type(e_party) == "celebrate"

    e_chill = CalendarEvent(
        "10", "😎 Decompress", datetime.now(timezone.utc), datetime.now(timezone.utc)
    )
    assert resolve_glyph_type(e_chill) == "coffee"


# --- 4. Aero Horizon FSM & Lifecycle Tests -----------------------------------


def test_fsm_idle_state_no_events() -> None:
    now = datetime.now(timezone.utc)
    elements, extra = evaluate_display_with_hardware(now, [])

    el_map = make_el_map(elements)
    assert el_map["mid_txt"]["text"] == "Schedule Clear"
    assert el_map["badge_txt"]["text"] == "FREE"
    assert el_map["title_txt"]["text"] == "Schedule clear"
    assert el_map["radar_bg"]["fill_colors"] == ["#141414FF"]
    assert extra["priority"] == 30


def test_fsm_idle_state_with_upcoming_events() -> None:
    now = datetime.now(timezone.utc)
    evt1 = CalendarEvent(
        "e1",
        "Sprint Planning",
        now + timedelta(minutes=42),
        now + timedelta(minutes=72),
        location="Room 3B",
    )
    evt2 = CalendarEvent(
        "e2", "1-on-1", now + timedelta(minutes=100), now + timedelta(minutes=130)
    )

    elements, _ = evaluate_display_with_hardware(now, [evt1, evt2])
    el_map = make_el_map(elements)

    assert el_map["mid_txt"]["text"] == "42m Focus Runway"
    assert "NEXT" in el_map["badge_txt"]["text"]
    assert "Sprint Planning" in el_map["title_txt"]["text"]
    assert "Room 3B" in el_map["title_txt"]["text"]
    assert "[THEN" in el_map["title_txt"]["text"]
    assert "1-on-1" in el_map["title_txt"]["text"]
    assert "queued" not in el_map["title_txt"]["text"].lower()

    # Check 3-hour radar meeting block rendering
    radar_blk_0 = el_map.get("radar_blk_0")
    assert radar_blk_0 is not None
    assert radar_blk_0["width"] > 0
    assert radar_blk_0["x"] > 0


def test_fsm_checkpoint_alert_sequence() -> None:
    now = datetime.now(timezone.utc)
    evt = CalendarEvent(
        "e1",
        "Architecture Review",
        now + timedelta(minutes=4),
        now + timedelta(minutes=34),
        location="Room 2A",
    )

    # T-4m triggers the 5 MIN checkpoint banner
    res_5m, meta_5m = evaluate_display_with_hardware(now, [evt])
    el_5m = make_el_map(res_5m)
    assert el_5m["badge_txt"]["text"] == "IN 5 MIN"
    assert "Architecture Review" in el_5m["title_txt"]["text"]
    assert meta_5m["priority"] == 45
    assert meta_5m["led_notification_color"] == "#FBBC05FF"

    # Active alert banner persists on next tick
    res_persist, _ = evaluate_display_with_hardware(now + timedelta(seconds=5), [evt])
    el_persist = make_el_map(res_persist)
    assert el_persist["badge_txt"]["text"] == "IN 5 MIN"

    # After 15s expiration, returns to idle focus runway
    res_idle, meta_idle = evaluate_display_with_hardware(
        now + timedelta(seconds=20), [evt]
    )
    el_idle = make_el_map(res_idle)
    assert el_idle["badge_txt"]["text"] != "IN 5 MIN"
    assert "Focus Runway" in el_idle["mid_txt"]["text"]
    assert meta_idle["priority"] == 30

    # T-2m checkpoint
    res_2m, _ = evaluate_display_with_hardware(now + timedelta(minutes=2), [evt])
    el_2m = make_el_map(res_2m)
    assert el_2m["badge_txt"]["text"] == "IN 2 MIN"

    # At start time: enters Active Meeting Phase 1 Title Verification
    res_active_p1, _ = evaluate_display_with_hardware(
        now + timedelta(minutes=4, seconds=5), [evt]
    )
    el_active_p1 = make_el_map(res_active_p1)
    assert el_active_p1["badge_txt"]["text"] == "LIVE CALL"


def test_fsm_active_phase_1_verification() -> None:
    now = datetime.now(timezone.utc)
    evt = CalendarEvent(
        "e1",
        "Quarterly Planning",
        now - timedelta(seconds=10),
        now + timedelta(minutes=30),
        location="Boardroom",
    )

    elements, extra = evaluate_display_with_hardware(now, [evt])
    el_map = make_el_map(elements)

    assert el_map["badge_txt"]["text"] == "LIVE CALL"
    assert "Boardroom" in el_map["title_txt"]["text"]
    assert el_map["tele_txt"]["color"] == "#EA4335FF"
    assert extra["priority"] == 60
    assert extra["led_notification_color"] == "#EA4335FF"


def test_fsm_active_phase_2_flight_deck() -> None:
    now = datetime.now(timezone.utc)
    evt = CalendarEvent(
        "e1",
        "Quarterly Planning",
        now - timedelta(minutes=15),
        now + timedelta(minutes=15),
    )
    next_evt = CalendarEvent(
        "e2",
        "1-on-1 Catchup",
        now + timedelta(minutes=30),
        now + timedelta(minutes=60),
    )

    elements, _ = evaluate_display_with_hardware(now, [evt, next_evt])
    el_map = make_el_map(elements)

    assert el_map["badge_txt"]["text"] == "LIVE"
    assert "15m left" in el_map["tele_txt"]["text"]
    assert "[NEXT" in el_map["title_txt"]["text"]
    assert "1-on-1 Catchup" in el_map["title_txt"]["text"]

    # Precision Horizon Milestone Ticks & Playhead Needle
    assert el_map["tick_25"]["x"] == 18
    assert el_map["tick_50"]["x"] == 36
    assert el_map["tick_75"]["x"] == 54
    assert el_map["playhead"]["fill_colors"] == ["#FFFFFFFF"]
    assert el_map["playhead"]["x"] == 36  # 50% elapsed of 30m call = 36px


def test_fsm_active_phase_3_wrapup_and_tight_turn() -> None:
    now = datetime.now(timezone.utc)
    evt = CalendarEvent(
        "e1",
        "Design Review",
        now - timedelta(minutes=28),
        now + timedelta(minutes=2),  # 2 minutes remaining -> Wrap-up
    )
    # Next event starts in 6 minutes -> 4m gap (<5m -> Tight Turn)
    tight_next = CalendarEvent(
        "e2",
        "Executive Sync",
        now + timedelta(minutes=6),
        now + timedelta(minutes=36),
    )
    # Mark earlier 15m checkpoint as already fired
    app.fired_checkpoints.add(("e2", 15))

    elements, extra = evaluate_display_with_hardware(now, [evt, tight_next])
    el_map = make_el_map(elements)

    assert el_map["badge_txt"]["text"] == "WRAP UP"
    assert "rem" in el_map["tele_txt"]["text"]
    assert "[TIGHT TURN]" in el_map["title_txt"]["text"]
    assert el_map["tight_pip"]["x"] == 70
    assert el_map["tight_pip"]["fill_colors"] == ["#FBBC05FF"]
    assert extra["priority"] == 60
    assert extra["led_notification_color"] == "#FBBC05FF"


def test_fsm_active_overtime_phase() -> None:
    now = datetime.now(timezone.utc)
    evt = CalendarEvent(
        "e1",
        "Sprint Standup",
        now - timedelta(minutes=34),
        now - timedelta(minutes=4),  # 4 minutes overdue
    )

    elements, extra = evaluate_display_with_hardware(now, [evt])
    el_map = make_el_map(elements)

    assert el_map["badge_txt"]["text"] == "+04m OVER"
    assert "End:" in el_map["tele_txt"]["text"]
    assert "Overrun" in el_map["title_txt"]["text"]
    assert el_map["bar_fill"]["width"] == GRID_WIDTH
    assert extra["priority"] == 60
    assert extra["led_notification_color"] == "#EA4335FF"


def test_fsm_interrupted_active_state_preserves_horizon() -> None:
    now = datetime.now(timezone.utc)
    current_meeting = CalendarEvent(
        "m1",
        "Current Strategy Meeting",
        now - timedelta(minutes=15),
        now + timedelta(minutes=15),
    )
    next_meeting = CalendarEvent(
        "m2",
        "Next Architecture Sync",
        now + timedelta(minutes=4),
        now + timedelta(minutes=34),
    )

    elements, _ = evaluate_display_with_hardware(now, [current_meeting, next_meeting])
    el_map = make_el_map(elements)

    # Top banner shows the upcoming checkpoint alert
    assert el_map["badge_txt"]["text"] == "IN 5 MIN"
    assert "[NEXT] Next Architecture" in el_map["title_txt"]["text"]
    assert "on call" in el_map["tele_txt"]["text"]

    # Bottom horizon bar PRESERVES current meeting's 50% progress!
    assert el_map["bar_fill"]["width"] == 36
    assert el_map["bar_fill"]["fill_colors"] == ["#EA4335FF"]
    assert el_map["playhead"]["x"] == 36


def test_render_aero_breather() -> None:
    now = datetime.now(timezone.utc)
    next_meeting = CalendarEvent(
        "m1", "Design Review", now + timedelta(minutes=15), now + timedelta(minutes=45)
    )

    elements, extra = render_aero_breather(now, next_meeting)
    el_map = make_el_map(elements)

    assert el_map["badge_txt"]["text"] == "15m BREAK"
    assert "Breather Window" in el_map["title_txt"]["text"]
    assert el_map["bar_fill"]["fill_colors"] == ["#34A853FF"]
    assert extra["priority"] == 30
    assert extra["led_notification_color"] == "#34A853FF"


def test_fsm_ended_event_pruning() -> None:
    now = datetime.now(timezone.utc)
    past_event = CalendarEvent(
        "past-1", "Past Sync", now - timedelta(minutes=60), now - timedelta(minutes=30)
    )
    app.fired_checkpoints.add(("past-1", 5))

    evaluate_display(now, [past_event])
    assert ("past-1", 5) not in app.fired_checkpoints


# --- 5. Helper & Formatting Tests -------------------------------------------


def test_resolve_label_precedence() -> None:
    now = datetime.now(timezone.utc)
    e_dnd = CalendarEvent("1", "[dnd] Critical release", now, now + timedelta(hours=1))
    assert resolve_label(e_dnd) == "DO NOT DISTURB"

    e_focus = CalendarEvent(
        "2", "Deep focus block [focus]", now, now + timedelta(hours=1)
    )
    assert resolve_label(e_focus) == "DEEP WORK"

    e_break = CalendarEvent(
        "3", "Team Coffee", now, now + timedelta(hours=1), color="#34A853"
    )
    assert resolve_label(e_break) == "ON BREAK"

    e_generic = CalendarEvent("4", "Regular Sync", now, now + timedelta(hours=1))
    assert resolve_label(e_generic) == "IN MEETING"


def test_fmt_12h_format() -> None:
    dt = datetime(2026, 8, 15, 14, 5, tzinfo=timezone.utc)
    res = fmt_12h(dt, include_ampm=True)
    assert "AM" in res or "PM" in res
    assert fmt_12h(dt, include_ampm=False).endswith(":05")


def test_fmt_countdown() -> None:
    assert fmt_countdown(45) == "00:45"
    assert fmt_countdown(125) == "02:05"
    assert fmt_countdown(3660) == "1h01m"


def test_fmt_remaining() -> None:
    assert fmt_remaining(timedelta(minutes=45)) == "45m"
    assert fmt_remaining(timedelta(minutes=90)) == "1h30m"


def test_clean_location() -> None:
    # Physical rooms
    assert clean_location("Room 3B") == "Room 3B"
    assert clean_location("  Building A, Floor 2  ") == "Building A, Floor 2"
    assert clean_location("") == ""

    # Google Meet URLs
    assert clean_location("https://meet.google.com/abc-defg-hij") == "Google Meet"
    assert (
        clean_location("", description="Join: https://meet.google.com/xyz")
        == "Google Meet"
    )
    assert clean_location("Room 3B - https://meet.google.com/abc") == "Room 3B"

    # Zoom URLs
    assert clean_location("https://us02web.zoom.us/j/123456789?pwd=xyz") == "Zoom"
    assert clean_location("", description="Zoom link: https://zoom.us/j/999") == "Zoom"

    # Teams URLs
    assert (
        clean_location("https://teams.microsoft.com/l/meetup-join/19%3ameeting...")
        == "Teams"
    )

    # Webex & Generic URLs
    assert clean_location("https://company.webex.com/meet/user") == "Webex"
    assert clean_location("https://custom-service.com/room1") == "Video Link"


def test_fmt_compact_12h() -> None:
    now_local = datetime.now().astimezone()

    dt_am = now_local.replace(hour=5, minute=51)
    assert fmt_compact_12h(dt_am) == "5:51A"

    dt_am_zero = now_local.replace(hour=10, minute=0)
    assert fmt_compact_12h(dt_am_zero) == "10A"

    dt_pm = now_local.replace(hour=13, minute=30)
    assert fmt_compact_12h(dt_pm) == "1:30P"

    dt_noon = now_local.replace(hour=12, minute=0)
    assert fmt_compact_12h(dt_noon) == "12P"

    dt_midnight = now_local.replace(hour=0, minute=0)
    assert fmt_compact_12h(dt_midnight) == "12A"


def test_format_idle_badge() -> None:
    now = datetime.now().astimezone()

    # 1. Empty schedule
    assert format_idle_badge(now, []) == "FREE"

    # 2. Next event today
    evt_today = CalendarEvent(
        "1",
        "Standup",
        now.replace(hour=10, minute=30),
        now.replace(hour=11, minute=0),
    )
    assert format_idle_badge(now, [evt_today]) == "NEXT 10:30A"

    # 3. Next event tomorrow
    evt_tomorrow = CalendarEvent(
        "2",
        "Travel",
        (now + timedelta(days=1)).replace(hour=5, minute=51),
        (now + timedelta(days=1)).replace(hour=7, minute=30),
    )
    assert format_idle_badge(now, [evt_tomorrow]) == "TMRW 5:51A"

    # 4. Next event within 7 days (weekday only)
    future_date = now + timedelta(days=3)
    day_name = future_date.strftime("%a").upper()
    evt_future = CalendarEvent(
        "3",
        "All Hands",
        future_date.replace(hour=15, minute=0),
        future_date.replace(hour=16, minute=0),
    )
    assert format_idle_badge(now, [evt_future]) == f"{day_name} 3P"

    # 5. Next event past 7 days (weekday + month/day)
    far_future_date = now + timedelta(days=9)
    far_day_name = far_future_date.strftime("%a").upper()
    evt_far = CalendarEvent(
        "4",
        "Offsite",
        far_future_date.replace(hour=10, minute=0),
        far_future_date.replace(hour=11, minute=0),
    )
    assert (
        format_idle_badge(now, [evt_far])
        == f"{far_day_name} {far_future_date.month}/{far_future_date.day} 10A"
    )


def test_format_idle_marquee() -> None:
    now = datetime.now().astimezone().replace(hour=10, minute=0)

    # 1. Empty schedule
    assert format_idle_marquee(now, [], lookahead_count=6) == "Schedule clear"

    # 2. Single event today (starts with time)
    evt_today = CalendarEvent(
        "1",
        "Standup",
        now.replace(hour=10, minute=30),
        now.replace(hour=11, minute=0),
        location="Room 2B",
    )
    res_today = format_idle_marquee(now, [evt_today], lookahead_count=6)
    assert res_today.startswith("10:30 AM - Standup")
    assert "Room 2B" in res_today
    assert "queued" not in res_today.lower()

    # 3. Multiple events today with lookahead_count = 3
    evt_today_2 = CalendarEvent(
        "2",
        "Design Review",
        now.replace(hour=14, minute=0),
        now.replace(hour=15, minute=0),
        location="https://meet.google.com/abc",
    )
    evt_today_3 = CalendarEvent(
        "3",
        "1-on-1 Sync",
        now.replace(hour=16, minute=0),
        now.replace(hour=16, minute=30),
    )
    res_multi = format_idle_marquee(
        now, [evt_today, evt_today_2, evt_today_3], lookahead_count=3
    )
    assert res_multi.startswith("10:30 AM - Standup - Room 2B")
    assert "[THEN 2:00 PM] Design Review (Google Meet)" in res_multi
    assert "[THEN 4:00 PM] 1-on-1 Sync" in res_multi

    # 4. Next event tomorrow (starts directly with title, NOT 'Tomorrow')
    evt_tomorrow = CalendarEvent(
        "4",
        "Quarterly Sync",
        (now + timedelta(days=1)).replace(hour=9, minute=0),
        (now + timedelta(days=1)).replace(hour=10, minute=0),
        location="Auditorium",
    )
    res_tomorrow = format_idle_marquee(now, [evt_tomorrow], lookahead_count=6)
    assert res_tomorrow.startswith("Quarterly Sync - Auditorium")
    assert "tomorrow" not in res_tomorrow.lower()

    # 5. Multi-day lookahead crossing into subsequent days
    res_cross = format_idle_marquee(now, [evt_tomorrow, evt_today_3], lookahead_count=2)
    assert res_cross.startswith("Quarterly Sync - Auditorium")
    assert "[THEN" in res_cross


def test_resolve_glyph_tint_color() -> None:
    assert resolve_glyph_tint_color("travel") == "#FDD663FF"
    assert resolve_glyph_tint_color("video") == "#AECBFAFF"
    assert resolve_glyph_tint_color("focus") == "#D7AEFBFF"
    assert resolve_glyph_tint_color("coffee") == "#81C995FF"
    assert resolve_glyph_tint_color("fitness") == "#81C995FF"
    assert resolve_glyph_tint_color("celebrate") == "#F28B82FF"
    assert resolve_glyph_tint_color("overtime") == "#F28B82FF"
    assert resolve_glyph_tint_color("calendar") == "#AECBFAFF"


def test_no_queued_string_across_all_lifecycle_states() -> None:
    now = datetime.now(timezone.utc)
    evt1 = CalendarEvent(
        "e1", "Event 1", now - timedelta(minutes=10), now + timedelta(minutes=20)
    )
    evt2 = CalendarEvent(
        "e2", "Event 2", now + timedelta(minutes=30), now + timedelta(minutes=60)
    )
    evt3 = CalendarEvent(
        "e3", "Event 3", now + timedelta(minutes=90), now + timedelta(minutes=120)
    )
    alert = AlertState(evt2, 5, now + timedelta(seconds=15))

    states = [
        evaluate_display(now, []),
        evaluate_display(now, [evt2, evt3]),
        evaluate_display(now, [evt1, evt2, evt3]),
        render_aero_alert(now, alert, [evt2, evt3])[0],
        render_aero_breather(now, evt2)[0],
        render_aero_active(now, evt1, [evt2, evt3], alert)[0],
    ]

    for state_elements in states:
        for elem in state_elements:
            if elem.get("type") == "text":
                txt = elem.get("text", "")
                assert "queued" not in txt.lower(), (
                    f"Found 'queued' in element {elem['id']}: {txt}"
                )


def test_sanitize_display_text() -> None:
    assert sanitize_display_text("🚌 Travel") == "Travel"
    assert sanitize_display_text("😎 Decompress") == "Decompress"
    assert sanitize_display_text("⚡ Tight Turn: Meeting") == "Tight Turn: Meeting"
    assert (
        sanitize_display_text("Sprint Planning \u00b7 Room 3B")
        == "Sprint Planning - Room 3B"
    )
    assert (
        sanitize_display_text("\u201cHello\u201d & \u2018World\u2019\u2026")
        == "\"Hello\" & 'World'..."
    )
    assert sanitize_display_text("") == ""


def test_lookahead_count_validation() -> None:
    """Test that lookahead_count is constrained between 2 and 20."""
    from pydantic import ValidationError

    from app import AppConfig

    # Valid values
    assert AppConfig().lookahead_count == 8
    assert AppConfig(lookahead_count=2).lookahead_count == 2
    assert AppConfig(lookahead_count=8).lookahead_count == 8
    assert AppConfig(lookahead_count=20).lookahead_count == 20

    # Invalid values below 2
    with pytest.raises(ValidationError):
        AppConfig(lookahead_count=1)
    with pytest.raises(ValidationError):
        AppConfig(lookahead_count=0)

    # Invalid values above 20
    with pytest.raises(ValidationError):
        AppConfig(lookahead_count=21)


def test_app_name_and_config_env_aliases(monkeypatch: pytest.MonkeyPatch) -> None:
    """Test APP identifier and environment variable alias fallback resolution."""
    from app import APP, AppConfig

    assert APP == "gcal-glance"

    # Primary GCAL_GLANCE_ prefix
    monkeypatch.setenv("GCAL_GLANCE_LOOKAHEAD_COUNT", "8")
    monkeypatch.setenv("GCAL_GLANCE_HOST", "192.168.1.50")
    cfg = AppConfig()
    assert cfg.lookahead_count == 8
    assert cfg.host == "192.168.1.50"

    # Backward compatibility with GCAL_ prefix
    monkeypatch.delenv("GCAL_GLANCE_LOOKAHEAD_COUNT", raising=False)
    monkeypatch.delenv("GCAL_GLANCE_HOST", raising=False)
    monkeypatch.setenv("GCAL_LOOKAHEAD_COUNT", "4")
    monkeypatch.setenv("GCAL_HOST", "10.0.0.99")
    cfg = AppConfig()
    assert cfg.lookahead_count == 4
    assert cfg.host == "10.0.0.99"

    # Backward compatibility with CALSYNC_ prefix
    monkeypatch.delenv("GCAL_LOOKAHEAD_COUNT", raising=False)
    monkeypatch.delenv("GCAL_HOST", raising=False)
    monkeypatch.setenv("CALSYNC_LOOKAHEAD_COUNT", "3")
    monkeypatch.setenv("CALSYNC_HOST", "127.0.0.1")
    cfg = AppConfig()
    assert cfg.lookahead_count == 3
    assert cfg.host == "127.0.0.1"


def test_build_peek_queue() -> None:
    from app import build_peek_queue

    now = datetime(2026, 8, 16, 10, 0, tzinfo=timezone.utc)

    # 1. Empty events
    assert build_peek_queue(now, [], lookahead_count=6) == []

    # 2. Events today + events tomorrow + events next week
    evt_today_1 = CalendarEvent(
        "1", "Standup", now.replace(hour=10, minute=30), now.replace(hour=11, minute=0)
    )
    evt_today_2 = CalendarEvent(
        "2", "Design", now.replace(hour=14, minute=0), now.replace(hour=15, minute=0)
    )
    evt_tmrw = CalendarEvent(
        "3",
        "1-on-1",
        now.replace(day=17, hour=9, minute=0),
        now.replace(day=17, hour=9, minute=30),
    )
    evt_future_1 = CalendarEvent(
        "4",
        "Review",
        now.replace(day=18, hour=10, minute=0),
        now.replace(day=18, hour=11, minute=0),
    )
    evt_future_2 = CalendarEvent(
        "5",
        "All Hands",
        now.replace(day=19, hour=14, minute=0),
        now.replace(day=19, hour=15, minute=0),
    )

    all_events = [evt_today_1, evt_today_2, evt_tmrw, evt_future_1, evt_future_2]

    # With lookahead_count = 2 (today's 2 + next 2 future = 4)
    q = build_peek_queue(now, all_events, lookahead_count=2)
    assert len(q) == 4
    assert [e.uid for e in q] == ["1", "2", "3", "4"]

    # When today has NO events (only future events)
    q_no_today = build_peek_queue(
        now, [evt_tmrw, evt_future_1, evt_future_2], lookahead_count=2
    )
    assert len(q_no_today) == 2
    assert [e.uid for e in q_no_today] == ["3", "4"]


def test_format_peek_bookmark_and_delta() -> None:
    from app import format_peek_bookmark, format_relative_delta

    now = datetime(2026, 8, 16, 10, 0, tzinfo=timezone.utc)

    # Today's event
    evt_today = CalendarEvent(
        "1", "Standup", now.replace(hour=10, minute=30), now.replace(hour=11, minute=0)
    )
    assert format_peek_bookmark(now, evt_today, 1, 4) == "1/4"
    assert format_relative_delta(now, evt_today) == "in 30m"

    # Active LIVE event
    evt_live = CalendarEvent(
        "2", "Call", now.replace(hour=9, minute=30), now.replace(hour=10, minute=30)
    )
    assert format_relative_delta(now, evt_live) == "LIVE"

    # Ended event (e.g. ended 15m ago vs ended 1h ago)
    evt_ended_15m = CalendarEvent(
        "3", "Breakfast", now.replace(hour=9, minute=15), now.replace(hour=9, minute=45)
    )
    assert format_relative_delta(now, evt_ended_15m) == "ended 15m"

    evt_ended_1h = CalendarEvent(
        "3b", "Breakfast", now.replace(hour=8, minute=0), now.replace(hour=9, minute=0)
    )
    assert format_relative_delta(now, evt_ended_1h) == "ended 1h"

    # Tomorrow event
    evt_tmrw = CalendarEvent(
        "4",
        "Sync",
        now.replace(day=17, hour=9, minute=0),
        now.replace(day=17, hour=10, minute=0),
    )
    assert format_peek_bookmark(now, evt_tmrw, 2, 4) == "TMRW 2/4"

    # Within 7 days event (e.g. Wednesday 8/19)
    evt_wed = CalendarEvent(
        "5",
        "Demo",
        now.replace(day=19, hour=14, minute=0),
        now.replace(day=19, hour=15, minute=0),
    )
    assert format_peek_bookmark(now, evt_wed, 3, 4) == "WED 3/4"

    # Far future event (e.g. 10 days out)
    evt_far = CalendarEvent(
        "6",
        "Offsite",
        now.replace(day=26, hour=9, minute=0),
        now.replace(day=26, hour=17, minute=0),
    )
    assert format_peek_bookmark(now, evt_far, 4, 4) == "WED 8/26 4/4"


def test_render_aero_peek() -> None:
    from app import AgendaPeekState, render_aero_peek

    now = datetime(2026, 8, 16, 10, 0, tzinfo=timezone.utc)
    evt1 = CalendarEvent(
        "1",
        "Sprint Sync",
        now.replace(hour=11, minute=0),
        now.replace(hour=11, minute=30),
        location="Room 3B",
    )
    evt2 = CalendarEvent(
        "2",
        "Design",
        now.replace(hour=14, minute=0),
        now.replace(hour=15, minute=0),
        location="https://meet.google.com/abc",
    )

    peek = AgendaPeekState(
        queue=[evt1, evt2],
        selected_index=0,
        expires_at=now + timedelta(seconds=6),
    )

    elements, extra = render_aero_peek(now, peek)
    assert extra["priority"] == 50

    # Invariant element IDs present
    elem_ids = {e["id"] for e in elements}
    assert "clock_txt" in elem_ids
    assert "badge_bg" in elem_ids
    assert "badge_txt" in elem_ids
    assert "title_txt" in elem_ids
    assert "radar_bg" in elem_ids
    assert "stepper_0" in elem_ids
    assert "stepper_1" in elem_ids


def test_decode_input_events() -> None:
    from app import decode_input_events

    # Construct mock protobuf frame for StateUpdate.input:
    # State.updates (f=2,w=2) -> StateUpdate.input (f=11,w=2) -> button_event (f=1,w=2)
    btn_payload = bytes([0x08, 0x00, 0x10, 0x00])  # BTN_OK, PRESS
    input_ev = bytes([0x0A, len(btn_payload)]) + btn_payload
    state_upd = bytes([0x5A, len(input_ev)]) + input_ev
    frame = bytes([0x12, len(state_upd)]) + state_upd

    events = decode_input_events(frame)
    assert len(events) == 1
    assert events[0] == ("button", (0, 0))


def test_apply_hardware_input_alert_dismissal_and_exit() -> None:
    import app
    from app import (
        ACTION_PRESS,
        BTN_BACK,
        BTN_OK,
        BTN_START,
        AgendaPeekState,
        AlertState,
        apply_hardware_input,
    )

    now = datetime(2026, 8, 16, 10, 0, tzinfo=timezone.utc)
    evt = CalendarEvent(
        "1", "Standup", now + timedelta(minutes=5), now + timedelta(minutes=35)
    )

    # 1. Early alert dismissal on START button
    app.active_alert = AlertState(
        event=evt, checkpoint_minutes=5, expires_at=now + timedelta(seconds=15)
    )
    changed = apply_hardware_input(now, "button", (BTN_START, ACTION_PRESS), [evt])
    assert changed is True
    assert app.active_alert is None

    # 2. Early alert dismissal on BACK button
    app.active_alert = AlertState(
        event=evt, checkpoint_minutes=5, expires_at=now + timedelta(seconds=15)
    )
    changed = apply_hardware_input(now, "button", (BTN_BACK, ACTION_PRESS), [evt])
    assert changed is True
    assert app.active_alert is None

    # 3. Exit peek mode on BACK button
    app.active_peek = AgendaPeekState(
        queue=[evt], selected_index=0, expires_at=now + timedelta(seconds=6)
    )
    changed = apply_hardware_input(now, "button", (BTN_BACK, ACTION_PRESS), [evt])
    assert changed is True
    assert app.active_peek is None

    # 4. Manual sync on OK button
    app.manual_sync_flash_until = None
    changed = apply_hardware_input(now, "button", (BTN_OK, ACTION_PRESS), [evt])
    assert changed is True
    assert app.manual_sync_flash_until is not None
    assert app.manual_sync_flash_until > now


def test_apply_hardware_input_encoder_dial() -> None:
    import app
    from app import CalendarEvent, apply_hardware_input

    now = datetime(2026, 8, 16, 10, 0, tzinfo=timezone.utc)
    evt1 = CalendarEvent(
        "1", "Standup", now.replace(hour=10, minute=30), now.replace(hour=11, minute=0)
    )
    evt2 = CalendarEvent(
        "2", "Design", now.replace(hour=14, minute=0), now.replace(hour=15, minute=0)
    )
    events = [evt1, evt2]

    app.active_peek = None

    # Step forward (+1 CW) opens peek view at event 0
    changed = apply_hardware_input(now, "encoder", 1, events)
    assert changed is True
    assert app.active_peek is not None
    assert app.active_peek.selected_index == 0

    # Step forward again (+1 CW) moves to event 1
    apply_hardware_input(now, "encoder", 1, events)
    assert app.active_peek.selected_index == 1

    # Step backward (-1 CCW) moves back to event 0
    apply_hardware_input(now, "encoder", -1, events)
    assert app.active_peek.selected_index == 0


def test_top_led_halo_states() -> None:
    import app
    from app import (
        LED_COLOR_ALERT,
        LED_COLOR_MEETING,
        LED_COLOR_SYNC_ACK,
        AlertState,
        CalendarEvent,
        evaluate_display_with_hardware,
    )

    now = datetime(2026, 8, 16, 10, 0, tzinfo=timezone.utc)

    # 1. Idle state: LED off (None)
    app.active_alert = None
    app.active_peek = None
    app.manual_sync_flash_until = None
    _, extra = evaluate_display_with_hardware(now, [])
    assert extra["led_notification_color"] is None

    # 2. Alert state: LED amber
    evt_soon = CalendarEvent(
        "1", "Standup", now + timedelta(minutes=5), now + timedelta(minutes=35)
    )
    app.active_alert = AlertState(
        event=evt_soon, checkpoint_minutes=5, expires_at=now + timedelta(seconds=15)
    )
    _, extra = evaluate_display_with_hardware(now, [evt_soon])
    assert extra["led_notification_color"] == LED_COLOR_ALERT

    # 3. Active meeting state: LED blue
    app.active_alert = None
    evt_active = CalendarEvent(
        "2", "Review", now - timedelta(minutes=10), now + timedelta(minutes=20)
    )
    _, extra = evaluate_display_with_hardware(now, [evt_active])
    assert extra["led_notification_color"] == LED_COLOR_MEETING

    # 4. Manual sync flash overrides LED color
    app.manual_sync_flash_until = now + timedelta(seconds=0.5)
    _, extra = evaluate_display_with_hardware(now, [])
    assert extra["led_notification_color"] == LED_COLOR_SYNC_ACK


def test_sound_and_volume_config() -> None:
    from pydantic import ValidationError

    from app import AppConfig

    # Default configuration: sound=True, volume=None
    cfg = AppConfig()
    assert cfg.sound is True
    assert cfg.volume is None

    # Custom configuration
    cfg_custom = AppConfig(sound=False, volume=75)
    assert cfg_custom.sound is False
    assert cfg_custom.volume == 75

    # Bounds validation for volume (0..100)
    assert AppConfig(volume=0).volume == 0
    assert AppConfig(volume=100).volume == 100

    with pytest.raises(ValidationError):
        AppConfig(volume=-1)
    with pytest.raises(ValidationError):
        AppConfig(volume=101)


async def test_play_sound_api_dispatch(mock_busy_bar_api: respx.MockRouter) -> None:
    from app import STOCK_SOUND_EVENT_START, play_sound

    play_route = mock_busy_bar_api.post("http://10.0.4.20/api/audio/play").respond(
        json={"success": True}
    )
    vol_route = mock_busy_bar_api.post("http://10.0.4.20/api/audio/volume").respond(
        json={"success": True}
    )

    async with httpx.AsyncClient() as client:
        # Play sound without explicit volume
        await play_sound(client, STOCK_SOUND_EVENT_START)
        assert play_route.called
        assert not vol_route.called
        assert (
            play_route.calls.last.request.headers["content-type"] == "application/json"
        )
        body = json.loads(play_route.calls.last.request.content)
        assert body["stock_path"] == "calendar_event_starts"

        # Play sound with explicit volume setting
        await play_sound(client, STOCK_SOUND_EVENT_START, volume=80)
        assert vol_route.called
        vol_body = json.loads(vol_route.calls.last.request.content)
        assert vol_body["volume"] == 80


def test_alert_triggers_queue_sound_chimes(monkeypatch: pytest.MonkeyPatch) -> None:
    import app
    from app import (
        STOCK_SOUND_EVENT_START,
        STOCK_SOUND_REMINDER,
        CalendarEvent,
        evaluate_display_with_hardware,
    )

    app._pending_audio_chimes.clear()
    app.fired_checkpoints.clear()
    app.active_alert = None

    now = datetime(2026, 8, 16, 9, 45, tzinfo=timezone.utc)
    evt = CalendarEvent(
        "audio-evt-1",
        "Design Review",
        now + timedelta(minutes=15),
        now + timedelta(minutes=45),
    )

    # 1. 15m warning alert -> triggers STOCK_SOUND_REMINDER
    monkeypatch.setattr(app.get_config(), "sound", True)
    evaluate_display_with_hardware(now, [evt])
    assert app._pending_audio_chimes == [STOCK_SOUND_REMINDER]
    app._pending_audio_chimes.clear()

    # 2. 0m event start -> triggers STOCK_SOUND_EVENT_START
    now_start = evt.start
    evaluate_display_with_hardware(now_start, [evt])
    assert app._pending_audio_chimes == [STOCK_SOUND_EVENT_START]
    app._pending_audio_chimes.clear()

    # 3. When sound is muted (sound=False), no chimes are queued
    app.fired_checkpoints.clear()
    app.active_alert = None
    monkeypatch.setattr(app.get_config(), "sound", False)
    evaluate_display_with_hardware(now, [evt])
    assert app._pending_audio_chimes == []

Source and Provenance

View This Version on GitHub →

Source commit: 26b19ba9ce2ba192c45b4c0eadcfad53df42b153
Content SHA-256: e652e22c685e1727e342f2a32a747630d51248d6c6375d17255ad1c58c066931

The Community Workbench

Rate and Discuss

Comments and reactions live in this site’s GitHub repository. Sign in with GitHub to participate.