card-widget

star 116

Push live data to the user's ai-desk-card M5Paper e-ink companion display. Use whenever the user asks to show todos, calendar, weather, AI status, inbox counts, next meeting, or any glanceable info on their card / 卡片 / 副屏 / 墨水屏 / e-ink display. 16 widget types available; AI picks slot + widget type, fills data, POSTs to the local card daemon (default 127.0.0.1:9877). Communicates over loopback, so individual widget writes don't trigger AI agent approval prompts.

op7418 By op7418 schedule Updated 5/22/2026

name: card-widget description: | Push live data to the user's ai-desk-card M5Paper e-ink companion display. Use whenever the user asks to show todos, calendar, weather, AI status, inbox counts, next meeting, or any glanceable info on their card / 卡片 / 副屏 / 墨水屏 / e-ink display. 16 widget types available; AI picks slot + widget type, fills data, POSTs to the local card daemon (default 127.0.0.1:9877). Communicates over loopback, so individual widget writes don't trigger AI agent approval prompts. trigger_keywords: - card widget - 卡片 - 副屏 - 墨水屏 - paper card - 显示器副屏 - secondary display allowed-tools: - Bash - Read - Write

card-widget — drive the M5Paper 副屏

The card is a 540 × 960 e-ink panel sitting next to the user's monitor. You push 1-4 widgets and the daemon renders a single frame; transfer time depends on the transport.

Transport & latency (v0.8)

The daemon auto-picks Wi-Fi > USB > BLE at startup. Each path has different per-push characteristics — be aware:

Transport Single-widget update Full frame Power use
Wi-Fi (HTTP) ~0.2 s ~2 s high while connected
USB serial @ 115200 ~1 s region / ~32 s full 32 s n/a
BLE (small cmds only) works for commands; frame data hangs broken low

The daemon also runs a dirty-region diff so unchanged pixels don't re-transmit — typical "one widget updated" push is 5-30 KB instead of the 250 KB full frame. You don't manage this; the daemon decides per push.

Push frequency rules (e-ink hardware):

  • Per-widget content change: fine, push freely (debounced internally at 1.5 s)
  • Per-keystroke streaming: NO. Each push triggers e-ink refresh which damages the panel over time.
  • ai-status taking off / completion: one push at start, one at end.
  • For sub-minute "live" info (timer countdowns, music progress): push at most every ~30 s.

If the user's on USB (1-32 s) instead of Wi-Fi (0.2 s), strongly suggest configuring Wi-Fi via /card-wifi-setup — it's a quality-of-life upgrade.

Layout cheat-sheet

Four slots. Narrow slots (top-left, top-right) are 270 px wide; wide slots (middle, bottom, full) span the full 540 px. Some widgets only shine when wide — see the catalog below.

┌───────────┬───────────┐   top-left  / top-right : 270×280   (narrow)
│ top-left  │ top-right │
├───────────┴───────────┤   middle               : 540×340   (wide)
│        middle         │
├───────────────────────┤   bottom               : 540×280   (wide)
│        bottom         │
└───────────────────────┘
        bar (60 px)         status/settings bar — always on

How to pick a layout

Walk this in order:

  1. Ask user what's most important (or infer from context). The card only fits 4 widgets — be choosy.
  2. One headline widget → middle. The biggest, most readable slot. Use for the day's most important info (calendar, focus, next-meeting).
  3. Two glance widgets → top-left + top-right. Pick widgets whose most-important info fits a narrow column (weather, inbox, ai-status, git-status).
  4. One detail widget → bottom. Lists, multi-row info (todo, deadlines, pr-queue, messages).

Default fallback if the user has no opinion:

top-left  = weather       top-right = ai-status
middle    = calendar      bottom    = todo

If the user is deep in a coding session, swap to:

top-left  = git-status    top-right = ai-status
middle    = focus         bottom    = ai-tasks

If it's a desk-companion / meeting-heavy day:

top-left  = weather       top-right = inbox
middle    = next-meeting  bottom    = pr-queue

Widget catalog

Every widget has a JSON schema at schemas/<type>.schema.json. The "shape" lines below are summaries — read the schema for full constraints.

Work staples

weathernarrow OK

City + big temp + condition + up to 2-day forecast.

{"location":"Beijing",
 "current":{"temp_c":22,"condition":"晴"},
 "forecast":[{"day":"明","high":26,"low":14,"condition":"多云"}]}

calendarwide preferred (narrow shows fewer rows)

Today's events. Pass now_iso so the renderer can mark which is current.

{"now_iso":"2026-05-21T13:30:00",
 "events":[{"start":"09:30","title":"standup"},
           {"start":"14:00","end":"15:00","title":"design review"}]}

next-meetingwide preferred

The single next event with big countdown. Use when only ONE thing matters.

{"title":"design review","start_in":"in 42m","start_at":"14:00",
 "location":"Zoom","attendees":"Alice, Bob, Carol"}

messageswide preferred

Up to 3 IM previews. Each item: sender + preview + age.

{"items":[{"sender":"Bob","preview":"PR ready for review","age":"2m"}]}

inboxnarrow OK

Total unread + per-source breakdown. Cap 4 sources.

{"total":12,
 "sources":[{"name":"gmail","count":4},{"name":"slack","count":8}]}

systemnarrow OK

CPU / mem / disk / battery / net / temp. Vertical 4-row in narrow slot. Note: field is memory_pct (not mem_pct); battery_pct=255 means no battery.

{"cpu_pct":42,"memory_pct":63,"disk_pct":81,"battery_pct":88,
 "net_down_kbps":120,"temp_c":56}

git-statusnarrow OK

Branch + modified/untracked/staged + ahead/behind + last commit.

{"repo_name":"ai-desk-card","branch":"main",
 "modified":3,"untracked":1,"staged":0,"ahead":2,"behind":0,
 "last_commit_msg":"add login flow"}

pr-queuewide preferred

PR counts + up to 4 items. Status: review | yours | approved | blocked.

{"review_count":2,"your_open_count":1,
 "items":[{"number":"#42","title":"fix race","author":"alice","status":"review"},
          {"number":"#51","title":"feat: cron","author":"you","status":"yours"}]}

now-playingwide preferred

Track + artist + position/duration (seconds, not float progress).

{"track":"Stairway","artist":"Led Zeppelin",
 "source":"Spotify","position_sec":252,"duration_sec":482,"playing":true}

Note-taking & focus

scratchwide preferred

Free-form sticky note. The most flexible 记事 component. Use when nothing else fits ("Bob coming at 3pm", "remember to update LinkedIn").

{"text":"3pm 见 Bob — 带上昨天那张设计稿","source":"manual","age":"5m"}

todoeither

Up to 4 tasks. tag: today | tomorrow | this-week | later | overdue.

{"title":"今天",
 "items":[{"text":"刷固件","tag":"today"},
          {"text":"写文档","tag":"overdue","due":"2026-05-20"}]}

focuswide preferred

ONE active task + big text + subtitle + Pomodoro dots.

{"task":"finish onboarding doc",
 "big_text":"18 min","subtitle":"started 12:18 · 番茄 2/4",
 "pomodoros_done":2,"pomodoros_planned":4}

deadlineseither

Multi-day countdown of must-finish-by dates. (Different from todo which is today-focused.)

{"items":[{"title":"H1 review","due_label":"in 2d","is_urgent":true},
          {"title":"tax filing","due_label":"3 weeks"}]}

break-remindernarrow OK

Health nudge. Last-break + sitting + eye-rest.

{"last_break_min_ago":78,"sitting_min":120,"next_eye_rest_min":-5,
 "advice":"stand up + look 20ft away"}

AI monitoring

ai-statusnarrow OK

Model + task + context bar. Push at the start of any non-trivial task.

{"session_name":"refactor auth","model":"Sonnet 4.6","task":"writing tests",
 "context":{"used":42000,"limit":200000},"elapsed_seconds":480}

ai-tasksnarrow OK

Running / waiting / blocked / done-today counters. Vertical in narrow.

{"running":2,"waiting":1,"blocked":0,"completed_today":7}

Disambiguation — common confusions

  • inbox vs messages: inbox is counts per source; messages is named senders' previews. Use inbox for "how much is waiting", messages for "who's poking me".
  • next-meeting vs calendar: next-meeting is THE next event (big countdown); calendar is today's schedule (list).
  • deadlines vs todo vs calendar: deadlines = multi-day countdown; todo = today's tasks; calendar = today's schedule.
  • focus vs todo: focus = ONE active task; todo = a list. If user has one main task and 3 background items, push focus middle + todo bottom.

Pushing — three ways

A. The helper (recommended; validates schema)

$CLAUDE_PLUGIN_ROOT/scripts/widget.sh push <type> <slot> <<EOF
{ "title":"今天", "items":[...] }
EOF

Or directly:

$CLAUDE_PLUGIN_ROOT/skills/card-widget/scripts/push_widget.py \
    todo --slot bottom --data-stdin <<EOF
{ "title":"今天", "items":[{"text":"刷固件","tag":"today"}] }
EOF

B. curl

curl -sf -X POST http://127.0.0.1:9877/widget \
    -H 'Content-Type: application/json' \
    -d '{"type":"todo","slot":"bottom","data":{...},"ttl":1800}'

C. Preview without device

curl -sf -X POST http://127.0.0.1:9877/widgets/preview -o /tmp/p.png && open /tmp/p.png

Pushing fields

  • type — required, one of the 16 above
  • slottop-left | top-right | middle | bottom | full
  • data — required, matches the type's schema
  • ttl — seconds; 0 = no expiry. Use ~1800 for ephemeral info.
  • stale_after — seconds; widget gets a "stale" badge but stays visible
  • theme"" default. Don't set unless user asks.

Failure modes

  • Schema mismatch → daemon returns HTTP 400 with the failing field. Fix and re-push.
  • Daemon unreachable → run /card-onboard to diagnose.
  • Widget shows but text is truncated → you sent too much. Fewer items, shorter strings. The card is glanceable, not a Kindle.
  • Pushed but nothing changed on screen → daemon debounces by ~1.5 s. Wait. If 30 s passes and still no change, check tail /tmp/ai_desk_card_daemon.log.

Before pushing — check connection

bash $CLAUDE_PLUGIN_ROOT/skills/card-onboard/scripts/probe.sh --quick

If transport.connected is false → run /card-onboard first; don't try to push.

Sleep-frame (name card)

The device has a "digital business card" mode: when it deep-sleeps the e-ink panel retains the last frame at 0 W until power-cycled. To push the card and put the device to sleep:

/card-sleep

The card content comes from ai-desk-card/assets/profile.yaml. When the user asks to update their card, edit the YAML directly:

name: "..."           # Big name (≤ ~10 chars wide ideal)
tagline: "..."        # One short subtitle (≤ 36 chars)
bio_lines:            # 2-5 lines, auto-wrap. Empty string = half-gap.
  - "..."
tags:                 # Up to 4 chips
  - icon: "Job"
    text: "..."
qr_image: "qr.png"    # Optional; placeholder if missing
qr_label: "..."       # One line under QR
avatar_image: "avatar.png"
footer: "..."

Avatar + QR are PNGs in ai-desk-card/assets/. If user wants a custom image, ask them to provide it; don't try to generate emoji or scannable QR — Pillow's bundled fonts lack emoji and we use image-based QR intentionally. Tag icons should be plain short labels (Job / City / Web), not emoji.

When to push proactively

  • Starting a non-trivial task: push ai-status with session_name + task. Once per task, not per action — e-ink doesn't like frequent refreshes.
  • Long-running operation finishes: push scratch with a success line ("PR opened: #1234"). User glances at card and knows.
  • Don't push for trivial state changes or every tool call.

Auto-refresh (every 2 hours)

If the user has set up the cron-driven refresh (see /card-refresh skill

  • REFRESH.md), the cron job will re-invoke a headless AI CLI every 2 hours to refresh widgets with fresh data. You don't need to manually poll — just push widgets when relevant during the conversation.
Install via CLI
npx skills add https://github.com/op7418/ai-desk-card --skill card-widget
Repository Details
star Stars 116
call_split Forks 9
navigation Branch main
article Path SKILL.md
More from Creator