Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,7 @@ export const calendarCommands = [
- [ ] iCal export/import
- [ ] Notifications for upcoming events
- [ ] Recurring events
- [x] Open google meet links
- [x] cli support (cronos add ...)
- [x] Persistent storage
- [x] Search through event list
Expand Down
1 change: 1 addition & 0 deletions src/app/commands/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ export interface AgendaCommandHandlers {
moveSelection: (delta: number) => void;
editSelection: () => void;
deleteSelection: () => void;
openLink: () => void;
}

export interface AddModalCommandHandlers {
Expand Down
12 changes: 12 additions & 0 deletions src/data/migrations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,18 @@ const MIGRATIONS: Migration[] = [
}
},
},
{
version: 4,
up: (db) => {
const eventColumns = db.query("PRAGMA table_info(events)").all() as {
name: string;
}[];
const eventColumnSet = new Set(eventColumns.map((col) => col.name));
if (!eventColumnSet.has("conference_url")) {
db.run("ALTER TABLE events ADD COLUMN conference_url TEXT");
}
},
},
];

/**
Expand Down
10 changes: 9 additions & 1 deletion src/data/repository.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ interface EventRow {
google_event_id: string | null;
google_calendar_id: string | null;
google_etag: string | null;
conference_url: string | null;
created_at: string;
updated_at: string;
}
Expand All @@ -30,6 +31,7 @@ function rowToEvent(row: EventRow): CalendarEvent {
googleEventId: row.google_event_id ?? undefined,
googleCalendarId: row.google_calendar_id ?? undefined,
googleEtag: row.google_etag ?? undefined,
conferenceUrl: row.conference_url ?? undefined,
updatedAt: row.updated_at ?? undefined,
};
}
Expand All @@ -52,9 +54,10 @@ export const insertEvent = (event: CalendarEvent) =>
google_event_id,
google_calendar_id,
google_etag,
conference_url,
updated_at
)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
`);
stmt.run(
event.id,
Expand All @@ -66,6 +69,7 @@ export const insertEvent = (event: CalendarEvent) =>
event.googleEventId ?? null,
event.googleCalendarId ?? null,
event.googleEtag ?? null,
event.conferenceUrl ?? null,
now,
);
});
Expand Down Expand Up @@ -116,6 +120,10 @@ export const updateEventById = (
setClauses.push("google_etag = ?");
values.push(updates.googleEtag ?? null);
}
if ("conferenceUrl" in updates) {
setClauses.push("conference_url = ?");
values.push(updates.conferenceUrl ?? null);
}

if (setClauses.length === 0) {
return; // Nothing to update
Expand Down
26 changes: 23 additions & 3 deletions src/features/agenda/AgendaSideView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { deleteEvent, useEventsForDate } from "@features/events/eventsState";
import { useTheme } from "@features/theme/themeState";
import type { ScrollBoxRenderable } from "@opentui/core";
import { formatDateKey, formatTimeRange } from "@shared/dateUtils";
import { openBrowser } from "@shared/openBrowser";
import type { CalendarEvent } from "@shared/types";
import { Effect } from "effect";
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
Expand Down Expand Up @@ -98,9 +99,21 @@ export function AgendaSideView({
}
}, [clampedIndex, events, isActive, selectedIndex]);

const openLink = useCallback(() => {
if (!isActive) return;
const event = events[clampedIndex];
const conferenceUrl = event?.conferenceUrl;
if (!conferenceUrl) return;
Effect.runSync(
Effect.catchAll(openBrowser(conferenceUrl), () =>
Effect.sync(() => undefined),
),
);
}, [clampedIndex, events, isActive]);

const agendaHandlers = useMemo(
() => ({ moveSelection, editSelection, deleteSelection }),
[moveSelection, editSelection, deleteSelection],
() => ({ moveSelection, editSelection, deleteSelection, openLink }),
[moveSelection, editSelection, deleteSelection, openLink],
);

useEffect(() => {
Expand All @@ -121,6 +134,7 @@ export function AgendaSideView({
label: "Navigate",
order: ["↑", "↓"],
},
{ commandIds: ["agenda.openLink"], label: "Open link" },
{ commandIds: ["agenda.edit"], label: "Edit" },
{ commandIds: ["agenda.delete"], label: "Delete" },
{ commandIds: ["calendar.toggleAgenda"], label: "Toggle" },
Expand Down Expand Up @@ -163,6 +177,12 @@ export function AgendaSideView({
>
{events.map((event, index) => {
const isSelected = index === clampedIndex;
const linkIndicator = event.conferenceUrl ? " ↗" : "";
const maxTitleLength = Math.max(
0,
titleWidth - linkIndicator.length,
);
const displayTitle = `${event.title.slice(0, maxTitleLength)}${linkIndicator}`;
return (
<box
key={event.id}
Expand All @@ -183,7 +203,7 @@ export function AgendaSideView({
fg={isSelected ? ui.foreground : ui.foregroundDim}
style={{ marginLeft: 1, width: titleWidth }}
>
{event.title.slice(0, titleWidth)}
{displayTitle}
</text>
{isSelected && (
<text fg={ui.selected} style={{ marginLeft: 1 }}>
Expand Down
10 changes: 10 additions & 0 deletions src/features/agenda/agendaCommands.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,4 +42,14 @@ export const agendaCommands = [
ctx.agenda?.deleteSelection();
}),
},
{
id: "agenda.openLink",
title: "Open link",
keys: [{ key: "return", preventDefault: true }],
layers: ["agenda"],
run: (ctx) =>
Effect.sync(() => {
ctx.agenda?.openLink();
}),
},
] as const satisfies readonly CommandDefinition[];
30 changes: 19 additions & 11 deletions src/features/calendar/DayCell.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -68,17 +68,25 @@ export function DayCell({
<text fg={fgColor} style={{ marginLeft: 1 }}>
{String(day).padStart(2, " ")}
</text>
{displayEvents.map((event) => (
<box key={event.id} style={{ flexDirection: "row", marginLeft: 1 }}>
<text fg={theme.eventColors[event.color]}>●</text>
<text
fg={isSelected ? ui.background : ui.foregroundDim}
style={{ marginLeft: 1 }}
>
{event.title.slice(0, DAY_CELL_TITLE_LENGTH)}
</text>
</box>
))}
{displayEvents.map((event) => {
const linkIndicator = event.conferenceUrl ? " ↗" : "";
const maxTitleLength = Math.max(
0,
DAY_CELL_TITLE_LENGTH - linkIndicator.length,
);
const displayTitle = `${event.title.slice(0, maxTitleLength)}${linkIndicator}`;
return (
<box key={event.id} style={{ flexDirection: "row", marginLeft: 1 }}>
<text fg={theme.eventColors[event.color]}>●</text>
<text
fg={isSelected ? ui.background : ui.foregroundDim}
style={{ marginLeft: 1 }}
>
{displayTitle}
</text>
</box>
);
})}
{moreCount > 0 && (
<text
fg={isSelected ? ui.background : ui.foreground}
Expand Down
21 changes: 15 additions & 6 deletions src/features/events/SearchEventsModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -220,6 +220,20 @@ export function SearchEventsModal({
entry.type === "group"
? `↻ x${entry.count}`
: formatTimeRange(event.startTime, event.endTime);
const hasLink =
entry.type === "group"
? entry.hasLink
: Boolean(event.conferenceUrl);
const linkIndicator = hasLink ? " ↗" : "";
const baseTitle =
entry.type === "group"
? `${entry.title} (${entry.count})`
: event.title;
const maxTitleLength = Math.max(
0,
SEARCH_MODAL_TITLE_LENGTH - linkIndicator.length,
);
const displayTitle = `${baseTitle.slice(0, maxTitleLength)}${linkIndicator}`;
return (
<box
key={event.id}
Expand Down Expand Up @@ -247,12 +261,7 @@ export function SearchEventsModal({
fg={isSelected ? ui.foreground : ui.foregroundDim}
style={{ marginLeft: 1 }}
>
{entry.type === "group"
? `${entry.title} (${entry.count})`.slice(
0,
SEARCH_MODAL_TITLE_LENGTH,
)
: event.title.slice(0, SEARCH_MODAL_TITLE_LENGTH)}
{displayTitle}
</text>
{isSelected && (
<text fg={ui.selected} style={{ marginLeft: 1 }}>
Expand Down
2 changes: 2 additions & 0 deletions src/features/events/search/searchUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ export type SearchEntry =
title: string;
color: CalendarEvent["color"];
count: number;
hasLink: boolean;
latest: CalendarEvent;
};

Expand Down Expand Up @@ -80,6 +81,7 @@ export function groupEventsForSearch(events: CalendarEvent[]): SearchEntry[] {
title: latest.title,
color: latest.color,
count: groupEvents.length,
hasLink: groupEvents.some((event) => Boolean(event.conferenceUrl)),
latest,
});
}
Expand Down
20 changes: 2 additions & 18 deletions src/features/google/googleApi.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { spawn } from "node:child_process";
import { createHash, randomBytes } from "node:crypto";
import { createServer } from "node:http";
import { URL } from "node:url";
import { openBrowser } from "@shared/openBrowser";
import type { CalendarEvent, GoogleSettings } from "@shared/types";
import { Effect } from "effect";

Expand Down Expand Up @@ -40,22 +40,6 @@ function createCodeChallenge(verifier: string): string {
return base64UrlEncode(digest);
}

function openBrowser(url: string): void {
const platform = process.platform;
if (platform === "darwin") {
spawn("open", [url], { stdio: "ignore", detached: true }).unref();
return;
}
if (platform === "win32") {
spawn("cmd", ["/c", "start", "", url], {
stdio: "ignore",
detached: true,
}).unref();
return;
}
spawn("xdg-open", [url], { stdio: "ignore", detached: true }).unref();
}

export const startGoogleOAuth = () =>
Effect.gen(function* () {
const clientId = getClientId();
Expand Down Expand Up @@ -126,7 +110,7 @@ export const startGoogleOAuth = () =>
authUrl.searchParams.set("code_challenge", codeChallenge);
authUrl.searchParams.set("code_challenge_method", "S256");

openBrowser(authUrl.toString());
yield* openBrowser(authUrl.toString());

const code = yield* Effect.tryPromise(() => codePromise);
const tokenResponse = yield* Effect.tryPromise(() =>
Expand Down
20 changes: 20 additions & 0 deletions src/features/google/googleSync.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,14 @@ interface GoogleEvent {
updated?: string;
etag?: string;
recurringEventId?: string;
hangoutLink?: string;
conferenceData?: {
entryPoints?: Array<{
entryPointType?: string;
uri?: string;
label?: string;
}>;
};
}

interface GoogleEventListResponse {
Expand Down Expand Up @@ -170,6 +178,15 @@ function parseGoogleDate(event: GoogleEvent): string | null {
return null;
}

function getConferenceUrl(event: GoogleEvent): string | undefined {
const entryPoints = event.conferenceData?.entryPoints ?? [];
const videoEntry = entryPoints.find(
(entry) => entry.entryPointType === "video" && entry.uri,
);
const anyEntry = entryPoints.find((entry) => entry.uri);
return videoEntry?.uri ?? anyEntry?.uri ?? event.hangoutLink ?? undefined;
}

function canWriteCalendar(accessRole?: string): boolean {
return accessRole === "owner" || accessRole === "writer";
}
Expand Down Expand Up @@ -226,6 +243,7 @@ function buildLocalEventFromGoogle(
googleEventId: event.id,
googleCalendarId: calendarId,
googleEtag: event.etag ?? undefined,
conferenceUrl: getConferenceUrl(event),
updatedAt: toIso(event.updated) ?? new Date().toISOString(),
};
}
Expand Down Expand Up @@ -323,6 +341,7 @@ const listGoogleEvents = (
singleEvents: "true",
maxResults: `${MAX_RESULTS}`,
showDeleted: "true",
conferenceDataVersion: "1",
};
if (syncToken) {
params.syncToken = syncToken;
Expand Down Expand Up @@ -519,6 +538,7 @@ const syncCalendar = (
title: mapped.title,
color: mapped.color,
googleEtag: mapped.googleEtag,
conferenceUrl: mapped.conferenceUrl,
updatedAt: mapped.updatedAt,
});
touchedLocalIds.add(local.id);
Expand Down
19 changes: 19 additions & 0 deletions src/shared/openBrowser.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { spawn } from "node:child_process";
import { Effect } from "effect";

export const openBrowser = (url: string) =>
Effect.try(() => {
const platform = process.platform;
if (platform === "darwin") {
spawn("open", [url], { stdio: "ignore", detached: true }).unref();
return;
}
if (platform === "win32") {
spawn("cmd", ["/c", "start", "", url], {
stdio: "ignore",
detached: true,
}).unref();
return;
}
spawn("xdg-open", [url], { stdio: "ignore", detached: true }).unref();
});
1 change: 1 addition & 0 deletions src/shared/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ export interface CalendarEvent {
googleEventId?: string;
googleCalendarId?: string;
googleEtag?: string;
conferenceUrl?: string;
updatedAt?: string; // ISO timestamp
}

Expand Down
Loading