diff --git a/packages/spike/src/discord/cron.ts b/packages/spike/src/discord/cron.ts index cbb0643..ff8540e 100644 --- a/packages/spike/src/discord/cron.ts +++ b/packages/spike/src/discord/cron.ts @@ -25,7 +25,6 @@ export const runCronJobs = async (client: Client) => { console.log(`Found ${upcomingReminders.length} upcoming reminders to notify.`) const content = `These reminders are due soon, let them know!: ${JSON.stringify(upcomingReminders)}` - console.log(`🌭`, { content }) const output = await respondToSystemMessage(content, channelId) ensure(output, "The response to reminders should not be undefined") diff --git a/packages/todo/src/index.css b/packages/todo/src/index.css index 515515f..24eb267 100644 --- a/packages/todo/src/index.css +++ b/packages/todo/src/index.css @@ -8,7 +8,7 @@ color: #3B82F6; } -.todo-completed .todo-tag, .todo-completed .todo-date { +.todo-completed .todo-tag, .todo-completed .todo-date, .todo-completed .todo-time-estimate { color: #6B7280; } @@ -16,6 +16,10 @@ color: #10B981; } +.todo-time-estimate { + color: #aBb2d0; +} + .todo-overdue .todo-date { color: #EF4444; } @@ -35,6 +39,10 @@ font-weight: 700; } +.todo-checkbox { + cursor: pointer; +} + .todo-filtered { background-color: greenyellow; } diff --git a/packages/todo/src/todo.test.ts b/packages/todo/src/todo.test.ts index f92611b..c5c3aaa 100644 --- a/packages/todo/src/todo.test.ts +++ b/packages/todo/src/todo.test.ts @@ -4,10 +4,11 @@ import { test, expect } from "bun:test" test("parsing valid todos", () => { expectTodo("- [ ] some words and that is it", { done: false, text: "some words and that is it" }) - expectTodo("- [x] has #some #tags and @2/5/25", { + expectTodo("- [x] has #some #tags and @2/5/25 5m", { done: true, - text: "has #some #tags and @2/5/25", + text: "has #some #tags and @2/5/25 5m", tags: ["some", "tags"], + timeEstimate: 300, dueDate: DateTime.fromISO("2025-02-05"), }) expectTodo("- [ ] works with iso dates @2055/01/2", { diff --git a/packages/todo/src/todo.ts b/packages/todo/src/todo.ts index 0fff502..15a26ee 100644 --- a/packages/todo/src/todo.ts +++ b/packages/todo/src/todo.ts @@ -41,6 +41,10 @@ export class Todo { return this.nodes.map((node) => node.content).join("") } + get timeEstimate() { + return this.nodes.find((node) => node.type === "time-estimate")?.seconds + } + get tags() { return this.nodes.filter((node) => node.type === "tag").map((node) => node.content.slice(1)) } @@ -56,6 +60,7 @@ type TodoNode = { content: string; start: number } & ( | { type: "text" } | { type: "date"; parsed: DateTime } | { type: "tag" } + | { type: "time-estimate"; seconds: number } ) const parseError = (line: string, offset: number, message?: string): string => { @@ -96,12 +101,15 @@ const parseTodo = (line: string): Todo | undefined => { // eat text const isTag = line.slice(offset).startsWith("#") const isDate = line.slice(offset).startsWith("@") + const isTimeEstimate = line.slice(offset).match(/^[\d\.]+[mh]$/) let node: TodoNode | undefined if (isTag) { node = parseTag(line, offset) } else if (isDate) { node = parseDate(line, offset) + } else if (isTimeEstimate) { + node = parseTimeEstimate(line, offset) } else { node = parseText(line, offset) } @@ -115,6 +123,29 @@ const parseTodo = (line: string): Todo | undefined => { return new Todo(todoNodes, done, indent) } +const parseTimeEstimate = (line: string, start: number) => { + const timeMatch = line.slice(start).match(/^([\d\.]+)([mh])($|\s+)/) + const [_, numberString, unitString] = timeMatch ?? [] + if (!numberString || !unitString) { + return parseText(line, start) + } + + const value = parseInt(numberString, 10) + const unit = unitString.toLowerCase() + + let seconds: number + if (unit === "m") { + seconds = value * 60 + } else if (unit === "h") { + seconds = value * 3600 + } else { + return parseText(line, start) + } + + const node: TodoNode = { type: "time-estimate", content: timeMatch![0], seconds, start } + return node +} + const parseTag = (line: string, start: number) => { const tagMatch = line.slice(start).match(/^#(\w+)/) const content = tagMatch?.[0] diff --git a/packages/todo/src/todoClickHandler.ts b/packages/todo/src/todoClickHandler.ts new file mode 100644 index 0000000..280149e --- /dev/null +++ b/packages/todo/src/todoClickHandler.ts @@ -0,0 +1,46 @@ +import { EditorView } from "@codemirror/view" +import { Todo } from "@/todo" + +export const todoClickHandler = EditorView.domEventHandlers({ + click(event, view) { + const pos = view.posAtCoords({ x: event.clientX, y: event.clientY }) + if (pos === null) return false + + const line = view.state.doc.lineAt(pos) + const todo = Todo.parse(line.text) + + if (!todo) return false + + // Find the checkbox position in the line + const checkboxMatch = line.text.match(/^(\s*-\s*\[)\s*(\w?)\s*(\]\s+)/) + if (!checkboxMatch) return false + + const [fullMatch, beforeCheckbox, currentState, afterCheckbox] = checkboxMatch + if (!beforeCheckbox || !afterCheckbox) return false + + const checkboxStart = line.from + beforeCheckbox.length + const checkboxEnd = checkboxStart + 1 + + // Check if the click was within the checkbox area (including some padding) + const clickOffset = pos - line.from + + // Allow clicking anywhere in the "- [ ]" or "- [x]" part + if (clickOffset >= 0 && clickOffset <= fullMatch.length) { + // Toggle the checkbox state + const newState = todo.done ? " " : "x" + + view.dispatch({ + changes: { + from: checkboxStart, + to: checkboxEnd, + insert: newState, + }, + }) + + event.preventDefault() + return true + } + + return false + }, +}) diff --git a/packages/todo/src/todoDecorations.ts b/packages/todo/src/todoDecorations.ts index aca527c..4166b96 100644 --- a/packages/todo/src/todoDecorations.ts +++ b/packages/todo/src/todoDecorations.ts @@ -54,6 +54,13 @@ export const todoDecorations = (filterRef: RefObject) => { } } + // Add checkbox decoration for cursor styling + const checkboxMatch = text.match(/^(\s*-\s*\[)\s*(\w?)\s*(\]\s+)/) + if (checkboxMatch) { + const checkboxEnd = checkboxMatch[0].length + builder.add(line.from, line.from + checkboxEnd, Decoration.mark({ class: "todo-checkbox" })) + } + for (const { type, start, end } of decorationsFor(todo)) { builder.add(line.from + start, line.from + end, Decoration.mark({ class: `todo-${type}` })) } @@ -77,14 +84,10 @@ const decorationsFor = (todo: Todo) => { const decorations: { type: string; start: number; end: number }[] = [] for (const node of todo.nodes) { - const start = node.start + const { type, start } = node const end = node.start + node.content.length - if (node.type === "date") { - decorations.push({ type: "date", start, end }) - } else if (node.type === "tag") { - decorations.push({ type: "tag", start, end }) - } + decorations.push({ type, start, end }) } return decorations diff --git a/packages/todo/src/todoEditor.tsx b/packages/todo/src/todoEditor.tsx index d235535..2a93925 100644 --- a/packages/todo/src/todoEditor.tsx +++ b/packages/todo/src/todoEditor.tsx @@ -7,6 +7,7 @@ import { foldGutter, foldKeymap } from "@codemirror/language" import { refreshFilterEffect, todoDecorations } from "@/todoDecorations" import { autoTodoOnNewline } from "@/autoTodoOnNewline" import { todoKeymap } from "@/todoKeymap" +import { todoClickHandler } from "@/todoClickHandler" import { dateAutocompletion } from "@/dateAutocompletion" import { DateTime } from "luxon" @@ -50,6 +51,7 @@ export const TodoEditor = ({ defaultValue, onChange }: TodoEditorProps) => { changeListener, autoTodoOnNewline, todoKeymap(filterElRef), + todoClickHandler, dateAutocompletion, keymap.of(historyKeymap), keymap.of(defaultKeymap), diff --git a/packages/todo/src/todoList.ts b/packages/todo/src/todoList.ts index d8ab800..773515a 100644 --- a/packages/todo/src/todoList.ts +++ b/packages/todo/src/todoList.ts @@ -24,7 +24,6 @@ export const todoListToString = (todoList: TodoList): string => { } const todoJsonToString = (todoOrString: TodoOrString): string => { - console.log(`🌭`, { todoOrString }) if (typeof todoOrString === "string") { return `${todoOrString}\n` }