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 { |
| 9 | 9 | self.link = link |
| 10 | 10 | self.description = description |
| 11 | 11 | } |
| 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 | } |
| 12 | 21 | } |
| 13 | 22 | |
| 14 | 23 | extension Links { |
Sources/Orgstar/OrgstarApp.swift
+2 −9
| @@ -32,16 +32,9 @@ final class AppCommands { |
| 32 | 32 | /// `org-stored-links`, most recent first; kept for the session, as in Emacs. |
| 33 | 33 | var storedLinks: [StoredLink] = [] |
| 34 | 34 | |
| 35 | | /// `org-link--add-to-stored-links`: puts `link` first and says what happened. |
| 35 | /// Puts `link` first in `storedLinks` and says what happened. |
| 36 | 36 | 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)) |
| 45 | 38 | } |
| 46 | 39 | |
| 47 | 40 | /// The keys that run `command`, as menus and the palette show them. |
Sources/OrgstarMobile/EditorScreen.swift
+18
| @@ -133,6 +133,22 @@ struct EditorScreen: View { |
| 133 | 133 | } |
| 134 | 134 | } |
| 135 | 135 | |
| 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 | |
| 136 | 152 | /// Runs a block once it's trusted, asking as `org-confirm-babel-evaluate` does. |
| 137 | 153 | private func run(_ job: BabelJob) { |
| 138 | 154 | let go = { |
| @@ -178,6 +194,8 @@ struct EditorScreen: View { |
| 178 | 194 | editor.onAppCommand = { id in |
| 179 | 195 | if id == "app.toggle-markup" { |
| 180 | 196 | model.showMarkup.toggle() |
| 197 | } else if MobileEditor.linkCommands.contains(id) { |
| 198 | link(id) |
| 181 | 199 | } else if !MobileClock.run(id, model: model, workspace: workspace, show: { message = $0 }) { |
| 182 | 200 | message = "Not available on iOS yet" |
| 183 | 201 | } |
Sources/OrgstarMobile/MobileEditor.swift
+5 −1
| @@ -333,7 +333,7 @@ final class MobileEditor: NSObject, DocumentBuffer { |
| 333 | 333 | case "edit.undo": textView.undoManager?.undo() |
| 334 | 334 | case "edit.redo": textView.undoManager?.redo() |
| 335 | 335 | 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): |
| 337 | 337 | if let onAppCommand { onAppCommand(id) } else { onMessage?("Not available on iOS yet") } |
| 338 | 338 | default: |
| 339 | 339 | if let command = Commands.command(id) { |
| @@ -344,6 +344,9 @@ final class MobileEditor: NSObject, DocumentBuffer { |
| 344 | 344 | } |
| 345 | 345 | } |
| 346 | 346 | |
| 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 | |
| 347 | 350 | func applies(_ id: String, in context: EditContext) -> Bool { |
| 348 | 351 | if let command = Commands.command(id) { return command.applies(in: context) } |
| 349 | 352 | switch id { |
| @@ -352,6 +355,7 @@ final class MobileEditor: NSObject, DocumentBuffer { |
| 352 | 355 | case "org.cycle-global", "edit.undo", "edit.redo", "org.toggle-inline-images", "editor.toggle-truncate-lines": return true |
| 353 | 356 | case "app.clock.in", "app.clock.out", "app.clock.cancel", "app.clock.goto", "app.clock.in-recent", "app.clock.in-last", |
| 354 | 357 | "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 |
| 355 | 359 | case "org.link.open": return OpenLinkAtPoint().applies(in: context) |
| 356 | 360 | case "org.table.toggle-column-width", "org.table.shrink", "org.table.expand": |
| 357 | 361 | return TableShrink.table(at: context.caret, in: context.text) != nil |
Sources/OrgstarMobile/MobileModel.swift
+2
| @@ -48,6 +48,8 @@ final class MobileModel { |
| 48 | 48 | private(set) var theme = ThemeSpec.default |
| 49 | 49 | /// `show-markup`, toggled in the editor; the folder's value applies when it changes. |
| 50 | 50 | var showMarkup = false |
| 51 | /// `org-stored-links`, most recent first; kept while the app runs, as in Emacs. |
| 52 | var storedLinks: [StoredLink] = [] |
| 51 | 53 | @ObservationIgnored private var configShowMarkup: Bool? |
| 52 | 54 | @ObservationIgnored private var presenter: FolderPresenter? |
| 53 | 55 | @ObservationIgnored private var sizeObserver: NSObjectProtocol? |
Sources/OrgstarMobile/OrgstarMobileApp.swift
+3
| @@ -226,6 +226,9 @@ struct RootView: View { |
| 226 | 226 | open(link) |
| 227 | 227 | } else if let request = CaptureRequest(link: url.absoluteString) { |
| 228 | 228 | 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 |
| 229 | 232 | } |
| 230 | 233 | } |
| 231 | 234 | // The Live Activity follows the clock, wherever it starts or stops. |
Tests/OrgCoreTests/LinkStoreTests.swift
+11
| @@ -219,4 +219,15 @@ struct LinkStoreTests { |
| 219 | 219 | let id = try #require(text.firstMatch(of: /:ID: +(\S+)/)?.1) |
| 220 | 220 | #expect(String(id).range(of: uuid, options: .regularExpression) != nil, "\(text)") |
| 221 | 221 | } |
| 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 | } |
| 222 | 233 | } |
docs/manual/guide/08-capture.org
+1 −1
| @@ -264,7 +264,7 @@ org-protocol://store-link?url=https%3A%2F%2Fexample.com&title=Example |
| 264 | 264 | |
| 265 | 265 | This 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. |
| 266 | 266 | |
| 267 | | Other handlers, such as =open-source=, report that Orgstar handles only capture and store-link. On iOS only =capture= links are handled. |
| 267 | Other handlers, such as =open-source=, report that Orgstar handles only capture and store-link. |
| 268 | 268 | |
| 269 | 269 | ** Bookmarklets |
| 270 | 270 | |
docs/manual/guide/09-links.org
+5 −2
| @@ -338,5 +338,8 @@ Insert Link. Setting up a browser bookmarklet, and =org-protocol://capture=, are |
| 338 | 338 | |
| 339 | 339 | The iOS app follows links: tap a link in the reader. Org files open in the app, and |
| 340 | 340 | web, mail and other files go to the system. The iOS editor shows inline images as the |
| 341 | | Mac does; the reader doesn't. Storing and inserting links and the backlinks pane are Mac |
| 342 | | features. See [[file:14-ios.org][iOS]]. |
| 341 | Mac does; the reader doesn't. In the editor, Store Link, Store ID Link and Insert Link… |
| 342 | are in Commands, and on a hardware keyboard the Emacs keys run them; they work as |
| 343 | described above, and stored links last until the app quits. =org-protocol://store-link= |
| 344 | links opened on the device are stored and copied too. The backlinks pane is Mac only. |
| 345 | See [[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. |
| 213 | 213 | |
| 214 | 214 | Templates 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. |
| 215 | 215 | |
| 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. |
| 217 | 217 | |
| 218 | 218 | ** From the share sheet |
| 219 | 219 | |
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 = |
| 166 | 166 | Orgstar 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: |
| 167 | 167 | |
| 168 | 168 | - =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. |
| 170 | 170 | |
| 171 | 171 | Other sub-protocols, such as =open-source=, show "Orgstar handles org-protocol capture and store-link". |
| 172 | 172 | |