import Foundation /// Pure helpers for session-folder names. A session folder is created at start /// with an auto name `_`; when the user names the /// recording on stop it's renamed to `__` (no HH-MM-SS), /// which is far easier to scan in `sessions/`. The app label always stays the /// LAST `_`-separated segment so `SessionController.appLabel(from:)` keeps working /// even when the meeting name itself contains spaces or underscores. enum SessionNaming { /// Filesystem- and parse-safe meeting name: trims, turns path separators into /// dashes, drops control characters, collapses whitespace runs, removes leading /// dots (no hidden/`.`/`..` folders), and caps the length. Returns "" if nothing /// usable is left, which callers treat as "skip the rename". static func sanitize(_ raw: String) -> String { var s = raw.trimmingCharacters(in: .whitespacesAndNewlines) // Path-hostile separators (`/` and the classic Mac `:`, plus `\`) → dash. s = s.components(separatedBy: CharacterSet(charactersIn: "/:\\")).joined(separator: "-") // Strip control characters outright. s = s.components(separatedBy: .controlCharacters).joined() // Collapse internal whitespace runs to single spaces. s = s.split(whereSeparator: { $0 == " " || $0 == "\t" }).joined(separator: " ") while s.hasPrefix(".") { s.removeFirst() } s = s.trimmingCharacters(in: .whitespaces) if s.count > 60 { s = String(s.prefix(60)).trimmingCharacters(in: .whitespaces) } return s } /// The date prefix of a session leaf name, e.g. `2026-06-17T09-59-48_signal` /// → `2026-06-17`. Already-renamed leaves (`2026-06-17_name_signal`) return the /// same date, so this is safe to call on either form. static func datePrefix(ofSessionNamed leaf: String) -> String { let head = leaf.split(separator: "_").first.map(String.init) ?? leaf return head.split(separator: "T").first.map(String.init) ?? head } /// Compose the renamed leaf `__`. A positive `counter` /// disambiguates a collision by suffixing the NAME segment (`-2`) so the /// trailing `_` stays parseable. Returns nil when the name sanitizes to /// empty (the caller keeps the auto timestamped name). static func renamedLeaf(date: String, app: String, meetingName: String, counter: Int = 0) -> String? { let clean = sanitize(meetingName) guard !clean.isEmpty else { return nil } let suffix = counter > 0 ? "-\(counter + 1)" : "" return "\(date)_\(clean)\(suffix)_\(app)" } /// Friendly recap title from a session id, understanding both folder forms: /// `2026-06-06T11-43-02_meet` → "Google Meet call — 2026-06-06 11:43" /// `2026-06-06_Weekly sync_meet` → "Weekly sync — Google Meet (2026-06-06)" static func recapTitle(app: String, sessionId: String) -> String { let appName = CallDetector.DetectedApp(rawValue: app)?.display ?? app.capitalized var parts = sessionId.split(separator: "_").map(String.init) if parts.count > 1 { parts.removeLast() } // drop the trailing "_" let head = parts.first ?? sessionId let tBits = head.split(separator: "T").map(String.init) let date = tBits.first ?? head let time: String = { guard tBits.count > 1 else { return "" } let b = tBits[1].split(separator: "-") return b.count >= 2 ? "\(b[0]):\(b[1])" : "" }() let when = [date, time].filter { !$0.isEmpty }.joined(separator: " ") // Rejoin with "_" — the faithful inverse of split("_") — so a name that // itself contained underscores survives the round-trip through the folder name. let name = parts.count > 1 ? parts[1...].joined(separator: "_") : "" if name.isEmpty { return "\(appName) call — \(when)".trimmingCharacters(in: .whitespaces) } return "\(name) — \(appName) (\(when))".trimmingCharacters(in: .whitespaces) } }