Files
failwell/macos/Sources/FailWell/Services/StepParser.swift
Julien Calixte 29a71a73b2 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.
2026-05-15 11:27:51 +02:00

82 lines
3.1 KiB
Swift

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()
}
}