209 lines
7.5 KiB
Swift
209 lines
7.5 KiB
Swift
import Foundation
|
|
|
|
struct NoteInfo: Identifiable {
|
|
let id: UUID
|
|
var title: String
|
|
var lastModified: Date
|
|
}
|
|
|
|
class RustBridge {
|
|
static let shared = RustBridge()
|
|
|
|
private var notes: [UUID: String] = [:]
|
|
private var noteMeta: [UUID: NoteInfo] = [:]
|
|
|
|
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
|
|
return id
|
|
}
|
|
|
|
func freeDocument(_ id: UUID) {
|
|
notes.removeValue(forKey: id)
|
|
noteMeta.removeValue(forKey: id)
|
|
}
|
|
|
|
func setText(_ id: UUID, text: String) {
|
|
notes[id] = text
|
|
if var info = noteMeta[id] {
|
|
info.title = titleFromText(text)
|
|
info.lastModified = Date()
|
|
noteMeta[id] = info
|
|
}
|
|
}
|
|
|
|
func getText(_ id: UUID) -> String {
|
|
return notes[id] ?? ""
|
|
}
|
|
|
|
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
|
|
}
|
|
|
|
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 ""
|
|
}
|
|
|
|
// 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
|
|
}
|
|
}
|
|
|
|
func loadNote(path: String) -> (UUID, String)? {
|
|
guard let text = try? String(contentsOfFile: path, encoding: .utf8) else {
|
|
return nil
|
|
}
|
|
let id = UUID()
|
|
notes[id] = text
|
|
let info = NoteInfo(id: id, title: titleFromText(text), lastModified: Date())
|
|
noteMeta[id] = info
|
|
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
|
|
}
|
|
}
|
|
|
|
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)
|
|
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 }
|
|
}
|
|
|
|
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)
|
|
}
|
|
|
|
// 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 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
|
|
}
|
|
}
|
|
if components.count == 1, let n = Double(components[0]) {
|
|
return formatNumber(n)
|
|
}
|
|
return "..."
|
|
}
|
|
|
|
private func formatNumber(_ n: Double) -> String {
|
|
if n == n.rounded() && abs(n) < 1e15 {
|
|
return String(format: "%.0f", n)
|
|
}
|
|
return String(format: "%.6g", n)
|
|
}
|
|
}
|