krz/orgstar

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

Commit d8259fc2c7

d8259fc2c727995f7e45dd89b04231a66a65862a

parent: f18103132a

Verified · cmc

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

Run Emacs export with Babel's PATH and stop it on cancel

Export uses BabelRunner.environment, which now also adds
/Library/TeX/texbin. Cancelling an export or a table recalculation
terminates Emacs; Task.sleep used to throw before the process was
terminated.

Layout: unified · split

Sources/OrgApp/BabelRunner.swift +1 −1
@@ -12,7 +12,7 @@ public enum BabelRunner {
12 /// PATH with the usual install locations, which an app launched from the Dock lacks. 12 /// PATH with the usual install locations, which an app launched from the Dock lacks.
13 public static var environment: [String: String] { 13 public static var environment: [String: String] {
14 var environment = ProcessInfo.processInfo.environment 14 var environment = ProcessInfo.processInfo.environment
15 let extra = ["/opt/homebrew/bin", "/usr/local/bin", "/usr/bin", "/bin"] 15 let extra = ["/opt/homebrew/bin", "/usr/local/bin", "/Library/TeX/texbin", "/usr/bin", "/bin"]
16 let path = (environment["PATH"] ?? "").split(separator: ":").map(String.init) 16 let path = (environment["PATH"] ?? "").split(separator: ":").map(String.init)
17 environment["PATH"] = (path + extra.filter { !path.contains($0) }).joined(separator: ":") 17 environment["PATH"] = (path + extra.filter { !path.contains($0) }).joined(separator: ":")
18 return environment 18 return environment
Sources/OrgApp/EmacsRunner.swift +8 −2
@@ -67,7 +67,7 @@ public enum EmacsRunner {
67 process.terminate() 67 process.terminate()
68 throw Failure(description: "Emacs took too long to recalculate the table.") 68 throw Failure(description: "Emacs took too long to recalculate the table.")
69 } 69 }
70 try await Task.sleep(for: .milliseconds(50)) 70 try? await Task.sleep(for: .milliseconds(50))
71 if Task.isCancelled { 71 if Task.isCancelled {
72 process.terminate() 72 process.terminate()
73 throw CancellationError() 73 throw CancellationError()
@@ -113,6 +113,8 @@ public enum EmacsRunner {
113 process.executableURL = emacs 113 process.executableURL = emacs
114 process.arguments = ["-Q", "--batch", "-l", script.path] 114 process.arguments = ["-Q", "--batch", "-l", script.path]
115 process.currentDirectoryURL = URL(fileURLWithPath: path).deletingLastPathComponent() 115 process.currentDirectoryURL = URL(fileURLWithPath: path).deletingLastPathComponent()
116 // Babel's PATH, so a Dock-launched app's Emacs finds latexmk and pdflatex.
117 process.environment = BabelRunner.environment
116 let errors = Pipe() 118 let errors = Pipe()
117 process.standardError = errors 119 process.standardError = errors
118 process.standardOutput = FileHandle.nullDevice 120 process.standardOutput = FileHandle.nullDevice
@@ -123,7 +125,11 @@ public enum EmacsRunner {
123 process.terminate() 125 process.terminate()
124 throw Failure(description: "Emacs took too long to export.") 126 throw Failure(description: "Emacs took too long to export.")
125 } 127 }
126 try await Task.sleep(for: .milliseconds(100)) 128 try? await Task.sleep(for: .milliseconds(100))
129 if Task.isCancelled {
130 process.terminate()
131 throw CancellationError()
132 }
127 } 133 }
128 guard let written = try? String(contentsOf: output, encoding: .utf8) else { 134 guard let written = try? String(contentsOf: output, encoding: .utf8) else {
129 let log = String(decoding: errors.fileHandleForReading.readDataToEndOfFile(), as: UTF8.self) 135 let log = String(decoding: errors.fileHandleForReading.readDataToEndOfFile(), as: UTF8.self)
Tests/OrgAppTests/AppTests.swift +32
@@ -783,6 +783,38 @@ struct EmacsExportTests {
783 #expect(text.uppercased().contains("IN THE BUFFER") && text.contains("Body."), "\(text)") 783 #expect(text.uppercased().contains("IN THE BUFFER") && text.contains("Body."), "\(text)")
784 #expect(try String(contentsOf: file, encoding: .utf8) == "#+TITLE: On disk\n") 784 #expect(try String(contentsOf: file, encoding: .utf8) == "#+TITLE: On disk\n")
785 } 785 }
786
787 @Test(.enabled(if: EmacsRunner.executable() != nil && ProcessInfo.processInfo.environment["ORGSTAR_SKIP_ORACLE"] == nil))
788 func exportUsesBabelPath() async throws {
789 let notes = try Folder()
790 let file = try notes.write("doc.org", "")
791 let function = #"(lambda () (with-temp-file "path.txt" (insert (getenv "PATH"))) "path.txt")"#
792 let written = try await EmacsRunner.export(text: "", path: file.path, function: function)
793 #expect(try String(contentsOfFile: written, encoding: .utf8) == BabelRunner.environment["PATH"])
794 }
795
796 @Test(.enabled(if: EmacsRunner.executable() != nil && ProcessInfo.processInfo.environment["ORGSTAR_SKIP_ORACLE"] == nil))
797 func cancellingStopsTheExport() async throws {
798 let notes = try Folder()
799 let file = try notes.write("doc.org", "")
800 let pidFile = notes.url.appendingPathComponent("pid")
801 let function = #"(lambda () (with-temp-file "pid" (insert (number-to-string (emacs-pid)))) (sleep-for 60) "x")"#
802 let export = Task { try await EmacsRunner.export(text: "", path: file.path, function: function) }
803 var pid: pid_t?
804 for _ in 0..<200 where pid == nil {
805 try await Task.sleep(for: .milliseconds(50))
806 pid = (try? String(contentsOf: pidFile, encoding: .utf8)).flatMap { pid_t($0) }
807 }
808 let started = try #require(pid)
809 export.cancel()
810 await #expect(throws: CancellationError.self) { try await export.value }
811 var alive = true
812 for _ in 0..<100 where alive {
813 alive = kill(started, 0) == 0
814 if alive { try await Task.sleep(for: .milliseconds(50)) }
815 }
816 #expect(!alive)
817 }
786} 818}
787 819
788@MainActor 820@MainActor