replace RustBridge stubs with real FFI
This commit is contained in:
parent
fe3a5d60d1
commit
c7b42d2f15
|
|
@ -9,200 +9,140 @@ struct NoteInfo: Identifiable {
|
||||||
class RustBridge {
|
class RustBridge {
|
||||||
static let shared = RustBridge()
|
static let shared = RustBridge()
|
||||||
|
|
||||||
private var notes: [UUID: String] = [:]
|
private var docs: [UUID: OpaquePointer] = [:]
|
||||||
private var noteMeta: [UUID: NoteInfo] = [:]
|
|
||||||
|
|
||||||
private init() {}
|
private init() {}
|
||||||
|
|
||||||
// MARK: - Document operations (stubbed)
|
|
||||||
|
|
||||||
func newDocument() -> UUID {
|
func newDocument() -> UUID {
|
||||||
let id = UUID()
|
let ptr = swiftly_doc_new()!
|
||||||
notes[id] = ""
|
let uuidStr = cacheSaveRaw(ptr)
|
||||||
let info = NoteInfo(id: id, title: "Untitled", lastModified: Date())
|
let id = UUID(uuidString: uuidStr) ?? UUID()
|
||||||
noteMeta[id] = info
|
docs[id] = ptr
|
||||||
return id
|
return id
|
||||||
}
|
}
|
||||||
|
|
||||||
func freeDocument(_ id: UUID) {
|
func freeDocument(_ id: UUID) {
|
||||||
notes.removeValue(forKey: id)
|
guard let ptr = docs.removeValue(forKey: id) else { return }
|
||||||
noteMeta.removeValue(forKey: id)
|
swiftly_doc_free(ptr)
|
||||||
}
|
}
|
||||||
|
|
||||||
func setText(_ id: UUID, text: String) {
|
func setText(_ id: UUID, text: String) {
|
||||||
notes[id] = text
|
guard let ptr = docs[id] else { return }
|
||||||
if var info = noteMeta[id] {
|
text.withCString { cstr in
|
||||||
info.title = titleFromText(text)
|
swiftly_doc_set_text(ptr, cstr)
|
||||||
info.lastModified = Date()
|
|
||||||
noteMeta[id] = info
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func getText(_ id: UUID) -> String {
|
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] {
|
func evaluate(_ id: UUID) -> [Int: String] {
|
||||||
// Stub: parse lines for /= prefix, return placeholder results
|
guard let ptr = docs[id] else { return [:] }
|
||||||
guard let text = notes[id] else { return [:] }
|
guard let cstr = swiftly_doc_evaluate(ptr) else { return [:] }
|
||||||
var results: [Int: String] = [:]
|
let json = String(cString: cstr)
|
||||||
let lines = text.components(separatedBy: "\n")
|
swiftly_free_string(cstr)
|
||||||
for (i, line) in lines.enumerated() {
|
return parseEvalJSON(json)
|
||||||
let trimmed = line.trimmingCharacters(in: .whitespaces)
|
|
||||||
if trimmed.hasPrefix("/=") {
|
|
||||||
let expr = String(trimmed.dropFirst(2)).trimmingCharacters(in: .whitespaces)
|
|
||||||
results[i] = stubbedEval(expr)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return results
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func evaluateLine(_ line: String) -> String {
|
func evaluateLine(_ line: String) -> String {
|
||||||
let trimmed = line.trimmingCharacters(in: .whitespaces)
|
guard let cstr = line.withCString({ swiftly_eval_line($0) }) else { return "" }
|
||||||
if trimmed.hasPrefix("/=") {
|
let str = String(cString: cstr)
|
||||||
let expr = String(trimmed.dropFirst(2)).trimmingCharacters(in: .whitespaces)
|
swiftly_free_string(cstr)
|
||||||
return stubbedEval(expr)
|
return str
|
||||||
}
|
|
||||||
return ""
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - File I/O (stubbed with UserDefaults)
|
|
||||||
|
|
||||||
func saveNote(_ id: UUID, path: String) -> Bool {
|
func saveNote(_ id: UUID, path: String) -> Bool {
|
||||||
guard let text = notes[id] else { return false }
|
guard let ptr = docs[id] else { return false }
|
||||||
do {
|
return path.withCString { cstr in
|
||||||
try text.write(toFile: path, atomically: true, encoding: .utf8)
|
swiftly_doc_save(ptr, cstr)
|
||||||
return true
|
|
||||||
} catch {
|
|
||||||
return false
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func loadNote(path: String) -> (UUID, String)? {
|
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
|
return nil
|
||||||
}
|
}
|
||||||
let id = UUID()
|
if let old = docs[id] { swiftly_doc_free(old) }
|
||||||
notes[id] = text
|
docs[id] = ptr
|
||||||
let info = NoteInfo(id: id, title: titleFromText(text), lastModified: Date())
|
|
||||||
noteMeta[id] = info
|
guard let cstr = swiftly_doc_get_text(ptr) else { return (id, "") }
|
||||||
|
let text = String(cString: cstr)
|
||||||
|
swiftly_free_string(cstr)
|
||||||
return (id, text)
|
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 {
|
func cacheSave(_ id: UUID) -> Bool {
|
||||||
guard let text = notes[id], let info = noteMeta[id] else { return false }
|
guard let ptr = docs[id] else { return false }
|
||||||
let dir = cacheDir().appendingPathComponent(id.uuidString, isDirectory: true)
|
guard let cstr = swiftly_cache_save(ptr) else { return false }
|
||||||
try? FileManager.default.createDirectory(at: dir, withIntermediateDirectories: true)
|
swiftly_free_string(cstr)
|
||||||
let textFile = dir.appendingPathComponent("content.txt")
|
return true
|
||||||
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
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func cacheLoad(_ id: UUID) -> Bool {
|
func cacheLoad(_ id: UUID) -> Bool {
|
||||||
let dir = cacheDir().appendingPathComponent(id.uuidString, isDirectory: true)
|
let uuidStr = id.uuidString.lowercased()
|
||||||
let textFile = dir.appendingPathComponent("content.txt")
|
guard let ptr = uuidStr.withCString({ swiftly_cache_load($0) }) else { return false }
|
||||||
let metaFile = dir.appendingPathComponent("meta.json")
|
if let old = docs[id] { swiftly_doc_free(old) }
|
||||||
guard let text = try? String(contentsOf: textFile, encoding: .utf8),
|
docs[id] = ptr
|
||||||
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)
|
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
func listNotes() -> [NoteInfo] {
|
func listNotes() -> [NoteInfo] {
|
||||||
let dir = cacheDir()
|
guard let cstr = swiftly_list_notes() else { return [] }
|
||||||
guard let entries = try? FileManager.default.contentsOfDirectory(at: dir, includingPropertiesForKeys: nil) else {
|
let json = String(cString: cstr)
|
||||||
return []
|
swiftly_free_string(cstr)
|
||||||
}
|
return parseNoteListJSON(json)
|
||||||
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 }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func deleteNote(_ id: UUID) {
|
func deleteNote(_ id: UUID) {
|
||||||
notes.removeValue(forKey: id)
|
freeDocument(id)
|
||||||
noteMeta.removeValue(forKey: id)
|
let cacheDir = FileManager.default.homeDirectoryForCurrentUser
|
||||||
let dir = cacheDir().appendingPathComponent(id.uuidString, isDirectory: true)
|
.appendingPathComponent(".swiftly/cache")
|
||||||
try? FileManager.default.removeItem(at: dir)
|
let cacheFile = cacheDir.appendingPathComponent("\(id.uuidString.lowercased()).sw")
|
||||||
|
try? FileManager.default.removeItem(at: cacheFile)
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Internal
|
// MARK: - Internal
|
||||||
|
|
||||||
private func titleFromText(_ text: String) -> String {
|
private func cacheSaveRaw(_ ptr: OpaquePointer) -> String {
|
||||||
let firstLine = text.components(separatedBy: "\n").first ?? ""
|
guard let cstr = swiftly_cache_save(ptr) else { return UUID().uuidString }
|
||||||
let trimmed = firstLine.trimmingCharacters(in: .whitespaces)
|
let str = String(cString: cstr)
|
||||||
if trimmed.isEmpty { return "Untitled" }
|
swiftly_free_string(cstr)
|
||||||
let clean = trimmed.replacingOccurrences(of: "^#+\\s*", with: "", options: .regularExpression)
|
return str
|
||||||
let maxLen = 60
|
|
||||||
if clean.count > maxLen {
|
|
||||||
return String(clean.prefix(maxLen)) + "..."
|
|
||||||
}
|
|
||||||
return clean
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private func stubbedEval(_ expr: String) -> String {
|
private func parseEvalJSON(_ json: String) -> [Int: String] {
|
||||||
// Stub: attempt basic arithmetic, otherwise return placeholder
|
guard let data = json.data(using: .utf8) else { return [:] }
|
||||||
let components = expr.components(separatedBy: .whitespaces).filter { !$0.isEmpty }
|
guard let obj = try? JSONSerialization.jsonObject(with: data) as? [String: Any] else { return [:] }
|
||||||
if components.count == 3,
|
guard let results = obj["results"] as? [[String: Any]] else { return [:] }
|
||||||
let a = Double(components[0]),
|
var dict: [Int: String] = [:]
|
||||||
let b = Double(components[2]) {
|
for item in results {
|
||||||
let op = components[1]
|
if let line = item["line"] as? Int, let result = item["result"] as? String {
|
||||||
switch op {
|
dict[line] = result
|
||||||
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
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if components.count == 1, let n = Double(components[0]) {
|
return dict
|
||||||
return formatNumber(n)
|
|
||||||
}
|
|
||||||
return "..."
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private func formatNumber(_ n: Double) -> String {
|
private func parseNoteListJSON(_ json: String) -> [NoteInfo] {
|
||||||
if n == n.rounded() && abs(n) < 1e15 {
|
guard let data = json.data(using: .utf8) else { return [] }
|
||||||
return String(format: "%.0f", n)
|
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 }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue