From c7b42d2f150922892936560be1f82b71efd11de4 Mon Sep 17 00:00:00 2001 From: jess Date: Sat, 4 Apr 2026 22:07:37 -0700 Subject: [PATCH] replace RustBridge stubs with real FFI --- src/RustBridge.swift | 220 ++++++++++++++++--------------------------- 1 file changed, 80 insertions(+), 140 deletions(-) diff --git a/src/RustBridge.swift b/src/RustBridge.swift index 9b5427e..0ae2840 100644 --- a/src/RustBridge.swift +++ b/src/RustBridge.swift @@ -9,200 +9,140 @@ struct NoteInfo: Identifiable { class RustBridge { static let shared = RustBridge() - private var notes: [UUID: String] = [:] - private var noteMeta: [UUID: NoteInfo] = [:] + private var docs: [UUID: OpaquePointer] = [:] private init() {} - // MARK: - Document operations (stubbed) - func newDocument() -> UUID { - let id = UUID() - notes[id] = "" - let info = NoteInfo(id: id, title: "Untitled", lastModified: Date()) - noteMeta[id] = info + let ptr = swiftly_doc_new()! + let uuidStr = cacheSaveRaw(ptr) + let id = UUID(uuidString: uuidStr) ?? UUID() + docs[id] = ptr return id } func freeDocument(_ id: UUID) { - notes.removeValue(forKey: id) - noteMeta.removeValue(forKey: id) + guard let ptr = docs.removeValue(forKey: id) else { return } + swiftly_doc_free(ptr) } func setText(_ id: UUID, text: String) { - notes[id] = text - if var info = noteMeta[id] { - info.title = titleFromText(text) - info.lastModified = Date() - noteMeta[id] = info + guard let ptr = docs[id] else { return } + text.withCString { cstr in + swiftly_doc_set_text(ptr, cstr) } } func getText(_ id: UUID) -> String { - return notes[id] ?? "" + guard let ptr = docs[id] else { return "" } + guard let cstr = swiftly_doc_get_text(ptr) else { return "" } + let str = String(cString: cstr) + swiftly_free_string(cstr) + return str } func evaluate(_ id: UUID) -> [Int: String] { - // Stub: parse lines for /= prefix, return placeholder results - guard let text = notes[id] else { return [:] } - var results: [Int: String] = [:] - let lines = text.components(separatedBy: "\n") - for (i, line) in lines.enumerated() { - let trimmed = line.trimmingCharacters(in: .whitespaces) - if trimmed.hasPrefix("/=") { - let expr = String(trimmed.dropFirst(2)).trimmingCharacters(in: .whitespaces) - results[i] = stubbedEval(expr) - } - } - return results + guard let ptr = docs[id] else { return [:] } + guard let cstr = swiftly_doc_evaluate(ptr) else { return [:] } + let json = String(cString: cstr) + swiftly_free_string(cstr) + return parseEvalJSON(json) } func evaluateLine(_ line: String) -> String { - let trimmed = line.trimmingCharacters(in: .whitespaces) - if trimmed.hasPrefix("/=") { - let expr = String(trimmed.dropFirst(2)).trimmingCharacters(in: .whitespaces) - return stubbedEval(expr) - } - return "" + guard let cstr = line.withCString({ swiftly_eval_line($0) }) else { return "" } + let str = String(cString: cstr) + swiftly_free_string(cstr) + return str } - // MARK: - File I/O (stubbed with UserDefaults) - func saveNote(_ id: UUID, path: String) -> Bool { - guard let text = notes[id] else { return false } - do { - try text.write(toFile: path, atomically: true, encoding: .utf8) - return true - } catch { - return false + guard let ptr = docs[id] else { return false } + return path.withCString { cstr in + swiftly_doc_save(ptr, cstr) } } func loadNote(path: String) -> (UUID, String)? { - guard let text = try? String(contentsOfFile: path, encoding: .utf8) else { + guard let ptr = path.withCString({ swiftly_doc_load($0) }) else { return nil } + let uuidStr = cacheSaveRaw(ptr) + guard let id = UUID(uuidString: uuidStr) else { + swiftly_doc_free(ptr) return nil } - let id = UUID() - notes[id] = text - let info = NoteInfo(id: id, title: titleFromText(text), lastModified: Date()) - noteMeta[id] = info + if let old = docs[id] { swiftly_doc_free(old) } + docs[id] = ptr + + guard let cstr = swiftly_doc_get_text(ptr) else { return (id, "") } + let text = String(cString: cstr) + swiftly_free_string(cstr) return (id, text) } - // MARK: - Cache (stubbed with local storage) - - func cacheDir() -> URL { - let appSupport = FileManager.default.urls(for: .applicationSupportDirectory, in: .userDomainMask).first! - let dir = appSupport.appendingPathComponent("Swiftly", isDirectory: true) - try? FileManager.default.createDirectory(at: dir, withIntermediateDirectories: true) - return dir - } - func cacheSave(_ id: UUID) -> Bool { - guard let text = notes[id], let info = noteMeta[id] else { return false } - let dir = cacheDir().appendingPathComponent(id.uuidString, isDirectory: true) - try? FileManager.default.createDirectory(at: dir, withIntermediateDirectories: true) - let textFile = dir.appendingPathComponent("content.txt") - let metaFile = dir.appendingPathComponent("meta.json") - do { - try text.write(to: textFile, atomically: true, encoding: .utf8) - let meta: [String: String] = [ - "title": info.title, - "lastModified": ISO8601DateFormatter().string(from: info.lastModified) - ] - let data = try JSONSerialization.data(withJSONObject: meta) - try data.write(to: metaFile, options: .atomic) - return true - } catch { - return false - } + guard let ptr = docs[id] else { return false } + guard let cstr = swiftly_cache_save(ptr) else { return false } + swiftly_free_string(cstr) + return true } func cacheLoad(_ id: UUID) -> Bool { - let dir = cacheDir().appendingPathComponent(id.uuidString, isDirectory: true) - let textFile = dir.appendingPathComponent("content.txt") - let metaFile = dir.appendingPathComponent("meta.json") - guard let text = try? String(contentsOf: textFile, encoding: .utf8), - let metaData = try? Data(contentsOf: metaFile), - let meta = try? JSONSerialization.jsonObject(with: metaData) as? [String: String] else { - return false - } - notes[id] = text - let formatter = ISO8601DateFormatter() - let date = formatter.date(from: meta["lastModified"] ?? "") ?? Date() - noteMeta[id] = NoteInfo(id: id, title: meta["title"] ?? "Untitled", lastModified: date) + let uuidStr = id.uuidString.lowercased() + guard let ptr = uuidStr.withCString({ swiftly_cache_load($0) }) else { return false } + if let old = docs[id] { swiftly_doc_free(old) } + docs[id] = ptr return true } func listNotes() -> [NoteInfo] { - let dir = cacheDir() - guard let entries = try? FileManager.default.contentsOfDirectory(at: dir, includingPropertiesForKeys: nil) else { - return [] - } - var result: [NoteInfo] = [] - let formatter = ISO8601DateFormatter() - for entry in entries { - guard entry.hasDirectoryPath, - let uuid = UUID(uuidString: entry.lastPathComponent) else { continue } - let metaFile = entry.appendingPathComponent("meta.json") - guard let data = try? Data(contentsOf: metaFile), - let meta = try? JSONSerialization.jsonObject(with: data) as? [String: String] else { continue } - let date = formatter.date(from: meta["lastModified"] ?? "") ?? Date() - let info = NoteInfo(id: uuid, title: meta["title"] ?? "Untitled", lastModified: date) - result.append(info) - if noteMeta[uuid] == nil { - noteMeta[uuid] = info - } - } - return result.sorted { $0.lastModified > $1.lastModified } + guard let cstr = swiftly_list_notes() else { return [] } + let json = String(cString: cstr) + swiftly_free_string(cstr) + return parseNoteListJSON(json) } func deleteNote(_ id: UUID) { - notes.removeValue(forKey: id) - noteMeta.removeValue(forKey: id) - let dir = cacheDir().appendingPathComponent(id.uuidString, isDirectory: true) - try? FileManager.default.removeItem(at: dir) + freeDocument(id) + let cacheDir = FileManager.default.homeDirectoryForCurrentUser + .appendingPathComponent(".swiftly/cache") + let cacheFile = cacheDir.appendingPathComponent("\(id.uuidString.lowercased()).sw") + try? FileManager.default.removeItem(at: cacheFile) } // MARK: - Internal - private func titleFromText(_ text: String) -> String { - let firstLine = text.components(separatedBy: "\n").first ?? "" - let trimmed = firstLine.trimmingCharacters(in: .whitespaces) - if trimmed.isEmpty { return "Untitled" } - let clean = trimmed.replacingOccurrences(of: "^#+\\s*", with: "", options: .regularExpression) - let maxLen = 60 - if clean.count > maxLen { - return String(clean.prefix(maxLen)) + "..." - } - return clean + private func cacheSaveRaw(_ ptr: OpaquePointer) -> String { + guard let cstr = swiftly_cache_save(ptr) else { return UUID().uuidString } + let str = String(cString: cstr) + swiftly_free_string(cstr) + return str } - private func stubbedEval(_ expr: String) -> String { - // Stub: attempt basic arithmetic, otherwise return placeholder - let components = expr.components(separatedBy: .whitespaces).filter { !$0.isEmpty } - if components.count == 3, - let a = Double(components[0]), - let b = Double(components[2]) { - let op = components[1] - switch op { - case "+": return formatNumber(a + b) - case "-": return formatNumber(a - b) - case "*": return formatNumber(a * b) - case "/": return b != 0 ? formatNumber(a / b) : "div/0" - default: break + private func parseEvalJSON(_ json: String) -> [Int: String] { + guard let data = json.data(using: .utf8) else { return [:] } + guard let obj = try? JSONSerialization.jsonObject(with: data) as? [String: Any] else { return [:] } + guard let results = obj["results"] as? [[String: Any]] else { return [:] } + var dict: [Int: String] = [:] + for item in results { + if let line = item["line"] as? Int, let result = item["result"] as? String { + dict[line] = result } } - if components.count == 1, let n = Double(components[0]) { - return formatNumber(n) - } - return "..." + return dict } - private func formatNumber(_ n: Double) -> String { - if n == n.rounded() && abs(n) < 1e15 { - return String(format: "%.0f", n) + private func parseNoteListJSON(_ json: String) -> [NoteInfo] { + guard let data = json.data(using: .utf8) else { return [] } + guard let arr = try? JSONSerialization.jsonObject(with: data) as? [[String: Any]] else { return [] } + var notes: [NoteInfo] = [] + for item in arr { + guard let uuidStr = item["uuid"] as? String, + let uuid = UUID(uuidString: uuidStr), + let title = item["title"] as? String else { continue } + let modified = item["modified"] as? Double ?? 0 + let date = Date(timeIntervalSince1970: modified) + notes.append(NoteInfo(id: uuid, title: title, lastModified: date)) } - return String(format: "%.6g", n) + return notes.sorted { $0.lastModified > $1.lastModified } } }