Skip to content

Latest commit

 

History

History
241 lines (187 loc) · 9.6 KB

File metadata and controls

241 lines (187 loc) · 9.6 KB

Raycast Reminders Plugin

What We Are Building

A lightweight Raycast plugin whose sole purpose is to surface today's Apple Reminders status at a glance — directly from the Raycast launcher.

Core Behaviour

  • Today's To-Do count — shows how many reminders are due today and not yet completed.
  • Done count — shows how many of today's reminders have already been marked complete.
  • Congratulations state — when every reminder for today is done, the plugin replaces the counters with a celebratory message confirming all tasks were completed.

Why We Are Building It

Raycast is the power-user launcher on macOS. Apple Reminders holds the day's tasks, but there is no native way to see a quick summary count without opening the Reminders app. This plugin brings that summary into the command palette so the user can check their daily progress in under a second without leaving their keyboard-driven workflow.

Tech Stack

  • Raycast Extensions API (TypeScript / React) — the standard framework for building Raycast commands.
  • Swift + EventKit bridge via @raycast/extensions-swift-tools — reads today's reminders natively from the system using Apple's EventKit framework, compiled into a local binary that the extension calls at runtime. This is the same approach used by the official apple-reminders extension.
  • Node.js (bundled by Raycast) — runs the extension process.

Scope (MVP)

  1. A single Raycast Menu Bar Command that shows:
    • 🔲 Remaining: Y ✅ Done: X in the menu bar title
    • Or 🎉 All done! Great work today. when Y = 0 and X > 0 and there were tasks.
  2. Auto-refreshes via Raycast background refresh (interval in package.json).
  3. No write access — read-only view of Reminders data.

Out of Scope (for now)

  • Creating, editing, or deleting reminders.
  • Multi-day or list-specific views.
  • Notifications or badges.

Developer Reference

Raycast API Docs

Menu Bar Command Setup

package.json — command entry:

{
  "name": "today-reminders",
  "title": "Today's Reminders",
  "description": "Shows today's reminder count in the menu bar",
  "mode": "menu-bar",
  "interval": "5m"
}

interval configures background refresh. Minimum value is 1m.

Basic MenuBarExtra shape (TypeScript/React):

import { MenuBarExtra } from "@raycast/api";

export default function Command() {
  return (
    <MenuBarExtra title="🔲 3  ✅ 2" tooltip="Today's Reminders">
      <MenuBarExtra.Item title="You're making great progress!" />
    </MenuBarExtra>
  );
}

Key rules for Menu Bar Commands:

  • isLoading={true} keeps the command alive during async data fetching; set it to false when done.
  • Commands are not long-lived processes — Raycast loads and unloads them on demand.
  • Use useCachedPromise or useCachedState from @raycast/utils for fast cached rendering.

Swift Bridge (via extensions-swift-tools)

The official apple-reminders extension uses a Swift binary to access Apple's EventKit framework. This is the recommended approach for native Reminders access.

Import syntax in TypeScript:

import { getReminders, ... } from "swift:../swift/AppleReminders";

Defining an exported Swift function:

import RaycastSwiftMacros
import EventKit

@raycast func getTodayReminders() async throws -> [ReminderItem] {
    // EventKit query filtered to today
}

Rules for @raycast-decorated functions:

  • Parameters must conform to Decodable; return types to Encodable
  • Only global functions are exported (not struct/class methods)
  • Functions can be async and/or throws

Package.swift dependencies:

// swift-tools-version: 5.9
import PackageDescription

let package = Package(
    name: "AppleReminders",
    platforms: [.macOS(.v12)],
    dependencies: [
        .package(url: "https://github.com/raycast/extensions-swift-tools", from: "1.0.4")
    ],
    targets: [
        .executableTarget(
            name: "AppleReminders",
            dependencies: [
                .product(name: "RaycastSwiftMacros", package: "extensions-swift-tools"),
                .product(name: "RaycastSwiftPlugin", package: "extensions-swift-tools"),
                .product(name: "RaycastTypeScriptPlugin", package: "extensions-swift-tools"),
            ]
        ),
    ]
)

Xcode must be installed on the user's machine to compile the Swift target.

Blueprint Reference — Official apple-reminders Extension

Repo (pinned commit): https://github.com/raycast/extensions/tree/769fcfe527c243875b4e86b8cdce33352f94080b/extensions/apple-reminders/

Key files to reference:

File Purpose
src/menu-bar.tsx Full menu bar command — filtering by today/overdue, section rendering, count logic
src/hooks/useData.tsx Data-fetching hook that calls the Swift bridge and returns { data, isLoading, mutate }
src/helpers.ts isToday(), isOverdue(), isTomorrow() date helpers
swift/AppleReminders/ Swift package — EventKit queries, @raycast exported functions
package.json Extension manifest with menu-bar mode, preferences, and background interval

Reminder data shape (from blueprint):

type Reminder = {
  id: string;
  title: string;
  isCompleted: boolean;
  dueDate?: string;        // ISO date string, optional
  list?: { id: string; title: string; color: string };
  priority?: "high" | "medium" | "low" | null;
  openUrl: string;
};

Filtering today's reminders (from blueprint menu-bar.tsx):

import { isToday, isOverdue } from "./helpers";

const todayReminders = reminders.filter(
  (r) => r.dueDate && (isToday(r.dueDate) || isOverdue(r.dueDate))
);
const done      = todayReminders.filter((r) => r.isCompleted).length;
const remaining = todayReminders.filter((r) => !r.isCompleted).length;

Congratulations state logic:

const allDone = remaining === 0 && done > 0;
const title   = allDone
  ? "🎉 All done!"
  : `🔲 ${remaining}${done}`;

Local Blueprint Files

Full source code from the official apple-reminders extension has been saved locally in reference/ for offline implementation. These are the working, production-grade files to base our code on:

Local file Blueprint source What to take from it
reference/blueprint-menu-bar.tsx src/menu-bar.tsx MenuBarExtra structure, useMemo filtering, isLoading handling, sections pattern
reference/blueprint-helpers.ts src/helpers.ts isToday(), isOverdue() — copy directly, they handle both full-day and datetime formats
reference/blueprint-Reminders.swift swift/.../Reminders.swift getData() function, Reminder/RemindersData structs, EventKit access pattern
reference/blueprint-EventKit+Additions.swift swift/.../EventKit+Additions.swift EKReminder.toStruct(), date formatters, fetchReminders async wrapper
reference/blueprint-package.json package.json Dependency versions, scripts, menu-bar command config with interval

Important for implementation: The blueprint's getData() fetches only incomplete reminders. To get today's done count, we also need getCompletedReminders() (also in the blueprint). Both are already saved in reference/blueprint-Reminders.swift.


Development Workflow

Prerequisites

  • Raycast installed on macOS
  • Node.js 22.14+ (use nvm to manage versions)
  • Xcode installed (needed to compile the Swift/EventKit bridge)

Dev Loop

cd reminders-plugin
npm install                 # install dependencies
npm run dev                 # start development server

npm run dev (which runs ray develop under the hood):

  1. Builds the TypeScript and compiles the Swift package.
  2. Registers the extension with Raycast under a "Development" section in root search.
  3. Hot-reloads on every file save — changes appear instantly in Raycast.
  4. Logs console.log() output to the terminal.
  5. Shows an error overlay inside Raycast for unhandled exceptions.

The Swift binary is compiled on first run and cached. It only recompiles when Swift source files change.

Testing Checklist

Scenario How to verify
Happy path Open Raycast → see menu bar icon with counts → compare against Reminders.app
Complete a reminder Mark done in Reminders.app → wait for refresh (or re-trigger) → count updates
All done Complete all today's reminders → menu bar shows 🎉 All done!
No reminders Clear all today's reminders → menu bar shows empty/no-reminders state
Overdue Create a reminder for yesterday → it should appear in "remaining" count
Permission denied Revoke Reminders access in System Settings → extension shows access-denied message
Background refresh Leave running → after interval (5m) counts auto-update without manual trigger

Debugging Tools

  • Console: console.log() in TypeScript → visible in terminal running npm run dev
  • React DevTools: Press ⌘ ⌥ D while command is focused in Raycast
  • Node.js debugger: Attach via VS Code extension for breakpoints
  • Error overlay: Unhandled exceptions render as visual errors in Raycast (dev mode only)

Useful Commands

npm run build              # production build (ray build -e dist)
npm run lint               # lint check
npm run fix-lint           # auto-fix lint issues
npm run publish            # publish to Raycast Store