{
  "format" : 1,
  "manifest" : {
    "accent" : "orange",
    "capabilities" : [
      {
        "reminders" : {

        }
      }
    ],
    "homepage" : "https://github.com/navbytes/stackbar-plugins/tree/main/Productivity/Todo",
    "id" : "io.navbytes.stackbar.plugins.todo",
    "minimumStackbar" : "0.18.0",
    "name" : "Todo",
    "summary" : "A calm to-do list that understands dates as you type them, kept in Apple Reminders so it's on all your devices.",
    "surfaces" : [
      "menuBar",
      "panel",
      "settings",
      "widget"
    ],
    "symbol" : "checklist",
    "triggers" : [
      {
        "launch" : {

        }
      },
      {
        "every" : {
          "seconds" : 30
        }
      },
      {
        "wake" : {

        }
      },
      {
        "reminders" : {

        }
      }
    ],
    "version" : "2.0.0"
  },
  "publisher" : {
    "name" : "navbytes",
    "publicKey" : "4dpLtXEpFXdi+6cviY/KdYuUulJPbEmCxxIK0EEN2/w="
  },
  "signature" : "yadEvlI/YogKMCP7lAlvmJqFSvJa73rbAAJEpejIkSd211DbZ9wD5pIQndb5C3/ORYUyY15mjSo8w/YHI38gCg==",
  "source" : "import StackbarKit\nimport Foundation\n\n/// A calm to-do list for the menu bar. Type a task the way you would say it\n/// (\"Call the dentist tomorrow at 2pm\", \"Water the plants every Sunday\") and\n/// the date, time and repeat come out of the words. Today's tasks sit on\n/// top, the rest wait below, and a timed task taps you on the shoulder when\n/// it is due. Ticking off a repeating task moves it to its next day. Every\n/// tick, delete and reschedule can be undone.\n///\n/// Tasks live in Apple Reminders by default, so they reach the iPhone, iPad\n/// and Watch, and Reminders lists are the categories. If Reminders access\n/// is declined, or Settings says \"This Mac only\", they stay on this Mac.\n/// A big task breaks into small, medium and big steps.\n@main\nfinal class Todo: StackbarPlugin {\n    static let manifest = Manifest(\n        id: \"io.navbytes.stackbar.plugins.todo\",\n        name: \"Todo\",\n        version: \"2.0.0\",\n        summary: \"A calm to-do list that understands dates as you type them, kept in Apple Reminders so it's on all your devices.\",\n        surfaces: [.menuBar, .panel, .settings, .widget],\n        triggers: [.launch, .every(.seconds(30)), .wake, .reminders],\n        capabilities: [.reminders],\n        minimumStackbar: \"0.18.0\",\n        homepage: \"https://github.com/navbytes/stackbar-plugins/tree/main/Productivity/Todo\",\n        accent: .orange,\n        symbol: \"checklist\"\n    )\n\n    // MARK: Previews\n\n    static let previews: [PreviewScene<Todo>] = [\n        PreviewScene(\"hero\", \"Type it the way you'd say it; dates are understood\") { plugin in\n            plugin.tasks = samples(now: plugin.now)\n        },\n        PreviewScene(\"widget\", \"Today's list on your desktop\", surface: .widget) { plugin in\n            plugin.tasks = samples(now: plugin.now)\n        },\n    ]\n\n    static func samples(now: Date) -> [Item] {\n        let calendar = Calendar.current\n        let today = calendar.startOfDay(for: now)\n        func at(_ days: Int, _ hour: Int?) -> Date? {\n            let day = calendar.date(byAdding: .day, value: days, to: today) ?? today\n            return hour.flatMap { calendar.date(bySettingHour: $0, minute: 0, second: 0, of: day) } ?? day\n        }\n        let list: [(String, Int?, Int?, Repeat?, Bool, Bool)] = [\n            (\"Send the invoice to Northwind\", -1, nil, nil, true, false),\n            (\"Call mum\", 0, 18, nil, false, false),\n            (\"Review Priya's checkout PR\", 0, 15, nil, true, false),\n            (\"Water the plants\", 0, nil, .weekly, false, false),\n            (\"Book flights for the offsite\", 1, nil, nil, false, false),\n            (\"Stand-up notes\", 1, 9, .weekdays, false, false),\n            (\"Renew passport\", 12, nil, nil, false, false),\n            (\"Read the SwiftUI layout chapter\", nil, nil, nil, false, false),\n            (\"Buy coffee beans\", 0, nil, nil, false, true),\n        ]\n        return list.enumerated().map { index, entry in\n            let (title, days, hour, repeats, important, finished) = entry\n            var item = Item(id: \"sample-\\(index)\", title: title, due: days.flatMap { at($0, hour) }, hasTime: hour != nil,\n                            repeats: repeats, important: important, created: now.addingTimeInterval(-86400 * 3))\n            if finished { item.done = now.addingTimeInterval(-3600) }\n            if title.hasPrefix(\"Book flights\") {\n                item.steps = [Step(id: \"s1\", title: \"Pick the dates with the team\", done: true), Step(id: \"s2\", title: \"Compare two airlines\"), Step(id: \"s3\", title: \"Book and share the plan\", size: .medium)]\n            }\n            return item\n        }\n    }\n\n    /// Warm and quiet: the earthy orange of the shared design system.\n    let theme = Theme.orange\n\n    @Stored(\"tasks\") var tasks: [Item] = []\n    @Stored(\"menuBarShows\") var menuBarShows = MenuBarShows.today.rawValue\n    @Stored(\"reminders\") var reminders = true\n    @Stored(\"sound\") var sound = false\n    @Stored(\"summaryHour\") var summaryHour = -1\n    @Stored(\"lastSummary\") var lastSummary = \"\"\n    @Stored(\"keepDoneDays\") var keepDoneDays = 7\n    @Stored(\"showDone\") var showDone = false\n    @Stored(\"storage\") var storage = Storage.reminders.rawValue\n    /// Each task as it was last written to or read from Reminders, by id:\n    /// what a change is measured against, so only changed fields are sent.\n    @Stored(\"mirror\") var mirror: [String: Item] = [:]\n    /// The Reminders list the panel shows; empty for all.\n    @Stored(\"listFilter\") var listFilter = \"\"\n    @Stored(\"nudgeHour\") var nudgeHour = -1\n    @Stored(\"lastNudge\") var lastNudge = \"\"\n\n    /// Whether Reminders said yes, as of the last refresh.\n    var access: ReminderAccess?\n    var lists: [ReminderList] = []\n    /// The last thing that went wrong talking to Reminders.\n    var syncProblem: String?\n    /// The task whose step field is open.\n    var breaking: String?\n\n    var now = Date()\n    /// The task open in the inline editor.\n    var editing: String?\n    /// What just happened, with the list as it was before, for Undo.\n    var notice: Notice?\n\n    struct Notice {\n        var text: String\n        var before: [Item]?\n        var at: Date\n    }\n\n    enum Storage: String, CaseIterable {\n        case reminders = \"Reminders\"\n        case local = \"This Mac only\"\n    }\n\n    /// Tasks go to and come from Reminders right now.\n    var syncing: Bool { storage == Storage.reminders.rawValue && access == .granted }\n\n    enum MenuBarShows: String, CaseIterable {\n        case today = \"Count for today\"\n        case next = \"Next task\"\n        case open = \"Count of all open\"\n        case icon = \"Icon only\"\n    }\n\n    func refresh() async throws {\n        now = Date()\n        await sync()\n        remind()\n        summarize()\n        nudge()\n        tidy()\n        if let notice, now.timeIntervalSince(notice.at) > 20, editing == nil { self.notice = nil }\n    }\n\n    // MARK: - Surfaces\n\n    var menuBar: ViewContent {\n        switch MenuBarShows(rawValue: menuBarShows) ?? .today {\n        case .today:\n            Label(dueToday.isEmpty ? \"\" : \"\\(dueToday.count)\", symbol: dueToday.isEmpty ? \"checklist.checked\" : \"checklist\").role(.secondary)\n        case .open:\n            Label(open.isEmpty ? \"\" : \"\\(open.count)\", symbol: open.isEmpty ? \"checklist.checked\" : \"checklist\").role(.secondary)\n        case .next:\n            if let next = dueToday.first {\n                Label(Self.clip(next.nextStep?.title ?? next.title, 22) + (next.hasTime ? \" · \" + Self.time(next.due ?? now) : \"\"), symbol: \"checklist\").role(.secondary)\n            } else {\n                Label(\"\", symbol: \"checklist.checked\").role(.secondary)\n            }\n        case .icon:\n            Label(\"\", symbol: \"checklist\").role(.secondary)\n        }\n    }\n\n    var panel: ViewContent {\n        PanelHeader(headline, subtitle: subtitle, symbol: open.isEmpty ? \"leaf.fill\" : \"checklist\", theme: theme) {\n            if todayTotal > 0 {\n                Gauge(value: Double(doneToday.count), total: Double(todayTotal), theme: theme, size: 30, lineWidth: 4, label: \"\\(doneToday.count) of \\(todayTotal) done today\")\n            }\n            IconButton(showDone ? \"eye.slash\" : \"checkmark.circle\", label: showDone ? \"Hide done\" : \"Show done\", theme: theme, prominence: showDone ? .tinted : .plain, key: \"show-done\") {\n                self.showDone.toggle()\n            }\n        }\n        TextField(\"Add a task, like “Call mum tomorrow 6pm”\", text: \"\", key: \"add\") { text in\n            self.add(text)\n            await self.save()\n        }\n        if syncing && lists.count > 1 {\n            Picker(\"List\", options: [\"All lists\"] + lists.map(\\.title), selection: lists.first { $0.id == listFilter }?.title ?? \"All lists\", key: \"list-filter\") { title in\n                self.listFilter = self.lists.first { $0.title == title }?.id ?? \"\"\n            }\n        }\n        if storage == Storage.reminders.rawValue, let access, access != .granted, access != .notDetermined {\n            Banner(access == .unavailable ? \"Reminders isn't available here, so tasks stay on this Mac.\" : \"Stackbar can't use Reminders, so tasks stay on this Mac. Allow it in System Settings › Privacy & Security › Reminders.\", symbol: \"icloud.slash\", theme: theme) {\n                ActionButton(\"Keep on this Mac\", theme: theme, key: \"use-local\") { self.storage = Storage.local.rawValue }\n            }\n        }\n        if let notice {\n            Banner(notice.text, symbol: \"checkmark.circle.fill\", theme: theme) {\n                if notice.before != nil {\n                    ActionButton(\"Undo\", theme: theme, key: \"undo\") {\n                        self.undo()\n                        await self.save()\n                    }\n                }\n            }\n        }\n        if shown(open).isEmpty && !(showDone && !shown(done).isEmpty) {\n            EmptyState(\"Nothing on your plate\", symbol: \"leaf\", message: \"Type a task above when something comes up. Dates and times in it are understood.\", theme: theme)\n        }\n        group(\"Still on your list\", overdue)\n        group(\"Today\", today)\n        group(\"Tomorrow\", tomorrow)\n        group(\"Coming up\", later)\n        group(\"Someday\", someday)\n        if showDone && !shown(done).isEmpty {\n            Card(\"Done\", detail: \"\\(shown(done).count)\", spacing: 0, padding: Space.s) {\n                ForEach(shown(done).prefix(30)) { task in row(task) }\n                Button(key: \"clear-done\", action: {\n                    self.clearDone()\n                    await self.save()\n                }) {\n                    Text(\"Clear done tasks\").typography(.caption).foreground(Theme.critical.textPaint)\n                }\n                .padding(horizontal: Space.s, vertical: Space.xs)\n            }\n        }\n    }\n\n    var widget: ViewContent {\n        VStack(alignment: .start, spacing: Space.xs) {\n            HStack(spacing: Space.xs) {\n                Text(\"TODAY\").typography(.overline).foreground(theme.textPaint)\n                Spacer()\n                if todayTotal > 0 {\n                    Text(\"\\(doneToday.count)/\\(todayTotal)\").role(.secondary).typography(.captionNumber)\n                }\n            }\n            if dueToday.isEmpty {\n                Label(open.isEmpty ? \"A clear list\" : \"Nothing due today\", symbol: \"leaf\").role(.secondary)\n            } else {\n                ForEach(dueToday.prefix(4)) { task in\n                    HStack(spacing: Space.xs) {\n                        Image(systemName: \"circle\").font(size: 11).foreground(.role(.secondary))\n                        Text(Self.clip(task.nextStep.map { \"\\(task.title): \\($0.title)\" } ?? task.title, 34)).typography(.body)\n                        Spacer()\n                        if task.hasTime, let due = task.due { Text(Self.time(due)).role(.secondary).typography(.captionNumber) }\n                    }\n                }\n                if dueToday.count > 4 { Text(\"and \\(dueToday.count - 4) more\").role(.secondary).typography(.caption) }\n            }\n        }\n        .frame(fillWidth: true, fillHeight: true, alignment: .topLeading)\n    }\n\n    var settings: ViewContent {\n        Section(\"Where tasks live\") {\n            Picker(\"Keep tasks in\", options: Storage.allCases.map(\\.rawValue), selection: storage) { choice in\n                guard choice != self.storage else { return }\n                self.storage = choice\n                // Measure from what Reminders has now, not from an old copy.\n                self.mirror = [:]\n                await self.sync()\n            }\n            Text(storageNote).role(.secondary)\n        }\n        Section(\"Menu bar\") {\n            Picker(\"Show\", options: MenuBarShows.allCases.map(\\.rawValue), selection: menuBarShows) { self.menuBarShows = $0 }\n        }\n        Section(\"Reminders\") {\n            Toggle(\"Notify me when a task with a time is due\", isOn: reminders) { self.reminders = $0 }\n            Toggle(\"Play a sound with the reminder\", isOn: sound) { self.sound = $0 }\n            Picker(\"Nudge me if nothing's done by\", options: Self.nudgeOptions.map(\\.title), selection: Self.nudgeOptions.first { $0.hour == nudgeHour }?.title ?? \"Off\") { title in\n                self.nudgeHour = Self.nudgeOptions.first { $0.title == title }?.hour ?? -1\n            }\n            Picker(\"Morning summary of the day\", options: Self.summaryOptions.map(\\.title), selection: Self.summaryOptions.first { $0.hour == summaryHour }?.title ?? \"Off\") { title in\n                self.summaryHour = Self.summaryOptions.first { $0.title == title }?.hour ?? -1\n            }\n        }\n        Section(\"Done tasks\") {\n            Picker(\"Keep done tasks for\", options: Self.keepOptions.map(\\.title), selection: Self.keepOptions.first { $0.days == keepDoneDays }?.title ?? \"7 days\") { title in\n                self.keepDoneDays = Self.keepOptions.first { $0.title == title }?.days ?? 7\n            }\n            Button(\"Clear done tasks now\", symbol: \"trash\", role: .critical) {\n                self.clearDone()\n                await self.save()\n            }\n        }\n        Section(\"Typing dates\") {\n            Text(\"Write the when anywhere in the task: today, tonight, tomorrow, friday, next week, this weekend, in 3 days, in 2 hours, Nov 5, the 12th. Add a time with 2pm, 14:30, at 9, noon or this evening, and a repeat with every day, every weekday, every monday or monthly. A ! marks a task as important. Put words in \\\"quotes\\\" to keep them as they are. #groceries files it in your Groceries list. “Plan the offsite: book a venue; send invites” adds a task with two steps, and several lines add several tasks.\").role(.secondary)\n        }\n    }\n\n    var tools: [AgentTool] {\n        AgentTool(\"tasks\", description: \"The user's open to-do tasks, one per line, with due date and time, repeat and importance\") { _ in\n            self.now = Date()\n            return self.open.isEmpty ? \"No open tasks\" : self.open.map(self.describe).joined(separator: \"\\n\")\n        }\n        AgentTool(\"add\", description: \"Adds a to-do task. Dates, times and repeats in the text are understood, like 'Call mum tomorrow 6pm' or 'Pay rent every month'\", parameters: [ToolParameter(\"task\", description: \"The task as the user would type it\")]) { arguments in\n            let result = self.add(arguments.string(\"task\") ?? \"\")\n            await self.save()\n            switch result {\n            case .message(let text): return text ?? \"Added\"\n            case .failure(let text): return text\n            default: return \"Added\"\n            }\n        }\n    }\n\n    var commands: [Command] {\n        Command(\"add\", title: \"Add a Todo\", subtitle: \"Dates and times in the text are understood\", symbol: \"plus.circle\", keywords: [\"todo\", \"task\", \"remind\", \"add\", \"new\"]) { invocation in\n            let result = self.add(invocation[\"task\"] ?? \"\")\n            await self.save()\n            return result\n        }\n        .argument(\"task\", placeholder: \"Call mum tomorrow 6pm\")\n        Command(\"complete\", title: \"Mark a Todo Done\", symbol: \"checkmark.circle\", keywords: [\"todo\", \"task\", \"done\", \"complete\", \"tick\"]) { invocation in\n            self.now = Date()\n            guard let task = self.find(invocation[\"task\"] ?? \"\") else { return .failure(\"No open task called \\(invocation[\"task\"] ?? \"that\")\") }\n            let message = self.complete(task.id)\n            await self.save()\n            return .message(message)\n        }\n        .argument(\"task\", placeholder: \"Item\", options: open.prefix(20).map(\\.title))\n        Command(\"snooze\", title: \"Snooze a Todo\", subtitle: \"To a time you type, or an hour or a day later\", symbol: \"moon.zzz\", keywords: [\"todo\", \"task\", \"snooze\", \"later\", \"postpone\"]) { invocation in\n            self.now = Date()\n            guard let task = self.find(invocation[\"task\"] ?? \"\") else { return .failure(\"No open task called \\(invocation[\"task\"] ?? \"that\")\") }\n            let result = self.snooze(task.id, until: invocation[\"until\"])\n            await self.save()\n            return result\n        }\n        .argument(\"task\", placeholder: \"Item\", options: open.prefix(20).map(\\.title))\n        .argument(\"until\", placeholder: \"tomorrow 9am\", required: false)\n        Command(\"today\", title: \"Today's Todos\", symbol: \"checklist\", keywords: [\"todo\", \"task\", \"today\", \"list\"])\n            .view { _ in\n                if self.dueToday.isEmpty {\n                    ContentUnavailableView(\"Nothing due today\", symbol: \"leaf\", message: \"Add one with Add a Todo.\")\n                } else {\n                    List(self.dueToday) { task in\n                        Row(task.title, subtitle: self.when(task), symbol: \"circle\", key: task.id)\n                            .action(\"Done\") {\n                                _ = self.complete(task.id)\n                                await self.save()\n                            }\n                    }\n                }\n            }\n    }\n\n    // MARK: - Panel parts\n\n    private var headline: String {\n        if !dueToday.isEmpty { return dueToday.count == 1 ? \"1 thing for today\" : \"\\(dueToday.count) things for today\" }\n        if !doneToday.isEmpty { return \"All done for today\" }\n        return open.isEmpty ? \"A clear list\" : \"Nothing due today\"\n    }\n\n    private var subtitle: String {\n        var parts = [Self.format(now, template: \"EEEEdMMMM\")]\n        if !overdue.isEmpty { parts.append(\"\\(overdue.count) still on your list\") }\n        if dueToday.isEmpty, let next = (tomorrow + later).first { parts.append(\"next: \\(when(next))\") }\n        return parts.joined(separator: \" · \")\n    }\n\n    @ViewBuilder private func group(_ title: String, _ all: [Item]) -> ViewContent {\n        let list = shown(all)\n        if !list.isEmpty {\n            Card(title, detail: \"\\(list.count)\", spacing: 0, padding: Space.s) {\n                ForEach(list) { task in row(task) }\n            }\n        }\n    }\n\n    @ViewBuilder private func row(_ task: Item) -> ViewContent {\n        if editing == task.id {\n            editor(task)\n        } else {\n            let finished = task.done != nil\n            HStack(alignment: .center, spacing: Space.s) {\n                Button(key: \"check-\\(task.id)\", action: {\n                    self.toggle(task.id)\n                    await self.save()\n                }) {\n                    Image(systemName: finished ? \"checkmark.circle.fill\" : \"circle\")\n                        .font(size: 17)\n                        .foreground(finished ? theme.textPaint : (task.important ? theme.textPaint : .role(.secondary)))\n                        .frame(width: 22, height: 22)\n                }\n                .hover(scale: 1.12, lift: false)\n                .accessibilityLabel(finished ? \"Mark \\(task.title) not done\" : \"Mark \\(task.title) done\")\n                Button(key: \"edit-\\(task.id)\", action: { self.edit(task.id) }) {\n                    VStack(alignment: .start, spacing: 1) {\n                        if finished {\n                            Text(task.title).role(.secondary).typography(.body)\n                        } else {\n                            Text(task.title).typography(.body)\n                        }\n                        if let detail = detail(task) {\n                            HStack(spacing: 3) {\n                                if task.repeats != nil { Image(systemName: \"repeat\").font(size: 9, weight: .semibold) }\n                                Text(detail).typography(.caption)\n                            }\n                            .foreground(.role(.secondary))\n                        }\n                    }\n                    .frame(fillWidth: true, alignment: .leading)\n                }\n                .accessibilityLabel(([task.title] + (detail(task).map { [$0] } ?? [])).joined(separator: \", \"))\n                if task.important && !finished {\n                    Image(systemName: \"star.fill\").font(size: 10).foreground(theme.textPaint).accessibilityLabel(\"Important\")\n                }\n            }\n            .padding(horizontal: Space.xs, vertical: 5)\n            .hover(scale: nil, highlight: .hoverWash, lift: false)\n            .opacity(finished ? 0.7 : 1)\n            if !finished, let step = task.nextStep {\n                nextStepRow(task, step)\n            }\n        }\n    }\n\n    /// The step to do next, under its task, ticked right there.\n    @ViewBuilder private func nextStepRow(_ task: Item, _ step: Step) -> ViewContent {\n        let steps = task.steps ?? []\n        HStack(alignment: .center, spacing: Space.s) {\n            Button(key: \"next-step-\\(task.id)\", action: {\n                self.toggleStep(task.id, step.id)\n                await self.save()\n            }) {\n                Image(systemName: \"circle\").font(size: 12).foreground(.role(.secondary)).frame(width: 22, height: 16)\n            }\n            .accessibilityLabel(\"Mark step \\(step.title) done\")\n            Text(\"Next: \\(step.title)\").typography(.caption)\n            Text(step.size.rawValue).role(.secondary).typography(.captionNumber)\n            Spacer()\n            Text(\"\\(steps.filter(\\.done).count) of \\(steps.count)\").role(.secondary).typography(.captionNumber)\n        }\n        .padding(EdgeInsets(top: 0, leading: 26, bottom: 4, trailing: Space.xs))\n    }\n\n    /// The task opened in place: rename (dates in the new text count),\n    /// quick reschedule chips, repeat, importance and delete.\n    @ViewBuilder private func editor(_ task: Item) -> ViewContent {\n        VStack(alignment: .start, spacing: Space.s) {\n            TextField(\"Item\", text: task.title, key: \"rename-\\(task.id)\") { text in\n                self.rename(task.id, to: text)\n                await self.save()\n            }\n            HStack(spacing: Space.xs) {\n                ForEach(Self.quickDates) { choice in\n                    Chip(choice.title, theme: theme, selected: isOn(choice, task), key: \"date-\\(choice.title)-\\(task.id)\") {\n                        self.reschedule(task.id, to: choice)\n                        await self.save()\n                    }\n                }\n            }\n            stepsEditor(task)\n            if syncing && lists.count > 1 {\n                Picker(\"List\", options: lists.map(\\.title), selection: listTitle(task.list) ?? lists.first { $0.isDefault }?.title, key: \"list-\\(task.id)\") { title in\n                    guard let list = self.lists.first(where: { $0.title == title }) else { return }\n                    self.update(task.id) { $0.list = list.id }\n                    await self.save()\n                }\n            }\n            HStack(alignment: .center, spacing: Space.xs) {\n                Picker(\"Repeat\", options: [\"Never\"] + Repeat.allCases.map(\\.rawValue), selection: task.repeats?.rawValue ?? \"Never\", key: \"repeat-\\(task.id)\") { choice in\n                    self.update(task.id) { $0.repeats = Repeat(rawValue: choice) }\n                    await self.save()\n                }\n                Spacer()\n                IconButton(task.important ? \"star.fill\" : \"star\", label: task.important ? \"Not important\" : \"Important\", theme: theme, prominence: task.important ? .tinted : .plain, key: \"star-\\(task.id)\") {\n                    self.update(task.id) { $0.important.toggle() }\n                    await self.save()\n                }\n                IconButton(\"trash\", label: \"Delete\", theme: .critical, key: \"delete-\\(task.id)\") {\n                    self.delete(task.id)\n                    await self.save()\n                }\n                ActionButton(\"Done\", theme: theme, prominent: true, key: \"close-\\(task.id)\") {\n                    self.editing = nil\n                    self.breaking = nil\n                }\n            }\n        }\n        .padding(Space.s)\n        .background(theme.tintPaint, in: .roundedRectangle(cornerRadius: Radius.medium))\n    }\n\n    // MARK: - Lists\n\n    private var open: [Item] { tasks.filter { $0.done == nil }.sorted(by: Self.order) }\n    private var done: [Item] { tasks.filter { $0.done != nil }.sorted { ($0.done ?? now) > ($1.done ?? now) } }\n\n    private var calendar: Calendar { Calendar.current }\n    private var startOfToday: Date { calendar.startOfDay(for: now) }\n    private func days(to date: Date) -> Int {\n        calendar.dateComponents([.day], from: startOfToday, to: calendar.startOfDay(for: date)).day ?? 0\n    }\n\n    private var overdue: [Item] { open.filter { $0.due.map { days(to: $0) < 0 } ?? false } }\n    private var today: [Item] { open.filter { $0.due.map { days(to: $0) == 0 } ?? false } }\n    private var tomorrow: [Item] { open.filter { $0.due.map { days(to: $0) == 1 } ?? false } }\n    private var later: [Item] { open.filter { $0.due.map { days(to: $0) > 1 } ?? false } }\n    private var someday: [Item] { open.filter { $0.due == nil } }\n    private var dueToday: [Item] { overdue + today }\n    private var doneToday: [Item] { done.filter { $0.done.map { days(to: $0) == 0 } ?? false } }\n    private var todayTotal: Int { dueToday.count + doneToday.count }\n\n    private func isOverdue(_ task: Item) -> Bool {\n        guard let due = task.due, task.done == nil else { return false }\n        return task.hasTime ? due < now : days(to: due) < 0\n    }\n\n    /// Dated first, by date; then important first; then oldest first.\n    private static func order(_ a: Item, _ b: Item) -> Bool {\n        switch (a.due, b.due) {\n        case let (x?, y?) where x != y: return x < y\n        case (.some, nil): return true\n        case (nil, .some): return false\n        default:\n            if a.important != b.important { return a.important }\n            return a.created < b.created\n        }\n    }\n\n    // MARK: - Changes\n\n    /// Adds what was typed. Several lines (a brain dump) add a task each;\n    /// \"Task: step; step\" adds a task with steps; #list files it in a\n    /// Reminders list.\n    @discardableResult\n    func add(_ text: String) -> CommandResult {\n        now = Date()\n        let lines = text.split(whereSeparator: \\.isNewline).map { $0.trimmingCharacters(in: .whitespaces) }.filter { !$0.isEmpty }\n        guard lines.count > 1 else { return addOne(text) }\n        let before = tasks\n        var added = 0\n        for line in lines {\n            if case .message = addOne(line) { added += 1 }\n        }\n        guard added > 0 else { return .failure(\"Say what to do as well as when\") }\n        let message = added == 1 ? \"Added 1 task\" : \"Added \\(added) tasks\"\n        notice = Notice(text: message, before: before, at: now)\n        return .message(message)\n    }\n\n    private func addOne(_ text: String) -> CommandResult {\n        let (words, list) = takeList(text)\n        let (head, stepTitles) = StepText.split(words)\n        let parsed = Phrase.parse(head, now: now, calendar: calendar)\n        guard !parsed.title.isEmpty else {\n            return .failure(text.trimmingCharacters(in: .whitespaces).isEmpty ? \"Type a task to add\" : \"Say what to do as well as when\")\n        }\n        let before = tasks\n        var task = Item(id: UUID().uuidString, title: parsed.title, due: parsed.due, hasTime: parsed.hasTime, repeats: parsed.repeats, important: parsed.important, created: now)\n        task.list = list ?? (syncing && !listFilter.isEmpty ? listFilter : nil)\n        if !stepTitles.isEmpty { task.steps = stepTitles.map { Step(title: $0) } }\n        tasks.append(task)\n        var message = \"Added “\\(task.title)”\" + (task.due == nil ? \"\" : \" for \\(when(task))\") + (task.repeats.map { \", repeats \\($0.rawValue.lowercased())\" } ?? \"\")\n        if !stepTitles.isEmpty { message += stepTitles.count == 1 ? \" with 1 step\" : \" with \\(stepTitles.count) steps\" }\n        if let name = listTitle(task.list), syncing, lists.count > 1 { message += \" in \\(name)\" }\n        notice = Notice(text: message, before: before, at: now)\n        return .message(message)\n    }\n\n    /// Takes a #list word naming a Reminders list (spaces and case don't\n    /// matter) out of the text.\n    private func takeList(_ text: String) -> (String, String?) {\n        guard syncing else { return (text, nil) }\n        var found: String?\n        let words = text.split(separator: \" \", omittingEmptySubsequences: false).filter { word in\n            guard found == nil, word.hasPrefix(\"#\"), word.count > 1 else { return true }\n            let key = Self.listKey(String(word.dropFirst()))\n            guard let list = lists.first(where: { Self.listKey($0.title) == key }) else { return true }\n            found = list.id\n            return false\n        }\n        return (words.joined(separator: \" \"), found)\n    }\n\n    static func listKey(_ name: String) -> String {\n        name.lowercased().filter { $0.isLetter || $0.isNumber }\n    }\n\n    private func listTitle(_ id: String?) -> String? {\n        lists.first { $0.id == id }?.title\n    }\n\n    /// The tasks in the list the panel shows.\n    private func shown(_ items: [Item]) -> [Item] {\n        guard syncing, !listFilter.isEmpty, lists.contains(where: { $0.id == listFilter }) else { return items }\n        let defaultList = lists.first { $0.isDefault }?.id\n        return items.filter { ($0.list ?? defaultList) == listFilter }\n    }\n\n    private func toggle(_ id: String) {\n        now = Date()\n        guard let task = tasks.first(where: { $0.id == id }) else { return }\n        if task.done != nil {\n            remember(\"Back on the list: “\\(task.title)”\")\n            update(id) { $0.done = nil }\n        } else {\n            _ = complete(id)\n        }\n    }\n\n    /// Ticks a task off. A repeating one leaves a done copy behind and moves\n    /// on to its next day.\n    func complete(_ id: String) -> String {\n        guard let index = tasks.firstIndex(where: { $0.id == id }) else { return \"That task is gone\" }\n        let before = tasks\n        var task = tasks[index]\n        var message = \"Done: “\\(task.title)”\"\n        if let repeats = task.repeats {\n            var record = task\n            record.id = UUID().uuidString\n            // A reminder of its own: the done one, while the task moves on.\n            record.remote = nil\n            record.external = nil\n            record.repeats = nil\n            record.done = now\n            tasks.append(record)\n            task.due = Phrase.next(after: task.due ?? now, repeats: repeats, hasTime: task.hasTime, now: now, calendar: calendar)\n            task.reminded = false\n            tasks[index] = task\n            message += \". Next: \\(when(task))\"\n        } else {\n            tasks[index].done = now\n        }\n        if editing == id { editing = nil }\n        notice = Notice(text: message, before: before, at: now)\n        return message\n    }\n\n    func snooze(_ id: String, until text: String?) -> CommandResult {\n        guard let task = tasks.first(where: { $0.id == id }) else { return .failure(\"That task is gone\") }\n        let before = tasks\n        let words = text?.trimmingCharacters(in: .whitespaces) ?? \"\"\n        if words.isEmpty {\n            if task.hasTime {\n                let from = max(task.due ?? now, now)\n                update(id) { $0.due = from.addingTimeInterval(3600); $0.reminded = false }\n            } else {\n                update(id) { $0.due = self.calendar.date(byAdding: .day, value: 1, to: self.startOfToday) }\n            }\n        } else {\n            let parsed = Phrase.parse(\"x \" + words, now: now, calendar: calendar)\n            guard let due = parsed.due else { return .failure(\"Stackbar didn't find a date or time in “\\(words)”\") }\n            update(id) { $0.due = due; $0.hasTime = parsed.hasTime; $0.reminded = false }\n        }\n        guard let moved = tasks.first(where: { $0.id == id }) else { return .failure(\"That task is gone\") }\n        let message = \"Snoozed “\\(moved.title)” to \\(when(moved))\"\n        notice = Notice(text: message, before: before, at: now)\n        return .message(message)\n    }\n\n    private func edit(_ id: String) {\n        editing = editing == id ? nil : id\n    }\n\n    /// New text for a task. A date in it reschedules; text without one\n    /// keeps the date the task had.\n    private func rename(_ id: String, to text: String) {\n        now = Date()\n        let parsed = Phrase.parse(text, now: now, calendar: calendar)\n        guard !parsed.title.isEmpty else { return }\n        update(id) { task in\n            task.title = parsed.title\n            if parsed.due != nil {\n                task.due = parsed.due\n                task.hasTime = parsed.hasTime\n                task.reminded = false\n            }\n            if let repeats = parsed.repeats { task.repeats = repeats }\n            if parsed.important { task.important = true }\n        }\n        editing = nil\n    }\n\n    private func reschedule(_ id: String, to choice: QuickDate) {\n        now = Date()\n        let before = tasks\n        update(id) { task in\n            guard let offset = choice.days(self.now, self.calendar) else {\n                task.due = nil\n                task.hasTime = false\n                task.repeats = nil\n                return\n            }\n            let day = self.calendar.date(byAdding: .day, value: offset, to: self.startOfToday) ?? self.startOfToday\n            if task.hasTime, let due = task.due {\n                let parts = self.calendar.dateComponents([.hour, .minute], from: due)\n                task.due = self.calendar.date(bySettingHour: parts.hour ?? 9, minute: parts.minute ?? 0, second: 0, of: day)\n            } else {\n                task.due = day\n            }\n            task.reminded = false\n        }\n        if let task = tasks.first(where: { $0.id == id }) {\n            notice = Notice(text: task.due == nil ? \"“\\(task.title)” moved to Someday\" : \"“\\(task.title)” moved to \\(when(task))\", before: before, at: now)\n        }\n    }\n\n    private func delete(_ id: String) {\n        guard let task = tasks.first(where: { $0.id == id }) else { return }\n        remember(\"Deleted “\\(task.title)”\")\n        tasks.removeAll { $0.id == id }\n        editing = nil\n    }\n\n    private func clearDone() {\n        let count = done.count\n        guard count > 0 else { return }\n        remember(count == 1 ? \"Cleared 1 done task\" : \"Cleared \\(count) done tasks\")\n        tasks.removeAll { $0.done != nil }\n    }\n\n    private func undo() {\n        guard let before = notice?.before else { return }\n        tasks = before\n        notice = nil\n        editing = nil\n    }\n\n    private func remember(_ text: String) {\n        notice = Notice(text: text, before: tasks, at: now)\n    }\n\n    private func update(_ id: String, _ change: (inout Item) -> Void) {\n        guard let index = tasks.firstIndex(where: { $0.id == id }) else { return }\n        var task = tasks[index]\n        change(&task)\n        tasks[index] = task\n    }\n\n    /// An open task by id, exact title, or a title that starts with or\n    /// contains the words.\n    private func find(_ words: String) -> Item? {\n        let key = words.trimmingCharacters(in: .whitespaces).lowercased()\n        guard !key.isEmpty else { return nil }\n        let open = open\n        return open.first { $0.id.lowercased() == key }\n            ?? open.first { $0.title.lowercased() == key }\n            ?? open.first { $0.title.lowercased().hasPrefix(key) }\n            ?? open.first { $0.title.lowercased().contains(key) }\n    }\n\n    // MARK: - Reminders and upkeep\n\n    /// A notification for each timed task that has come due. One the Mac\n    /// slept through by more than half a day is let go quietly.\n    private func remind() {\n        var changed = tasks\n        for index in changed.indices {\n            let task = changed[index]\n            guard task.done == nil, task.hasTime, !task.reminded, let due = task.due, due <= now else { continue }\n            changed[index].reminded = true\n            // Reminders alerts on every device itself, with Done and Snooze.\n            guard reminders, !(syncing && task.remote != nil), now.timeIntervalSince(due) < 12 * 3600 else { continue }\n            Stackbar.notify(task.title, body: task.repeats.map { \"Due now · repeats \\($0.rawValue.lowercased())\" } ?? \"Due now\")\n            if sound { Stackbar.play(.glass) }\n        }\n        if changed != tasks { tasks = changed }\n    }\n\n    /// The morning summary, once a day at the hour chosen in Settings.\n    private func summarize() {\n        guard summaryHour >= 0, calendar.component(.hour, from: now) >= summaryHour else { return }\n        let key = Self.format(now, fixed: \"yyyy-MM-dd\")\n        guard lastSummary != key else { return }\n        lastSummary = key\n        let list = dueToday\n        guard !list.isEmpty else { return }\n        let names = list.prefix(3).map(\\.title).joined(separator: \", \") + (list.count > 3 ? \" and \\(list.count - 3) more\" : \"\")\n        Stackbar.notify(list.count == 1 ? \"1 thing for today\" : \"\\(list.count) things for today\", body: names)\n    }\n\n    /// Forgets done tasks older than the setting.\n    private func tidy() {\n        guard let cutoff = calendar.date(byAdding: .day, value: -keepDoneDays, to: startOfToday) else { return }\n        let old = tasks.filter { ($0.done ?? .distantFuture) < cutoff }.map(\\.id)\n        guard !old.isEmpty else { return }\n        // Forgotten here only: Reminders keeps its completed reminders.\n        tasks.removeAll { old.contains($0.id) }\n        for id in old { mirror[id] = nil }\n    }\n\n    // MARK: - Words\n\n    /// When a task is due, the way a person says it: \"Today\", \"Tomorrow\n    /// 6:00 PM\", \"Friday\", \"12 Nov\".\n    func when(_ task: Item) -> String {\n        guard let due = task.due else { return \"Someday\" }\n        let offset = days(to: due)\n        let day: String\n        switch offset {\n        case 0: day = \"Today\"\n        case 1: day = \"Tomorrow\"\n        case -1: day = \"Yesterday\"\n        case 2...6: day = Self.format(due, template: \"EEEE\")\n        default: day = Self.format(due, template: calendar.component(.year, from: due) == calendar.component(.year, from: now) ? \"dMMM\" : \"dMMMy\")\n        }\n        return task.hasTime ? \"\\(day) \\(Self.time(due))\" : day\n    }\n\n    /// The grey line under a task's title.\n    private func detail(_ task: Item) -> String? {\n        var parts: [String] = []\n        if let done = task.done {\n            parts.append(\"Done \" + (days(to: done) == 0 ? Self.time(done) : Self.format(done, template: \"dMMM\")))\n        } else if let due = task.due {\n            // The section already says the day; a row says the rest.\n            let offset = days(to: due)\n            if offset < 0 || offset > 1 { parts.append(when(task)) } else if task.hasTime { parts.append(Self.time(due)) }\n        }\n        if task.done == nil, let repeats = task.repeats { parts.append(repeats.rawValue) }\n        if syncing, lists.count > 1, listFilter.isEmpty, let name = listTitle(task.list) { parts.append(name) }\n        return parts.isEmpty ? nil : parts.joined(separator: \" · \")\n    }\n\n    private func describe(_ task: Item) -> String {\n        var line = \"- \\(task.title)\"\n        var facts: [String] = []\n        if let due = task.due { facts.append(\"due \" + Self.format(due, fixed: task.hasTime ? \"yyyy-MM-dd HH:mm\" : \"yyyy-MM-dd\")) }\n        if let repeats = task.repeats { facts.append(\"repeats \" + repeats.rawValue.lowercased()) }\n        if task.important { facts.append(\"important\") }\n        if isOverdue(task) { facts.append(\"overdue\") }\n        if let steps = task.steps, !steps.isEmpty { facts.append(\"\\(steps.filter(\\.done).count) of \\(steps.count) steps done\") }\n        if let list = lists.first(where: { $0.id == task.list }), !list.isDefault { facts.append(\"list \\(list.title)\") }\n        if !facts.isEmpty { line += \" (\" + facts.joined(separator: \", \") + \")\" }\n        return line\n    }\n\n    static func time(_ date: Date) -> String { format(date, template: \"jmm\") }\n\n    static func format(_ date: Date, template: String) -> String {\n        let formatter = DateFormatter()\n        formatter.locale = .current\n        formatter.setLocalizedDateFormatFromTemplate(template)\n        return formatter.string(from: date)\n    }\n\n    static func format(_ date: Date, fixed: String) -> String {\n        let formatter = DateFormatter()\n        formatter.locale = Locale(identifier: \"en_US_POSIX\")\n        formatter.dateFormat = fixed\n        return formatter.string(from: date)\n    }\n\n    static func clip(_ text: String, _ length: Int) -> String {\n        text.count > length ? String(text.prefix(length - 1)) + \"…\" : text\n    }\n\n    // MARK: - Choices\n\n    struct QuickDate: Identifiable {\n        var title: String\n        /// Days from today, or nil for no date.\n        var days: (Date, Calendar) -> Int?\n        var id: String { title }\n    }\n\n    static let quickDates: [QuickDate] = [\n        QuickDate(title: \"Today\") { _, _ in 0 },\n        QuickDate(title: \"Tomorrow\") { _, _ in 1 },\n        QuickDate(title: \"Weekend\") { now, calendar in\n            let weekday = calendar.component(.weekday, from: now)\n            return weekday == 7 || weekday == 1 ? 0 : 7 - weekday\n        },\n        QuickDate(title: \"Next week\") { now, calendar in\n            let weekday = calendar.component(.weekday, from: now)\n            return (9 - weekday - 1) % 7 + 1\n        },\n        QuickDate(title: \"Someday\") { _, _ in nil },\n    ]\n\n    private func isOn(_ choice: QuickDate, _ task: Item) -> Bool {\n        guard let due = task.due else { return choice.title == \"Someday\" }\n        guard let offset = choice.days(now, calendar) else { return false }\n        return days(to: due) == offset\n    }\n\n    static let summaryOptions: [(title: String, hour: Int)] = [(\"Off\", -1), (\"7 AM\", 7), (\"8 AM\", 8), (\"9 AM\", 9), (\"10 AM\", 10)]\n    static let keepOptions: [(title: String, days: Int)] = [(\"1 day\", 1), (\"7 days\", 7), (\"30 days\", 30)]\n    static let nudgeOptions: [(title: String, hour: Int)] = [(\"Off\", -1), (\"11 AM\", 11), (\"1 PM\", 13), (\"3 PM\", 15), (\"5 PM\", 17)]\n}\n\n// MARK: - Model\n\nstruct Item: Codable, Equatable, Identifiable {\n    var id: String\n    var title: String\n    var due: Date?\n    /// Whether `due` has a time of day, or only a day.\n    var hasTime: Bool\n    var repeats: Repeat?\n    var important: Bool\n    var created: Date\n    var done: Date?\n    /// The reminder for the current `due` has gone out.\n    var reminded = false\n    /// The Reminders list, or nil for the default one (or no Reminders).\n    var list: String?\n    /// The reminder this task is, once it has been saved to Reminders.\n    var remote: String?\n    var external: String?\n    /// The reminder's own notes, without the steps.\n    var notes: String?\n    var steps: [Step]?\n\n    /// The step to do next: small ones first, then medium, then big, in\n    /// the order they were added.\n    var nextStep: Step? {\n        let open = (steps ?? []).filter { !$0.done }\n        return open.first { $0.size == .small } ?? open.first { $0.size == .medium } ?? open.first\n    }\n\n    init(id: String, title: String, due: Date?, hasTime: Bool, repeats: Repeat?, important: Bool, created: Date) {\n        self.id = id\n        self.title = title\n        self.due = due\n        self.hasTime = hasTime\n        self.repeats = repeats\n        self.important = important\n        self.created = created\n    }\n}\n\nenum Repeat: String, Codable, CaseIterable {\n    case daily = \"Daily\", weekdays = \"Weekdays\", weekly = \"Weekly\", monthly = \"Monthly\", yearly = \"Yearly\"\n}\n\n/// One piece of a task broken down. Small, medium or big, like Little\n/// Things' sizes, so the next thing to do can be a small one.\nstruct Step: Codable, Equatable, Identifiable {\n    enum Size: String, Codable, CaseIterable {\n        case small = \"S\", medium = \"M\", big = \"B\"\n\n        var next: Size {\n            switch self {\n            case .small: return .medium\n            case .medium: return .big\n            case .big: return .small\n            }\n        }\n    }\n\n    var id: String\n    var title: String\n    var size: Size\n    var done: Bool\n\n    init(id: String = UUID().uuidString, title: String, size: Size = .small, done: Bool = false) {\n        self.id = id\n        self.title = title\n        self.size = size\n        self.done = done\n    }\n}\n\n// MARK: - Understanding dates in what the user types\n\n/// Turns \"Call the dentist tomorrow at 2pm\" into a title and a due date.\n/// Each phrase it understands is taken out of the title; anything in double\n/// quotes is left alone.\nenum Phrase {\n    struct Parsed: Equatable {\n        var title: String\n        var due: Date?\n        var hasTime = false\n        var repeats: Repeat?\n        var important = false\n    }\n\n    static let weekdayNames = [\"sunday\", \"monday\", \"tuesday\", \"wednesday\", \"thursday\", \"friday\", \"saturday\"]\n    static let fullWeekday = \"(sunday|monday|tuesday|wednesday|thursday|friday|saturday)\"\n    static let shortWeekday = \"(sun|mon|tues?|wed|thu(?:rs?)?|fri|sat)\"\n    static let month = \"(jan(?:uary)?|feb(?:ruary)?|mar(?:ch)?|apr(?:il)?|may|june?|july?|aug(?:ust)?|sep(?:t(?:ember)?)?|oct(?:ober)?|nov(?:ember)?|dec(?:ember)?)\"\n    static let months = [\"jan\", \"feb\", \"mar\", \"apr\", \"may\", \"jun\", \"jul\", \"aug\", \"sep\", \"oct\", \"nov\", \"dec\"]\n    static let lead = #\"(?:\\b(?:on|by|due|for)\\s+)?\"#\n    static let numbers = [\"a\": 1, \"an\": 1, \"one\": 1, \"two\": 2, \"three\": 3, \"four\": 4, \"five\": 5, \"six\": 6, \"seven\": 7, \"eight\": 8, \"nine\": 9, \"ten\": 10, \"fifteen\": 15, \"twenty\": 20, \"thirty\": 30, \"forty-five\": 45]\n\n    static func parse(_ input: String, now: Date, calendar: Calendar) -> Parsed {\n        var result = Parsed(title: \"\")\n        // Quoted words are kept as written: swap them out while parsing.\n        var kept: [String] = []\n        var text = \"\"\n        let pieces = input.components(separatedBy: \"\\\"\")\n        for (index, piece) in pieces.enumerated() {\n            if index % 2 == 1 && index < pieces.count - 1 {\n                text += \" \\u{E000}\" + String(repeating: \"q\", count: kept.count + 1) + \"\\u{E001} \"\n                kept.append(piece)\n            } else {\n                text += piece\n            }\n        }\n        text = \" \" + text + \" \"\n\n        /// Takes the first match of `pattern` out of the text and returns\n        /// its groups (\"\" for a group that did not take part).\n        func take(_ pattern: String) -> [String]? {\n            guard let regex = try? NSRegularExpression(pattern: pattern, options: [.caseInsensitive]) else { return nil }\n            let string = text as NSString\n            guard let match = regex.firstMatch(in: text, range: NSRange(location: 0, length: string.length)) else { return nil }\n            let groups = (0..<match.numberOfRanges).map { index -> String in\n                let range = match.range(at: index)\n                return range.location == NSNotFound ? \"\" : string.substring(with: range).lowercased()\n            }\n            text = string.replacingCharacters(in: match.range, with: \" \")\n            return groups\n        }\n\n        let today = calendar.startOfDay(for: now)\n        func day(_ offset: Int) -> Date { calendar.date(byAdding: .day, value: offset, to: today) ?? today }\n        func weekdayIndex(_ name: String) -> Int {\n            (weekdayNames.firstIndex { $0.hasPrefix(String(name.prefix(3))) } ?? 0) + 1\n        }\n        /// The next `weekday` (1 is Sunday) after today, or from today on.\n        func upcoming(_ weekday: Int, includingToday: Bool) -> Date {\n            let current = calendar.component(.weekday, from: today)\n            var offset = (weekday - current + 7) % 7\n            if offset == 0 && !includingToday { offset = 7 }\n            return day(offset)\n        }\n        func count(_ word: String) -> Int? { Int(word) ?? numbers[word] }\n\n        var date: Date?\n        var exact: Date?\n        var hour: Int?\n        var minute = 0\n\n        if take(#\"(?:^|\\s)!{1,3}(?=\\s|$)\"#) != nil { result.important = true }\n\n        // Repeats.\n        if let groups = take(#\"\\bevery\\s+(day|night|morning|evening|weekday|week|month|year|\\#(fullWeekday)|\\#(shortWeekday))\\b\"#) {\n            switch groups[1] {\n            case \"day\": result.repeats = .daily\n            case \"night\": result.repeats = .daily; hour = 21\n            case \"morning\": result.repeats = .daily; hour = 9\n            case \"evening\": result.repeats = .daily; hour = 18\n            case \"weekday\": result.repeats = .weekdays\n            case \"week\": result.repeats = .weekly\n            case \"month\": result.repeats = .monthly\n            case \"year\": result.repeats = .yearly\n            default:\n                result.repeats = .weekly\n                date = upcoming(weekdayIndex(groups[1]), includingToday: true)\n            }\n        } else if let groups = take(#\"\\b(daily|weekly|monthly|yearly|annually)\\b\"#) {\n            result.repeats = [\"daily\": .daily, \"weekly\": .weekly, \"monthly\": .monthly][groups[1]] ?? .yearly\n        }\n\n        // In a while.\n        if take(#\"\\bin\\s+half\\s+an?\\s+hour\\b\"#) != nil {\n            exact = now.addingTimeInterval(1800)\n        } else if let groups = take(#\"\\bin\\s+(\\d+|an?|one|two|three|four|five|six|seven|eight|nine|ten|fifteen|twenty|thirty|forty-five)\\s*(minutes?|mins?|hours?|hrs?|h|days?|weeks?|months?)\\b\"#),\n                  let amount = count(groups[1]) {\n            switch groups[2].prefix(1) {\n            case \"m\" where groups[2].hasPrefix(\"mi\"): exact = now.addingTimeInterval(Double(amount) * 60)\n            case \"h\": exact = now.addingTimeInterval(Double(amount) * 3600)\n            case \"d\": date = day(amount)\n            case \"w\": date = day(amount * 7)\n            default: date = calendar.date(byAdding: .month, value: amount, to: today)\n            }\n        }\n\n        // Calendar dates.\n        if date == nil, let groups = take(#\"\\b(\\d{4})-(\\d{1,2})-(\\d{1,2})\\b\"#) {\n            date = calendar.date(from: DateComponents(year: Int(groups[1]), month: Int(groups[2]), day: Int(groups[3])))\n        }\n        func monthDay(_ monthName: String, _ dayText: String, _ yearText: String) -> Date? {\n            guard let monthNumber = months.firstIndex(of: String(monthName.prefix(3))).map({ $0 + 1 }), let dayNumber = Int(dayText), (1...31).contains(dayNumber) else { return nil }\n            if let year = Int(yearText) { return calendar.date(from: DateComponents(year: year, month: monthNumber, day: dayNumber)) }\n            let year = calendar.component(.year, from: today)\n            guard let candidate = calendar.date(from: DateComponents(year: year, month: monthNumber, day: dayNumber)) else { return nil }\n            return candidate < today ? calendar.date(byAdding: .year, value: 1, to: candidate) : candidate\n        }\n        if date == nil, let groups = take(lead + #\"\\b\"# + month + #\"\\.?\\s+(\\d{1,2})(?:st|nd|rd|th)?\\b(?:,?\\s+(\\d{4})\\b)?\"#) {\n            date = monthDay(groups[1], groups[2], groups[3])\n        }\n        if date == nil, let groups = take(lead + #\"\\b(?:the\\s+)?(\\d{1,2})(?:st|nd|rd|th)?\\s+(?:of\\s+)?\"# + month + #\"\\b(?:,?\\s+(\\d{4})\\b)?\"#) {\n            date = monthDay(groups[2], groups[1], groups[3])\n        }\n        if date == nil, let groups = take(lead + #\"\\bthe\\s+(\\d{1,2})(?:st|nd|rd|th)\\b\"#), let dayNumber = Int(groups[1]), (1...31).contains(dayNumber) {\n            var parts = calendar.dateComponents([.year, .month], from: today)\n            parts.day = dayNumber\n            if let candidate = calendar.date(from: parts) {\n                date = candidate < today ? calendar.date(byAdding: .month, value: 1, to: candidate) : candidate\n            }\n        }\n\n        // Day words.\n        if date == nil {\n            if take(lead + #\"\\b(?:the\\s+)?day\\s+after\\s+tomorrow\\b\"#) != nil {\n                date = day(2)\n            } else if take(lead + #\"\\b(?:tomorrow|tmrw|tmr|tmw)\\b\"#) != nil {\n                date = day(1)\n            } else if take(lead + #\"\\btoday\\b\"#) != nil {\n                date = today\n            } else if take(#\"\\btonight\\b\"#) != nil {\n                date = today\n                hour = hour ?? 20\n            } else if let groups = take(lead + #\"\\b(this\\s+|next\\s+)?weekend\\b\"#) {\n                let saturday = upcoming(7, includingToday: true)\n                let weekday = calendar.component(.weekday, from: today)\n                date = groups[1].hasPrefix(\"next\") && weekday != 1 ? calendar.date(byAdding: .day, value: 7, to: saturday) : (weekday == 1 ? today : saturday)\n            } else if take(lead + #\"\\bnext\\s+week\\b\"#) != nil {\n                date = upcoming(2, includingToday: false)\n            } else if take(lead + #\"\\bnext\\s+month\\b\"#) != nil {\n                var parts = calendar.dateComponents([.year, .month], from: today)\n                parts.day = 1\n                date = calendar.date(from: parts).flatMap { calendar.date(byAdding: .month, value: 1, to: $0) }\n            } else if let groups = take(lead + #\"\\b(this\\s+|next\\s+)?\"# + fullWeekday + #\"\\b\"#) ?? take(lead + #\"\\b(this\\s+|next\\s+|on\\s+)\"# + shortWeekday + #\"\\b\"#) {\n                let weekday = weekdayIndex(groups[2])\n                if groups[1].hasPrefix(\"next\") {\n                    // In the week after this one (weeks start on Monday).\n                    let monday = upcoming(2, includingToday: false)\n                    let offset = (weekday - 2 + 7) % 7\n                    date = calendar.date(byAdding: .day, value: offset, to: monday)\n                } else {\n                    date = upcoming(weekday, includingToday: groups[1].hasPrefix(\"this\"))\n                }\n            }\n        }\n\n        // Times of day.\n        if exact == nil {\n            if let groups = take(#\"(?:(?:\\bat|\\bby|@)\\s*)?\\b(\\d{1,2})(?:[:.](\\d{2}))?\\s*([ap])\\.?m\\.?(?=\\W|$)\"#), let value = Int(groups[1]), (1...12).contains(value) {\n                hour = value % 12 + (groups[3] == \"p\" ? 12 : 0)\n                minute = Int(groups[2]) ?? 0\n            } else if let groups = take(#\"(?:(?:\\bat|\\bby|@)\\s*)?\\b([01]?\\d|2[0-3]):([0-5]\\d)\\b\"#) {\n                // \"3:30\" is half past three in the afternoon; \"03:30\" is not.\n                let value = Int(groups[1]) ?? 0\n                hour = groups[1].count == 1 && (1...6).contains(value) ? value + 12 : value\n                minute = Int(groups[2]) ?? 0\n            } else if let groups = take(#\"(?:\\bat|@)\\s*(\\d{1,2})\\b(?![:.]?\\d|\\s*(?:%|st|nd|rd|th|minutes?|mins?|hours?|days?))\"#), let value = Int(groups[1]), (0...23).contains(value) {\n                hour = (1...6).contains(value) ? value + 12 : value\n            } else if let groups = take(#\"(?:\\bat\\s+)?\\b(noon|midday)\\b\"#), !groups.isEmpty {\n                hour = 12\n            } else if let groups = take(#\"\\b(?:this\\s+|in\\s+the\\s+)?(morning|afternoon|evening)\\b\"#) {\n                hour = [\"morning\": 9, \"afternoon\": 14][groups[1]] ?? 18\n            }\n        }\n\n        if let exact {\n            result.due = exact\n            result.hasTime = true\n        } else if let hour {\n            var base = date ?? today\n            var due = calendar.date(bySettingHour: hour, minute: minute, second: 0, of: base) ?? base\n            if date == nil && due <= now {\n                // A time already gone today means the next one.\n                base = result.repeats == .weekdays ? nextWeekday(after: today, calendar: calendar) : day(1)\n                due = calendar.date(bySettingHour: hour, minute: minute, second: 0, of: base) ?? base\n            }\n            result.due = due\n            result.hasTime = true\n        } else if let date {\n            result.due = date\n        } else if let repeats = result.repeats {\n            result.due = repeats == .weekdays && calendar.isDateInWeekend(today) ? nextWeekday(after: today, calendar: calendar) : today\n        }\n        if result.repeats == .weekdays, let due = result.due, calendar.isDateInWeekend(due) {\n            result.due = nextWeekday(after: due, calendar: calendar)\n        }\n\n        // What is left is the title.\n        var title = text\n        for (index, piece) in kept.enumerated() {\n            title = title.replacingOccurrences(of: \"\\u{E000}\" + String(repeating: \"q\", count: index + 1) + \"\\u{E001}\", with: piece)\n        }\n        var words = title.split(whereSeparator: \\.isWhitespace).map(String.init)\n        let dangling: Set<String> = [\"at\", \"on\", \"by\", \"due\", \"in\", \"every\", \"this\", \"next\", \"the\", \"-\", \"–\", \"—\", \",\", \"@\"]\n        while let last = words.last, dangling.contains(last.lowercased()) { words.removeLast() }\n        while let first = words.first, [\"-\", \"–\", \"—\", \",\", \":\"].contains(first) { words.removeFirst() }\n        result.title = words.joined(separator: \" \").trimmingCharacters(in: CharacterSet(charactersIn: \" ,;:-–—\"))\n        return result\n    }\n\n    static func nextWeekday(after date: Date, calendar: Calendar) -> Date {\n        var day = calendar.date(byAdding: .day, value: 1, to: date) ?? date\n        while calendar.isDateInWeekend(day) { day = calendar.date(byAdding: .day, value: 1, to: day) ?? day }\n        return day\n    }\n\n    /// The next time a repeating task is due once it is done: past both its\n    /// current date and now.\n    static func next(after due: Date, repeats: Repeat, hasTime: Bool, now: Date, calendar: Calendar) -> Date {\n        var date = due\n        let floor = hasTime ? now : calendar.startOfDay(for: now)\n        repeat {\n            switch repeats {\n            case .daily: date = calendar.date(byAdding: .day, value: 1, to: date) ?? date\n            case .weekdays: date = nextWeekday(after: date, calendar: calendar)\n            case .weekly: date = calendar.date(byAdding: .day, value: 7, to: date) ?? date\n            case .monthly: date = calendar.date(byAdding: .month, value: 1, to: date) ?? date\n            case .yearly: date = calendar.date(byAdding: .year, value: 1, to: date) ?? date\n            }\n        } while date <= floor\n        return date\n    }\n}\n\n// MARK: - Reminders\n\nextension Todo {\n    /// What Settings says about where tasks are.\n    var storageNote: String {\n        guard storage == Storage.reminders.rawValue else {\n            return \"Tasks stay on this Mac. Choose Reminders to have them on your iPhone, iPad and Watch too.\"\n        }\n        switch access {\n        case .granted?:\n            if let syncProblem { return \"Reminders didn't take the last change: \\(syncProblem)\" }\n            let names = lists.map(\\.title)\n            return \"Tasks are in Reminders, so they're on your iPhone, iPad and Watch too. Lists: \" + (names.isEmpty ? \"none yet\" : names.joined(separator: \", \")) + \". Type #listname in a task to file it.\"\n        case .denied?:\n            return \"Stackbar isn't allowed to use Reminders, so tasks stay on this Mac. Allow it in System Settings › Privacy & Security › Reminders.\"\n        case .unavailable?:\n            return \"Reminders isn't available here, so tasks stay on this Mac.\"\n        case .notDetermined?, nil:\n            return \"Stackbar asks macOS for Reminders access the first time it's needed.\"\n        }\n    }\n\n    /// Sends this Mac's changes to Reminders, then takes in what changed\n    /// there (on any device). Local changes always go first, so nothing\n    /// typed here is overwritten by an older copy.\n    func sync() async {\n        guard storage == Storage.reminders.rawValue else {\n            access = nil\n            return\n        }\n        access = (try? await Stackbar.reminderAccess(ask: true)) ?? .unavailable\n        guard access == .granted else { return }\n        do {\n            lists = try await Stackbar.reminderLists()\n            try await push()\n            try await pull()\n            syncProblem = nil\n        } catch {\n            syncProblem = \"\\(error)\"\n        }\n    }\n\n    /// After a change made here: send it to Reminders now.\n    func save() async {\n        guard syncing else { return }\n        do {\n            try await push()\n            syncProblem = nil\n        } catch {\n            syncProblem = \"\\(error)\"\n        }\n    }\n\n    /// Adds new tasks to Reminders, writes the fields that changed since the\n    /// last sync, and deletes what was deleted here.\n    private func push() async throws {\n        let alarms = reminders\n        var current = tasks\n        var known = mirror\n        for index in current.indices {\n            let task = current[index]\n            let wanted = task.reminder(alarms: alarms)\n            if task.remote == nil {\n                let saved = try await Stackbar.saveReminder(wanted)\n                current[index].attach(saved)\n            } else if let before = known[task.id] {\n                let fields = Item.changes(from: before.reminder(alarms: alarms), to: wanted)\n                guard !fields.isEmpty else { continue }\n                do {\n                    let saved = try await Stackbar.saveReminder(wanted, fields: fields)\n                    current[index].attach(saved)\n                } catch where \"\\(error)\".contains(\"deleted\") {\n                    // Gone from Reminders (an Undo after a delete): add it again.\n                    var fresh = wanted\n                    fresh.id = \"\"\n                    let saved = try await Stackbar.saveReminder(fresh)\n                    current[index].attach(saved)\n                }\n            } else {\n                // Never seen in Reminders by this copy: the pull decides.\n                continue\n            }\n            known[task.id] = current[index]\n        }\n        let ids = Set(current.map(\\.id))\n        for (id, before) in known where !ids.contains(id) {\n            if let remote = before.remote { try? await Stackbar.deleteReminder(id: remote) }\n            known[id] = nil\n        }\n        if current != tasks { tasks = current }\n        if known != mirror { mirror = known }\n    }\n\n    /// Takes in Reminders as it is: changes from other devices win (this\n    /// Mac's own went out just before), new reminders join, and deleted ones\n    /// leave.\n    private func pull() async throws {\n        let cutoff = calendar.date(byAdding: .day, value: -keepDoneDays, to: startOfToday) ?? startOfToday\n        let remote = try await Stackbar.reminders(completedSince: cutoff)\n        var byID: [String: Reminder] = [:]\n        for reminder in remote where byID[reminder.id] == nil { byID[reminder.id] = reminder }\n        var idByExternal: [String: String] = [:]\n        for reminder in remote { if let external = reminder.externalID, idByExternal[external] == nil { idByExternal[external] = reminder.id } }\n\n        var result: [Item] = []\n        for var task in tasks {\n            guard let id = task.remote else {\n                result.append(task)\n                continue\n            }\n            // The id can change after a full iCloud resync; the external id\n            // usually doesn't.\n            let match = byID.removeValue(forKey: id) ?? task.external.flatMap { idByExternal[$0] }.flatMap { byID.removeValue(forKey: $0) }\n            if let match {\n                task.adopt(match)\n                result.append(task)\n            } else if let done = task.done, done < cutoff {\n                result.append(task)\n            }\n            // Otherwise it was deleted in Reminders.\n        }\n        for reminder in remote where byID[reminder.id] != nil {\n            var task = Item(id: UUID().uuidString, title: reminder.title, due: nil, hasTime: false, repeats: nil, important: false, created: reminder.modified ?? now)\n            task.adopt(reminder)\n            // A timed reminder that's already due has already alerted.\n            if let due = task.due, due <= now { task.reminded = true }\n            result.append(task)\n        }\n        if result != tasks { tasks = result }\n        let known = Dictionary(result.filter { $0.remote != nil }.map { ($0.id, $0) }, uniquingKeysWith: { first, _ in first })\n        if known != mirror { mirror = known }\n    }\n\n    // MARK: Steps\n\n    /// The steps under a task in its editor: tick, size, remove, and a field\n    /// to add more.\n    @ViewBuilder func stepsEditor(_ task: Item) -> ViewContent {\n        let steps = task.steps ?? []\n        if steps.isEmpty && breaking != task.id {\n            ActionButton(\"Break into steps\", symbol: \"list.bullet.indent\", theme: theme, key: \"break-\\(task.id)\") { self.breaking = task.id }\n        } else {\n            VStack(alignment: .start, spacing: 2) {\n                ForEach(steps) { step in\n                    HStack(alignment: .center, spacing: Space.xs) {\n                        Button(key: \"step-\\(task.id)-\\(step.id)\", action: {\n                            self.toggleStep(task.id, step.id)\n                            await self.save()\n                        }) {\n                            Image(systemName: step.done ? \"checkmark.circle.fill\" : \"circle\").font(size: 13).foreground(step.done ? theme.textPaint : .role(.secondary))\n                        }\n                        .accessibilityLabel(step.done ? \"Mark step \\(step.title) not done\" : \"Mark step \\(step.title) done\")\n                        if step.done {\n                            Text(step.title).role(.secondary).typography(.body)\n                        } else {\n                            Text(step.title).typography(.body)\n                        }\n                        Spacer()\n                        Chip(step.size.rawValue, theme: theme, selected: step.size != .small, key: \"size-\\(task.id)-\\(step.id)\") {\n                            self.updateStep(task.id, step.id) { $0.size = $0.size.next }\n                            await self.save()\n                        }\n                        IconButton(\"xmark\", label: \"Remove step\", theme: theme, size: 22, key: \"remove-step-\\(task.id)-\\(step.id)\") {\n                            self.update(task.id) { item in\n                                item.steps?.removeAll { $0.id == step.id }\n                                if item.steps?.isEmpty == true { item.steps = nil }\n                            }\n                            await self.save()\n                        }\n                    }\n                }\n                TextField(\"Add a step, then Return\", text: \"\", key: \"add-step-\\(task.id)\") { text in\n                    self.addStep(task.id, text)\n                    await self.save()\n                }\n                Text(\"S, M and B are a step's size: tap to change it.\").role(.secondary).typography(.caption)\n            }\n        }\n    }\n\n    func addStep(_ id: String, _ text: String) {\n        let title = text.trimmingCharacters(in: .whitespacesAndNewlines)\n        guard !title.isEmpty else { return }\n        breaking = id\n        update(id) { task in\n            task.steps = (task.steps ?? []) + title.split(separator: \";\").map { Step(title: $0.trimmingCharacters(in: .whitespaces)) }.filter { !$0.title.isEmpty }\n        }\n    }\n\n    /// Ticks a step (or unticks it). Ticking the last one finishes the task.\n    func toggleStep(_ id: String, _ stepID: String) {\n        now = Date()\n        guard let task = tasks.first(where: { $0.id == id }), let step = task.steps?.first(where: { $0.id == stepID }) else { return }\n        let before = tasks\n        updateStep(id, stepID) { $0.done.toggle() }\n        guard !step.done, let steps = tasks.first(where: { $0.id == id })?.steps, steps.allSatisfy(\\.done), task.done == nil else {\n            notice = Notice(text: step.done ? \"Back on the list: “\\(step.title)”\" : \"Done: “\\(step.title)”\", before: before, at: now)\n            return\n        }\n        _ = complete(id)\n        notice?.before = before\n    }\n\n    private func updateStep(_ id: String, _ stepID: String, _ change: (inout Step) -> Void) {\n        update(id) { task in\n            guard let index = task.steps?.firstIndex(where: { $0.id == stepID }) else { return }\n            change(&task.steps![index])\n        }\n    }\n\n    // MARK: Nudge\n\n    /// One gentle nudge at the chosen hour, only on a day nothing has been\n    /// ticked off yet, naming the smallest thing to start with.\n    func nudge() {\n        guard nudgeHour >= 0, calendar.component(.hour, from: now) >= nudgeHour else { return }\n        let key = Self.format(now, fixed: \"yyyy-MM-dd\")\n        guard lastNudge != key else { return }\n        lastNudge = key\n        guard doneToday.isEmpty, let start = smallestStart else { return }\n        Stackbar.notify(\"One small thing?\", body: start)\n    }\n\n    /// The smallest next step among today's tasks, or the first task.\n    private var smallestStart: String? {\n        let candidates = dueToday.isEmpty ? open : dueToday\n        if let task = candidates.first(where: { $0.nextStep?.size == .small }), let step = task.nextStep {\n            return \"\\(step.title) (for “\\(task.title)”)\"\n        }\n        return candidates.first.map { task in task.nextStep.map { \"\\($0.title) (for “\\(task.title)”)\" } ?? task.title }\n    }\n}\n\n// MARK: - Tasks as reminders\n\nextension Item {\n    /// The task as a reminder: steps go at the end of the notes as a\n    /// checklist, so they're visible on the iPhone too.\n    func reminder(alarms: Bool) -> Reminder {\n        Reminder(\n            id: remote ?? \"\",\n            externalID: external,\n            list: list ?? \"\",\n            title: title,\n            notes: StepText.notes(notes, steps: steps ?? []),\n            due: due,\n            dueHasTime: due != nil && hasTime,\n            priority: important ? .high : .none,\n            completed: done,\n            recurrence: repeats?.recurrence,\n            alarm: alarms && hasTime ? due : nil\n        )\n    }\n\n    /// Fields that differ between two versions of a reminder.\n    static func changes(from old: Reminder, to new: Reminder) -> [Reminder.Field] {\n        Reminder.Field.allCases.filter { field in\n            switch field {\n            case .list: return old.list != new.list\n            case .title: return old.title != new.title\n            case .notes: return old.notes != new.notes\n            case .due: return old.due != new.due || old.dueHasTime != new.dueHasTime\n            case .priority: return old.priority != new.priority\n            case .completed: return old.completed != new.completed\n            case .recurrence: return old.recurrence != new.recurrence\n            case .alarm: return old.alarm != new.alarm\n            case .url: return false\n            }\n        }\n    }\n\n    /// Remembers which reminder this task is, after a save.\n    mutating func attach(_ saved: Reminder) {\n        remote = saved.id\n        external = saved.externalID\n        list = saved.list.isEmpty ? list : saved.list\n    }\n\n    /// Takes the reminder's values, as Reminders has them now.\n    mutating func adopt(_ reminder: Reminder) {\n        attach(reminder)\n        if due != reminder.due || hasTime != reminder.dueHasTime { reminded = false }\n        title = reminder.title\n        due = reminder.due\n        hasTime = reminder.due != nil && reminder.dueHasTime\n        if reminder.recurrence != repeats?.recurrence { repeats = reminder.recurrence.flatMap(Repeat.init) }\n        if (reminder.priority == .high) != important { important = reminder.priority == .high }\n        done = reminder.completed\n        let parts = StepText.parse(reminder.notes)\n        notes = parts.notes\n        let old = steps ?? []\n        let merged = parts.steps.map { line -> Step in\n            var step = old.first { $0.title == line.title } ?? Step(title: line.title)\n            step.done = line.done\n            return step\n        }\n        steps = merged.isEmpty ? nil : merged\n    }\n}\n\nextension Repeat {\n    var recurrence: ReminderRecurrence {\n        switch self {\n        case .daily: return ReminderRecurrence(.daily)\n        case .weekdays: return .weekdays\n        case .weekly: return ReminderRecurrence(.weekly)\n        case .monthly: return ReminderRecurrence(.monthly)\n        case .yearly: return ReminderRecurrence(.yearly)\n        }\n    }\n\n    /// The repeat a reminder's recurrence means, when Todo has one like it.\n    init?(_ recurrence: ReminderRecurrence) {\n        guard recurrence.interval == 1 else { return nil }\n        switch recurrence.frequency {\n        case .daily: self = .daily\n        case .weekly: self = recurrence.weekdays.sorted() == [2, 3, 4, 5, 6] ? .weekdays : .weekly\n        case .monthly: self = .monthly\n        case .yearly: self = .yearly\n        }\n    }\n}\n\n/// Steps in words: typed after a colon (\"Plan the offsite: book a venue;\n/// send invites\") and kept at the end of a reminder's notes as a checklist.\nenum StepText {\n    static let open = \"☐ \"\n    static let done = \"☑ \"\n\n    /// \"Task: a; b\" into the task and its steps. Needs a semicolon, so\n    /// \"Call: mum\" stays one task.\n    static func split(_ text: String) -> (String, [String]) {\n        guard let colon = text.range(of: \": \"), text[colon.upperBound...].contains(\";\") else { return (text, []) }\n        let steps = text[colon.upperBound...].split(separator: \";\").map { $0.trimmingCharacters(in: .whitespaces) }.filter { !$0.isEmpty }\n        return (String(text[..<colon.lowerBound]), steps)\n    }\n\n    static func notes(_ notes: String?, steps: [Step]) -> String? {\n        let text = notes?.trimmingCharacters(in: .whitespacesAndNewlines) ?? \"\"\n        let list = steps.map { ($0.done ? done : open) + $0.title }.joined(separator: \"\\n\")\n        let joined = [text, list].filter { !$0.isEmpty }.joined(separator: \"\\n\\n\")\n        return joined.isEmpty ? nil : joined\n    }\n\n    static func parse(_ notes: String?) -> (notes: String?, steps: [(title: String, done: Bool)]) {\n        var text: [String] = []\n        var steps: [(String, Bool)] = []\n        for line in (notes ?? \"\").components(separatedBy: \"\\n\") {\n            if line.hasPrefix(open) {\n                steps.append((String(line.dropFirst(open.count)), false))\n            } else if line.hasPrefix(done) {\n                steps.append((String(line.dropFirst(done.count)), true))\n            } else {\n                text.append(line)\n            }\n        }\n        let rest = text.joined(separator: \"\\n\").trimmingCharacters(in: .whitespacesAndNewlines)\n        return (rest.isEmpty ? nil : rest, steps)\n    }\n}\n",
  "sourceDigest" : "b48271d78a07bba9998d19febf8153cf2b64f08678e4de5b3a767d66f86d4714"
}