krz/orgstar

A native macOS editor for org-mode files. editor org-mode swift

Commit 6a95a830c0

6a95a830c04dbf6370533f2379be54cac2d4fbd0

parent: 3c3aea2b9d

Verified · cmc

cmc <hello@cleberg.net> · 2026-10-09 05:01 UTC

Capture: schedule a filled entry on a day

Capture.scheduling adds SCHEDULED to an entry captured from an agenda day,
unless it already has an active timestamp.

Layout: unified · split

Sources/OrgCore/Capture/CaptureSchedule.swift added +33
@@ -0,0 +1,33 @@
1import Foundation
2
3extension Capture {
4 /// A filled entry scheduled on `day` (as `Days` numbers it), for capturing from an agenda
5 /// day: `SCHEDULED: <2026-10-09 Fri>` goes on the planning line after the heading. Text that
6 /// is not an entry, or already has an active timestamp (`%t`, `%^t`), is left as it is.
7 /// `cursor` is a UTF-16 offset and moves with the text after it.
8 public static func scheduling(_ text: String, cursor: Int, on day: Int) -> (text: String, cursor: Int) {
9 let ns = text as NSString
10 guard text.range(of: "^\\*+ ", options: .regularExpression) != nil,
11 text.range(of: "<[0-9]{4}-[0-9]{2}-[0-9]{2}", options: .regularExpression) == nil else { return (text, cursor) }
12 let d = Days.date(day)
13 let name = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"][Days.weekday(day)]
14 let stamp = String(format: "SCHEDULED: <%04d-%02d-%02d %@>", d.year, d.month, d.day, name)
15 let headingEnd = ns.range(of: "\n").location
16 let insertion: (at: Int, text: String)
17 if headingEnd == NSNotFound {
18 insertion = (ns.length, "\n" + stamp)
19 } else {
20 let rest = ns.substring(from: headingEnd + 1)
21 let lineEnd = (rest as NSString).range(of: "\n").location
22 let next = lineEnd == NSNotFound ? rest : (rest as NSString).substring(to: lineEnd)
23 if next.range(of: "^\\s*(CLOSED|DEADLINE|SCHEDULED):", options: .regularExpression) != nil {
24 insertion = (headingEnd + 1 + (next as NSString).length, " " + stamp)
25 } else {
26 insertion = (headingEnd + 1, stamp + "\n")
27 }
28 }
29 let result = ns.replacingCharacters(in: NSRange(location: insertion.at, length: 0), with: insertion.text)
30 let moved = cursor > insertion.at || (cursor == insertion.at && headingEnd != NSNotFound && cursor > headingEnd)
31 return (result, moved ? cursor + (insertion.text as NSString).length : cursor)
32 }
33}
Tests/OrgCoreTests/CaptureScheduleTests.swift added +36
@@ -0,0 +1,36 @@
1import Testing
2@testable import OrgCore
3
4struct CaptureScheduleTests {
5 static let day = Days.absolute(year: 2026, month: 10, day: 9)
6
7 @Test func addsPlanningLineAfterHeading() {
8 let text = "* TODO \n body"
9 let result = Capture.scheduling(text, cursor: 7, on: Self.day)
10 #expect(result.text == "* TODO \nSCHEDULED: <2026-10-09 Fri>\n body")
11 #expect(result.cursor == 7)
12 }
13
14 @Test func headingAlone() {
15 let result = Capture.scheduling("* TODO ", cursor: 7, on: Self.day)
16 #expect(result.text == "* TODO \nSCHEDULED: <2026-10-09 Fri>")
17 #expect(result.cursor == 7)
18 }
19
20 @Test func cursorInBodyMoves() {
21 let text = "* TODO x\n"
22 let result = Capture.scheduling(text, cursor: 9, on: Self.day)
23 #expect(result.text == "* TODO x\nSCHEDULED: <2026-10-09 Fri>\n")
24 #expect(result.cursor == 9 + 28)
25 }
26
27 @Test func joinsExistingPlanningLine() {
28 let result = Capture.scheduling("* DONE x\nCLOSED: [2026-10-01 Thu]\n", cursor: 0, on: Self.day)
29 #expect(result.text == "* DONE x\nCLOSED: [2026-10-01 Thu] SCHEDULED: <2026-10-09 Fri>\n")
30 }
31
32 @Test func leavesDatedAndNonEntries() {
33 #expect(Capture.scheduling("* TODO x\n<2026-10-09 Fri>", cursor: 0, on: Self.day).text == "* TODO x\n<2026-10-09 Fri>")
34 #expect(Capture.scheduling("- [ ] item", cursor: 0, on: Self.day).text == "- [ ] item")
35 }
36}