krz/orgstar

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

Commit 778058aafc

778058aafc7ddacee4a613b1da87c60aa0c8e113

parent: c1e47f7751

Verified · cmc

cmc <hello@cleberg.net> · 2026-10-08 08:07 UTC

Clock resolution keys and import as Org

The resolution question reads J (clock out now and jump) and not c, as
org-clock-resolve does; j or J while clocking in shows the open clock
afterwards; Emacs import reads org-clock-idle-time and
org-clock-history-length.

Layout: unified · split

Sources/OrgApp/ClockModel.swift +1
@@ -286,6 +286,7 @@ public final class ClockModel {
286 clock, description: "Dangling clock started \(minutes) mins ago", lastValid: clock.startedAt, clockingIn: true, session, ask: ask, now: now 286 clock, description: "Dangling clock started \(minutes) mins ago", lastValid: clock.startedAt, clockingIn: true, session, ask: ask, now: now
287 ) 287 )
288 outcome.messages += result.messages 288 outcome.messages += result.messages
289 if let jump = result.jump { outcome.jump = jump }
289 } 290 }
290 } 291 }
291 let location: (path: String, offset: Int) 292 let location: (path: String, offset: Int)
Sources/OrgApp/EmacsImport.swift +6
@@ -512,6 +512,12 @@ public struct EmacsImport: Sendable {
512 let days: Int? = value.integer ?? ["day": 1, "week": 7, "fortnight": 14, "month": 30, "year": 365][value.symbol ?? ""] 512 let days: Int? = value.integer ?? ["day": 1, "week": 7, "fortnight": 14, "month": 30, "year": 365][value.symbol ?? ""]
513 guard let days else { return note("unknown span \(value.description)") } 513 guard let days else { return note("unknown span \(value.description)") }
514 add(name, .integer(days)) 514 add(name, .integer(days))
515 case "org-clock-idle-time":
516 // nil is never; a number is minutes.
517 if value.symbol == "nil" { add(name, .integer(0)) } else if let minutes = value.integer, minutes > 0 { add(name, .integer(minutes)) } else { note("not a number of minutes") }
518 case "org-clock-history-length":
519 guard let length = value.integer, length > 0 else { return note("not a positive number") }
520 add(name, .integer(length))
515 case "org-agenda-start-day": 521 case "org-agenda-start-day":
516 if value.symbol == "nil" { add(name, .string("+0d")) } else if let s = value.string, UserConfig.dayOffset(s) != nil { add(name, .string(s)) } else { note("only day offsets such as \"-3d\" are supported") } 522 if value.symbol == "nil" { add(name, .string("+0d")) } else if let s = value.string, UserConfig.dayOffset(s) != nil { add(name, .string(s)) } else { note("only day offsets such as \"-3d\" are supported") }
517 case "org-todo-keywords": 523 case "org-todo-keywords":
Sources/OrgCore/Commands/ClockResolution.swift +1 −1
@@ -6,7 +6,7 @@ import Foundation
6 6
7public enum ClockResolution { 7public enum ClockResolution {
8 /// The keys `org-clock-resolve` reads. 8 /// The keys `org-clock-resolve` reads.
9 public static let keys: [Character] = ["j", "k", "K", "t", "T", "g", "G", "S", "s", "c", "C", "i", "q"] 9 public static let keys: [Character] = ["j", "J", "k", "K", "t", "T", "g", "G", "S", "s", "C", "i", "q"]
10 10
11 /// The question's suffix, after the clock's description. 11 /// The question's suffix, after the clock's description.
12 public static let keyHint = " [jkKtTgGSscCiq]? " 12 public static let keyHint = " [jkKtTgGSscCiq]? "
Tests/OrgAppTests/ClockHistoryModelTests.swift +12
@@ -111,6 +111,18 @@ struct ClockHistoryModelTests {
111 #expect(clock.leftover == nil) 111 #expect(clock.leftover == nil)
112 } 112 }
113 113
114 @Test func jumpingFromADanglingClockShowsItAfterClockingIn() async throws {
115 let other = try notes.write("other.org", "* Old\n:LOGBOOK:\nCLOCK: [2026-09-09 Wed 09:00]\n:END:\n")
116 let clock = ClockModel(file: stateFile, defaults: defaults)
117 let started = clock.openClocks(session, paths: [other.path])[0].startedAt
118 // `c` isn't a key org-clock-resolve reads; `J` clocks out and jumps.
119 let answers = ScriptedAnswers(["c", "J"])
120 let outcome = await clock.clockIn(session, target: .caret, paths: [other.path], ask: answers.ask, now: { started.addingTimeInterval(600) })
121 #expect(answers.asked.count == 2)
122 #expect(outcome.jump?.path == other.path)
123 #expect(clock.running?.heading == "A")
124 }
125
114 @Test func idleTimeIsSubtractedAndOfferedToTheNextClock() async throws { 126 @Test func idleTimeIsSubtractedAndOfferedToTheNextClock() async throws {
115 let clock = ClockModel(file: stateFile, defaults: defaults) 127 let clock = ClockModel(file: stateFile, defaults: defaults)
116 _ = clock.clockIn(session, path: work.path, at: offset("* A"), now: start) 128 _ = clock.clockIn(session, path: work.path, at: offset("* A"), now: start)
Tests/OrgAppTests/EmacsImportTests.swift +5 −1
@@ -40,6 +40,8 @@ struct EmacsImportTests {
40 (setq org-tags-column 0 40 (setq org-tags-column 0
41 org-agenda-span 10 41 org-agenda-span 10
42 org-agenda-start-day "-3d" 42 org-agenda-start-day "-3d"
43 org-clock-idle-time 15
44 org-clock-history-length 10
43 org-todo-keywords '((sequence "TODO(t)" "WAIT(w)" "|" "DONE(d)") (sequence "[ ](T)" "|" "[X](D)") (sequence "BUG" "|" "FIXED")))) 45 org-todo-keywords '((sequence "TODO(t)" "WAIT(w)" "|" "DONE(d)") (sequence "[ ](T)" "|" "[X](D)") (sequence "BUG" "|" "FIXED"))))
44 (after! org (setq org-M-RET-may-split-line nil org-list-allow-alphabetical t)) 46 (after! org (setq org-M-RET-may-split-line nil org-list-allow-alphabetical t))
45 (map! :map org-mode-map "C-c q" #'org-set-tags-command) 47 (map! :map org-mode-map "C-c q" #'org-set-tags-command)
@@ -79,6 +81,8 @@ struct EmacsImportTests {
79 #expect(s["org-tags-column"] == .integer(-80)) 81 #expect(s["org-tags-column"] == .integer(-80))
80 #expect(s["org-agenda-span"] == .integer(10)) 82 #expect(s["org-agenda-span"] == .integer(10))
81 #expect(s["org-agenda-start-day"] == .string("-3d")) 83 #expect(s["org-agenda-start-day"] == .string("-3d"))
84 #expect(s["org-clock-idle-time"] == .integer(15))
85 #expect(s["org-clock-history-length"] == .integer(10))
82 #expect(s["org-M-RET-may-split-line"] == .bool(false)) 86 #expect(s["org-M-RET-may-split-line"] == .bool(false))
83 #expect(s["org-todo-keywords"] == .string("TODO(t) WAIT(w) | DONE(d)\nBUG | FIXED")) 87 #expect(s["org-todo-keywords"] == .string("TODO(t) WAIT(w) | DONE(d)\nBUG | FIXED"))
84 #expect(s["fill-column"] == nil) 88 #expect(s["fill-column"] == nil)
@@ -167,7 +171,7 @@ struct EmacsImportTests {
167 let keymap = folder.appendingPathComponent("keymap.toml") 171 let keymap = folder.appendingPathComponent("keymap.toml")
168 #expect(KeymapLoader.load(.doom, userFile: keymap).problems.isEmpty) 172 #expect(KeymapLoader.load(.doom, userFile: keymap).problems.isEmpty)
169 #expect(KeymapLoader.load(.doom, userFile: keymap).keymap.keyLabels(for: "app.agenda", modes: ["normal"]).contains("C-c a")) 173 #expect(KeymapLoader.load(.doom, userFile: keymap).keymap.keyLabels(for: "app.agenda", modes: ["normal"]).contains("C-c a"))
170 #expect(summary.hasPrefix("Imported 17 settings, 3 capture templates, 3 key bindings."), "\(summary)") 174 #expect(summary.hasPrefix("Imported 19 settings, 3 capture templates, 3 key bindings."), "\(summary)")
171 #expect(defaults.string(forKey: "themeFont") == "JetBrains Mono") 175 #expect(defaults.string(forKey: "themeFont") == "JetBrains Mono")
172 #expect(defaults.integer(forKey: "themeFontSize") == 14) 176 #expect(defaults.integer(forKey: "themeFontSize") == 14)
173 177
docs/manual/guide/06-dates-and-clocking.org +3 −2
@@ -302,7 +302,8 @@ The answer is a key, as in =org-clock-resolve=. The idle time is the time since
302 302
303| Key | What happens to the =CLOCK:= line | 303| Key | What happens to the =CLOCK:= line |
304|----------------+-----------------------------------------------------------------------------------------------------| 304|----------------+-----------------------------------------------------------------------------------------------------|
305| =j= | Nothing; after Resolve Open Clocks… or the idle question, the line opens in the editor to adjust by hand. | 305| =j= | Nothing; the line opens in the editor to adjust by hand (after the clock-in, when you were clocking in). |
306| =J= | Ends the line now, then opens it in the editor. |
306| =k= | Asks how many minutes of the idle time to keep, all of them by default. With all, the clock keeps running. With fewer, the line ends that many minutes after the idle time began, and a new clock starts now. | 307| =k= | Asks how many minutes of the idle time to keep, all of them by default. With all, the clock keeps running. With fewer, the line ends that many minutes after the idle time began, and a new clock starts now. |
307| =K= | Asks the same, then ends the line after the minutes kept and leaves you clocked out. | 308| =K= | Asks the same, then ends the line after the minutes kept and leaves you clocked out. |
308| =t= | Like =k=, but asks for the date and time you got distracted; the line ends then. | 309| =t= | Like =k=, but asks for the date and time you got distracted; the line ends then. |
@@ -312,7 +313,7 @@ The answer is a key, as in =org-clock-resolve=. The idle time is the time since
312| =s= | Subtracts the idle time: the line ends when the idle time began, and a new clock starts now. | 313| =s= | Subtracts the idle time: the line ends when the idle time began, and a new clock starts now. |
313| =S= | Subtracts the idle time and leaves you clocked out. | 314| =S= | Subtracts the idle time and leaves you clocked out. |
314| =C= | Removes the line, as though you never clocked in. | 315| =C= | Removes the line, as though you never clocked in. |
315| =c=, =i=, =q=, =Esc= | Leaves the line as it is, keeping all the idle time. | 316| =i=, =q=, =Esc= | Leaves the line as it is, keeping all the idle time. |
316 317
317- =k=, =K=, =g= and =G= ask for minutes with the default in the question, as =Keep how many minutes (default 95):=; Return with an empty field takes the default. 318- =k=, =K=, =g= and =G= ask for minutes with the default in the question, as =Keep how many minutes (default 95):=; Return with an empty field takes the default.
318- =t= and =T= ask =Date+time:= with Org's date syntax, as =14:00= or =-1h=. 319- =t= and =T= ask =Date+time:= with Org's date syntax, as =14:00= or =-1h=.
docs/manual/guide/13-configuration.org +2
@@ -416,6 +416,8 @@ Only variables whose names start with =org-=, =appt-=, =display-line-numbers=, =
416| =org-log-into-drawer= | A string as given; =t= becomes ="LOGBOOK"=; =nil= becomes =""= | 416| =org-log-into-drawer= | A string as given; =t= becomes ="LOGBOOK"=; =nil= becomes =""= |
417| =display-line-numbers-type= | =true= unless =nil=; =relative= and =visual= become absolute numbers | 417| =display-line-numbers-type= | =true= unless =nil=; =relative= and =visual= become absolute numbers |
418| =org-agenda-span= | A number, or =day= (1), =week= (7), =fortnight= (14), =month= (30), =year= (365) | 418| =org-agenda-span= | A number, or =day= (1), =week= (7), =fortnight= (14), =month= (30), =year= (365) |
419| =org-clock-idle-time= | Minutes; =nil= becomes =0= (never) |
420| =org-clock-history-length= | The same key, when the value is a positive number |
419| =org-agenda-start-day= | A day offset such as ="-3d"=; =nil= becomes ="+0d"=. Other forms aren't supported. | 421| =org-agenda-start-day= | A day offset such as ="-3d"=; =nil= becomes ="+0d"=. Other forms aren't supported. |
420| =org-todo-keywords= | =org-todo-keywords=, one line per sequence. Keywords with spaces are left out; =type= sequences are read as sequences. | 422| =org-todo-keywords= | =org-todo-keywords=, one line per sequence. Keywords with spaces are left out; =type= sequences are read as sequences. |
421| =org-directory= | A folder to add | 423| =org-directory= | A folder to add |