krz/orgstar

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

Commit b98a6509c5

b98a6509c5ae75e5172dd333d1b6105fd6ceee0e

parent: 322a87d475

Verified · cmc

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

Store Link, Store ID Link and Insert Link on iOS, and org-protocol store-link

Layout: unified · split

Sources/OrgCore/Links/LinkStore.swift +9
@@ -9,6 +9,15 @@ public struct StoredLink: Sendable, Equatable, Codable {
99 self.link = link
1010 self.description = description
1111 }
12
13 /// `org-link--add-to-stored-links`: puts `link` first in `links` and says what happened.
14 public static func add(_ link: StoredLink, to links: inout [StoredLink]) -> String {
15 if links.first == link { return "This link has already been stored" }
16 let moved = links.contains(link)
17 links.removeAll { $0 == link }
18 links.insert(link, at: 0)
19 return (moved ? "Link moved to front: " : "Stored: ") + (link.description ?? link.link)
20 }
1221}
1322
1423extension Links {
Sources/Orgstar/OrgstarApp.swift +2 −9
@@ -32,16 +32,9 @@ final class AppCommands {
3232 /// `org-stored-links`, most recent first; kept for the session, as in Emacs.
3333 var storedLinks: [StoredLink] = []
3434
35 /// `org-link--add-to-stored-links`: puts `link` first and says what happened.
35 /// Puts `link` first in `storedLinks` and says what happened.
3636 func storeLink(_ link: StoredLink) {
37 if storedLinks.first == link {
38 show("This link has already been stored")
39 } else {
40 let moved = storedLinks.contains(link)
41 storedLinks.removeAll { $0 == link }
42 storedLinks.insert(link, at: 0)
43 show((moved ? "Link moved to front: " : "Stored: ") + (link.description ?? link.link))
44 }
37 show(StoredLink.add(link, to: &storedLinks))
4538 }
4639
4740 /// The keys that run `command`, as menus and the palette show them.
Sources/OrgstarMobile/EditorScreen.swift +18
@@ -133,6 +133,22 @@ struct EditorScreen: View {
133133 }
134134 }
135135
136 /// Store Link, Store ID Link and Insert Link, with the links stored while the app runs.
137 private func link(_ id: String) {
138 guard let editor else { return }
139 switch id {
140 case "org.link.store":
141 message = StoredLink.add(Links.store(in: editor.editContext(), path: Links.abbreviate(path, home: NSHomeDirectory())), to: &model.storedLinks)
142 case "org.id.store-link":
143 if case .commit = editor.perform(CreateID()),
144 let link = Links.storeID(in: editor.editContext(), fileName: (path as NSString).lastPathComponent) {
145 message = StoredLink.add(link, to: &model.storedLinks)
146 }
147 default:
148 _ = editor.perform(InsertLink(stored: model.storedLinks, file: path))
149 }
150 }
151
136152 /// Runs a block once it's trusted, asking as `org-confirm-babel-evaluate` does.
137153 private func run(_ job: BabelJob) {
138154 let go = {
@@ -178,6 +194,8 @@ struct EditorScreen: View {
178194 editor.onAppCommand = { id in
179195 if id == "app.toggle-markup" {
180196 model.showMarkup.toggle()
197 } else if MobileEditor.linkCommands.contains(id) {
198 link(id)
181199 } else if !MobileClock.run(id, model: model, workspace: workspace, show: { message = $0 }) {
182200 message = "Not available on iOS yet"
183201 }
Sources/OrgstarMobile/MobileEditor.swift +5 −1
@@ -333,7 +333,7 @@ final class MobileEditor: NSObject, DocumentBuffer {
333333 case "edit.undo": textView.undoManager?.undo()
334334 case "edit.redo": textView.undoManager?.redo()
335335 case "org.table.toggle-column-width", "org.table.shrink", "org.table.expand": tableColumnWidth(id)
336 case _ where id.hasPrefix("app."):
336 case _ where id.hasPrefix("app.") || Self.linkCommands.contains(id):
337337 if let onAppCommand { onAppCommand(id) } else { onMessage?("Not available on iOS yet") }
338338 default:
339339 if let command = Commands.command(id) {
@@ -344,6 +344,9 @@ final class MobileEditor: NSObject, DocumentBuffer {
344344 }
345345 }
346346
347 /// Link commands that need the stored links or the file, which the screen has.
348 static let linkCommands: Set<String> = ["org.link.store", "org.id.store-link", "org.link.insert"]
349
347350 func applies(_ id: String, in context: EditContext) -> Bool {
348351 if let command = Commands.command(id) { return command.applies(in: context) }
349352 switch id {
@@ -352,6 +355,7 @@ final class MobileEditor: NSObject, DocumentBuffer {
352355 case "org.cycle-global", "edit.undo", "edit.redo", "org.toggle-inline-images", "editor.toggle-truncate-lines": return true
353356 case "app.clock.in", "app.clock.out", "app.clock.cancel", "app.clock.goto", "app.clock.in-recent", "app.clock.in-last",
354357 "app.clock.goto-recent", "app.clock.default", "app.clock.resolve", "app.toggle-markup": return onAppCommand != nil
358 case _ where Self.linkCommands.contains(id): return onAppCommand != nil
355359 case "org.link.open": return OpenLinkAtPoint().applies(in: context)
356360 case "org.table.toggle-column-width", "org.table.shrink", "org.table.expand":
357361 return TableShrink.table(at: context.caret, in: context.text) != nil
Sources/OrgstarMobile/MobileModel.swift +2
@@ -48,6 +48,8 @@ final class MobileModel {
4848 private(set) var theme = ThemeSpec.default
4949 /// `show-markup`, toggled in the editor; the folder's value applies when it changes.
5050 var showMarkup = false
51 /// `org-stored-links`, most recent first; kept while the app runs, as in Emacs.
52 var storedLinks: [StoredLink] = []
5153 @ObservationIgnored private var configShowMarkup: Bool?
5254 @ObservationIgnored private var presenter: FolderPresenter?
5355 @ObservationIgnored private var sizeObserver: NSObjectProtocol?
Sources/OrgstarMobile/OrgstarMobileApp.swift +3
@@ -226,6 +226,9 @@ struct RootView: View {
226226 open(link)
227227 } else if let request = CaptureRequest(link: url.absoluteString) {
228228 capture = request
229 } else if case .storeLink(let link, let title)? = OrgProtocol.parse(url.absoluteString) {
230 message = StoredLink.add(StoredLink(link: link, description: title), to: &model.storedLinks)
231 UIPasteboard.general.string = link
229232 }
230233 }
231234 // The Live Activity follows the clock, wherever it starts or stops.
Tests/OrgCoreTests/LinkStoreTests.swift +11
@@ -219,4 +219,15 @@ struct LinkStoreTests {
219219 let id = try #require(text.firstMatch(of: /:ID: +(\S+)/)?.1)
220220 #expect(String(id).range(of: uuid, options: .regularExpression) != nil, "\(text)")
221221 }
222
223 @Test func addingMovesALinkToTheFront() {
224 let a = StoredLink(link: "file:a.org", description: "A")
225 let b = StoredLink(link: "file:b.org", description: nil)
226 var links: [StoredLink] = []
227 #expect(StoredLink.add(a, to: &links) == "Stored: A")
228 #expect(StoredLink.add(b, to: &links) == "Stored: file:b.org")
229 #expect(StoredLink.add(b, to: &links) == "This link has already been stored")
230 #expect(StoredLink.add(a, to: &links) == "Link moved to front: A")
231 #expect(links == [a, b])
232 }
222233}
docs/manual/guide/08-capture.org +1 −1
@@ -264,7 +264,7 @@ org-protocol://store-link?url=https%3A%2F%2Fexample.com&title=Example
264264
265265This stores the link, so =C-c C-l= in the editor offers it (see [[file:09-links.org][Links]]), and puts the URL on the clipboard.
266266
267Other handlers, such as =open-source=, report that Orgstar handles only capture and store-link. On iOS only =capture= links are handled.
267Other handlers, such as =open-source=, report that Orgstar handles only capture and store-link.
268268
269269** Bookmarklets
270270
docs/manual/guide/09-links.org +5 −2
@@ -338,5 +338,8 @@ Insert Link. Setting up a browser bookmarklet, and =org-protocol://capture=, are
338338
339339The iOS app follows links: tap a link in the reader. Org files open in the app, and
340340web, mail and other files go to the system. The iOS editor shows inline images as the
341Mac does; the reader doesn't. Storing and inserting links and the backlinks pane are Mac
342features. See [[file:14-ios.org][iOS]].
341Mac does; the reader doesn't. In the editor, Store Link, Store ID Link and Insert Link…
342are in Commands, and on a hardware keyboard the Emacs keys run them; they work as
343described above, and stored links last until the app quits. =org-protocol://store-link=
344links opened on the device are stored and copied too. The backlinks pane is Mac only.
345See [[file:14-ios.org][iOS]].
docs/manual/guide/14-ios.org +1 −1
@@ -213,7 +213,7 @@ Tap Capture (the pencil icon) in the Agenda or Folders tab.
213213
214214Templates come from =capture.toml= in the configuration folder (see Settings below). Without one, the two default templates apply. A template with =immediate-finish= files as soon as its questions are answered, and one with =jump-to-captured= opens the captured entry. =%^g= offers the target file's tags as choices.
215215
216=org-protocol://capture= links opened on the device open the capture sheet with the link's template, URL, title and text. When the link names a template key that =capture.toml= doesn't have, the sheet starts on the first template and shows =No capture template "x"= with the key.
216=org-protocol://store-link= links add the URL to the stored links that Insert Link… offers, and copy it. =org-protocol://capture= links opened on the device open the capture sheet with the link's template, URL, title and text. When the link names a template key that =capture.toml= doesn't have, the sheet starts on the first template and shows =No capture template "x"= with the key.
217217
218218** From the share sheet
219219
docs/manual/guide/15-alongside-emacs.org +1 −1
@@ -166,7 +166,7 @@ Orgstar declares the =org.orgmode.org= file type for the extensions =.org= and =
166166Orgstar registers the =org-protocol:= URL scheme on the Mac and iOS, and handles two sub-protocols, in both the =?key=value= form and the older =:/a/b/c= form:
167167
168168- =org-protocol://capture= opens Capture with the link's template, URL, title and body.
169- =org-protocol://store-link= stores the link for =C-c C-l= and copies it to the clipboard. This is Mac only; the iOS app handles only =capture=.
169- =org-protocol://store-link= stores the link for =C-c C-l= and copies it to the clipboard.
170170
171171Other sub-protocols, such as =open-source=, show "Orgstar handles org-protocol capture and store-link".
172172