feat(macos): add native menu bar app with notch overlay
Introduces a SwiftUI companion app for the Fail Well PWA: menu bar status item, popover with task list / record / history views, command palette, and a Dynamic Island-style notch overlay that shows the active step and timer beside the hardware notch and expands on hover to reveal pause/next controls.
This commit is contained in:
81
macos/Sources/FailWell/Services/StepParser.swift
Normal file
81
macos/Sources/FailWell/Services/StepParser.swift
Normal file
@@ -0,0 +1,81 @@
|
||||
import CryptoKit
|
||||
import Foundation
|
||||
|
||||
enum StepParser {
|
||||
static func adaptStepsToTextarea(_ steps: [Step]) -> String {
|
||||
steps.map { "- \($0.title) | \($0.estimation)" }.joined(separator: "\n")
|
||||
}
|
||||
|
||||
static func adaptTextareaToSteps(_ value: String) -> [Step] {
|
||||
let lines = value.components(separatedBy: "\n")
|
||||
var result: [(title: String, estimation: Int)] = []
|
||||
var currentParent: (title: String, estimation: Int)?
|
||||
var parentHasChildren = false
|
||||
|
||||
func flushParent() {
|
||||
if let parent = currentParent, !parentHasChildren {
|
||||
result.append(parent)
|
||||
}
|
||||
currentParent = nil
|
||||
parentHasChildren = false
|
||||
}
|
||||
|
||||
for line in lines {
|
||||
let (title, estimation) = extractTitleAndEstimation(from: line)
|
||||
if title.isEmpty { continue }
|
||||
|
||||
if isIndented(line) {
|
||||
if let parent = currentParent, !parent.title.isEmpty {
|
||||
let flattened = "(\(parent.title)) - \(title)"
|
||||
result.append((flattened, estimation))
|
||||
parentHasChildren = true
|
||||
} else {
|
||||
result.append((title, estimation))
|
||||
}
|
||||
} else {
|
||||
flushParent()
|
||||
currentParent = (title, estimation)
|
||||
}
|
||||
}
|
||||
flushParent()
|
||||
|
||||
// Step identity is derived from title only. Re-estimation must preserve identity so
|
||||
// the existing Step Record (keyed by Step id) survives — see CONTEXT.md §"Re-estimation".
|
||||
let bases = result.map { md5($0.title) }
|
||||
return result.enumerated().map { index, item in
|
||||
let base = bases[index]
|
||||
let duplicates = bases[...index].filter { $0 == base }.count
|
||||
return Step(id: "\(base)-\(duplicates)", title: item.title, estimation: item.estimation)
|
||||
}
|
||||
}
|
||||
|
||||
private static func isIndented(_ line: String) -> Bool {
|
||||
guard let first = line.first else { return false }
|
||||
return first == " " || first == "\t"
|
||||
}
|
||||
|
||||
nonisolated(unsafe) private static let stripPrefix = /^-\s*(\[[ x]\]\s*)?/
|
||||
|
||||
private static func extractTitleAndEstimation(from rawStep: String) -> (String, Int) {
|
||||
let trimmed = rawStep.trimmingCharacters(in: .whitespaces)
|
||||
let stripped = trimmed.replacing(stripPrefix, with: "")
|
||||
let parts = stripped.split(separator: "|", maxSplits: 1, omittingEmptySubsequences: false)
|
||||
let title = parts.first.map { $0.trimmingCharacters(in: .whitespaces) } ?? ""
|
||||
let rawEstimation = parts.count > 1 ? String(parts[1]).trimmingCharacters(in: .whitespaces) : ""
|
||||
|
||||
let estimation: Int
|
||||
if rawEstimation.isEmpty {
|
||||
estimation = 0
|
||||
} else if let d = Double(rawEstimation), !d.isNaN {
|
||||
estimation = Int(d.rounded())
|
||||
} else {
|
||||
estimation = 0
|
||||
}
|
||||
return (title, estimation)
|
||||
}
|
||||
|
||||
private static func md5(_ s: String) -> String {
|
||||
let digest = Insecure.MD5.hash(data: Data(s.utf8))
|
||||
return digest.map { String(format: "%02x", $0) }.joined()
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user