krz/orgstar

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

Commit 37a20f818f

37a20f818f27acd77d128e2287d6291b43bc0de9

parent: bbff3cc2a0

Verified · cmc

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

Give accurate iOS messages

Only table recalculation says it needs Emacs on the Mac; other requests
iOS doesn't handle say the command isn't available on iPhone and iPad.
A key sequence bound to a command that doesn't apply or isn't supported
runs it anyway, so it reports why, as on the Mac. An org-protocol capture
naming a missing template reports it. Settings shows the agenda start day
in words.

Layout: unified · split

Sources/OrgstarMobile/AgendaScreen.swift +2 −2
@@ -250,8 +250,8 @@ struct AgendaScreen: View {
250 } 250 }
251 case .failed(let text): 251 case .failed(let text):
252 message = text 252 message = text
253 case .external: 253 case .external(let request):
254 message = "\(command.title) needs Emacs, which runs on the Mac." 254 message = MobileEditor.unavailable(request, title: command.title)
255 case .commit: 255 case .commit:
256 message = nil 256 message = nil
257 } 257 }
Sources/OrgstarMobile/CaptureScreen.swift +1
@@ -65,6 +65,7 @@ struct CaptureScreen: View {
65 templates = loaded.templates 65 templates = loaded.templates
66 problem = loaded.problems.first 66 problem = loaded.problems.first
67 key = request.template.flatMap { wanted in templates.first { $0.key == wanted }?.key } ?? templates.first?.key 67 key = request.template.flatMap { wanted in templates.first { $0.key == wanted }?.key } ?? templates.first?.key
68 if let wanted = request.template, !templates.contains(where: { $0.key == wanted }) { problem = "No capture template \"\(wanted)\"" }
68 } 69 }
69 .onChange(of: key, initial: true) { choose() } 70 .onChange(of: key, initial: true) { choose() }
70 } 71 }
Sources/OrgstarMobile/MobileEditor.swift +11 −6
@@ -184,14 +184,18 @@ final class MobileEditor: NSObject, DocumentBuffer {
184 onTangle?(scope) 184 onTangle?(scope)
185 case .external(.agenda(let start, let count)) where onAgenda != nil: 185 case .external(.agenda(let start, let count)) where onAgenda != nil:
186 onAgenda?(start, count) 186 onAgenda?(start, count)
187 case .external(.emacsTable(_, _, _, let reason, _)): 187 case .external(let request):
188 onMessage?("This table needs Emacs, which runs on the Mac: \(reason).") 188 onMessage?(Self.unavailable(request, title: command.title))
189 case .external:
190 onMessage?("\(command.title) needs Emacs, which runs on the Mac.")
191 } 189 }
192 return step 190 return step
193 } 191 }
194 192
193 /// Why a request the app doesn't handle did nothing: only table recalculation needs Emacs.
194 static func unavailable(_ request: ExternalRequest, title: String) -> String {
195 if case .emacsTable(_, _, _, let reason, _) = request { return "This table needs Emacs, which runs on the Mac: \(reason)." }
196 return "\(title) isn't available on iPhone and iPad."
197 }
198
195 // MARK: - Commands by name and key 199 // MARK: - Commands by name and key
196 200
197 /// Runs what `keys` (as `C-c C-t`) is bound to in the Emacs keymap, for the caret's context. 201 /// Runs what `keys` (as `C-c C-t`) is bound to in the Emacs keymap, for the caret's context.
@@ -288,8 +292,9 @@ final class MobileEditor: NSObject, DocumentBuffer {
288 let runnable = candidates.first { binding in 292 let runnable = candidates.first { binding in
289 (binding.when.map { KeyContext.holds($0, in: context) } ?? true) && applies(binding.command, in: context) 293 (binding.when.map { KeyContext.holds($0, in: context) } ?? true) && applies(binding.command, in: context)
290 } 294 }
291 guard let binding = runnable else { return keys.count > 1 } 295 // A sequence runs its command even where it doesn't apply, which says why, as on the Mac.
292 if binding.command.hasPrefix("edit."), !["edit.undo", "edit.redo"].contains(binding.command) { return keys.count > 1 } 296 guard let binding = runnable ?? (keys.count > 1 ? candidates.first : nil) else { return false }
297 if keys.count == 1, binding.command.hasPrefix("edit."), !["edit.undo", "edit.redo"].contains(binding.command) { return false }
293 run(binding.command) 298 run(binding.command)
294 return true 299 return true
295 } 300 }
Sources/OrgstarMobile/SettingsScreen.swift +9 −1
@@ -26,7 +26,7 @@ struct SettingsScreen: View {
26 Section("In use") { 26 Section("In use") {
27 let _ = model.settingsVersion 27 let _ = model.settingsVersion
28 LabeledContent("TODO keywords", value: todoKeywords) 28 LabeledContent("TODO keywords", value: todoKeywords)
29 LabeledContent("Agenda", value: "\(OrgPreferences.agendaSpan()) days from \(OrgPreferences.agendaStartOffset())d") 29 LabeledContent("Agenda", value: agenda)
30 LabeledContent("Reminders", value: OrgPreferences.reminders() ? "\(OrgPreferences.reminderLead()) min before" : "Off") 30 LabeledContent("Reminders", value: OrgPreferences.reminders() ? "\(OrgPreferences.reminderLead()) min before" : "Off")
31 LabeledContent("Capture templates", value: CaptureTemplates.load().templates.map(\.key).joined(separator: " ")) 31 LabeledContent("Capture templates", value: CaptureTemplates.load().templates.map(\.key).joined(separator: " "))
32 } 32 }
@@ -37,6 +37,14 @@ struct SettingsScreen: View {
37 } 37 }
38 } 38 }
39 39
40 /// The span and start day, worded as the Mac's Settings word the start.
41 private var agenda: String {
42 func days(_ count: Int) -> String { count == 1 ? "1 day" : "\(count) days" }
43 let offset = OrgPreferences.agendaStartOffset()
44 let start = offset == 0 ? "today" : offset < 0 ? "\(days(-offset)) before today" : "\(days(offset)) after today"
45 return "\(days(OrgPreferences.agendaSpan())), starting \(start)"
46 }
47
40 private var todoKeywords: String { 48 private var todoKeywords: String {
41 OrgPreferences.orgSettings().todoSequences.map { ($0.active.map(\.name) + ["|"] + $0.done.map(\.name)).joined(separator: " ") } 49 OrgPreferences.orgSettings().todoSequences.map { ($0.active.map(\.name) + ["|"] + $0.done.map(\.name)).joined(separator: " ") }
42 .joined(separator: "; ") 50 .joined(separator: "; ")