import Foundation import MacToolsPluginKit enum ActivityBarConstants { static let pluginID = "activity-bar" static let defaultSocketPath = "/tmp/mactools-activity-bar.sock" } enum ActivityBarInputEvent: Equatable, Sendable { case keystroke(app: String) case pointerClick(app: String) case scroll(app: String) case screenTime(app: String, seconds: TimeInterval) } enum ActivityBarInputMonitorStatus: Equatable, Sendable { case idle case running case inputMonitoringDenied } struct ActivityBarAppStats: Codable, Equatable, Sendable { var keystrokes: Int = 0 var pointerClicks: Int = 0 var scrollEvents: Int = 0 var screenTimeSeconds: TimeInterval = 0 var totalInputs: Int { keystrokes + pointerClicks + scrollEvents } } struct ActivityBarDailyStats: Codable, Identifiable, Equatable, Sendable { let date: String var keystrokes: Int = 0 var pointerClicks: Int = 0 var scrollEvents: Int = 0 var screenTimeSeconds: TimeInterval = 0 var perApp: [String: ActivityBarAppStats] = [:] var id: String { date } var totalInputs: Int { keystrokes + pointerClicks + scrollEvents } var topApps: [(name: String, stats: ActivityBarAppStats)] { perApp .filter { !$0.key.isEmpty && $0.key != "loginwindow" } .map { (name: $0.key, stats: $0.value) } .sorted { if $0.stats.screenTimeSeconds == $1.stats.screenTimeSeconds { return $0.stats.totalInputs > $1.stats.totalInputs } return $0.stats.screenTimeSeconds > $1.stats.screenTimeSeconds } } } enum ActivityBarHookEventType: String, Codable, Equatable, Sendable { case userPromptSubmit = "UserPromptSubmit" case preToolUse = "PreToolUse" case postToolUse = "PostToolUse" case stop = "Stop" case subagentStop = "SubagentStop" case preCompact = "PreCompact" case sessionStart = "SessionStart" case sessionEnd = "SessionEnd" case permissionRequest = "PermissionRequest" case unknown init(from decoder: Decoder) throws { let container = try decoder.singleValueContainer() let rawValue = try container.decode(String.self) self = ActivityBarHookEventType(rawValue: rawValue) ?? .unknown } } enum ActivityBarHookStatus: String, Codable, Equatable, Sendable { case processing case compacting case waitingForInput = "waiting_for_input" case ended case runningTool = "running_tool" case unknown init(from decoder: Decoder) throws { let container = try decoder.singleValueContainer() let rawValue = try container.decode(String.self) self = ActivityBarHookStatus(rawValue: rawValue) ?? .unknown } } struct ActivityBarHookEvent: Codable, Equatable, Sendable { let sessionID: String let cwd: String? let event: ActivityBarHookEventType let status: ActivityBarHookStatus let userPrompt: String? let tool: String? let interactive: Bool? enum CodingKeys: String, CodingKey { case sessionID = "session_id" case cwd case event case status case userPrompt = "user_prompt" case tool case interactive } } struct ActivityBarProjectStats: Codable, Equatable, Sendable { var durationSeconds: TimeInterval = 0 var wordCount: Int = 0 var toolCallCount: Int = 0 } enum ActivityBarCodingTool: String, CaseIterable, Codable, Equatable, Sendable { case claudeCode = "Claude Code" case cursor = "Cursor" case codex = "Codex" static func displayName(forSessionID sessionID: String) -> String { if sessionID.hasPrefix("cursor-") { return cursor.rawValue } if sessionID.hasPrefix("codex-") { return codex.rawValue } return claudeCode.rawValue } } struct ActivityBarCodingDailyStats: Codable, Identifiable, Equatable, Sendable { let date: String var durationSeconds: TimeInterval = 0 var wordCount: Int = 0 var toolCallCount: Int = 0 var perProject: [String: ActivityBarProjectStats] = [:] var perTool: [String: ActivityBarProjectStats] = [:] init( date: String, durationSeconds: TimeInterval = 0, wordCount: Int = 0, toolCallCount: Int = 0, perProject: [String: ActivityBarProjectStats] = [:], perTool: [String: ActivityBarProjectStats] = [:] ) { self.date = date self.durationSeconds = durationSeconds self.wordCount = wordCount self.toolCallCount = toolCallCount self.perProject = perProject self.perTool = perTool } var id: String { date } var topProjects: [(name: String, stats: ActivityBarProjectStats)] { perProject .map { (name: $0.key, stats: $0.value) } .sorted { if $0.stats.durationSeconds == $1.stats.durationSeconds { return $0.stats.wordCount > $1.stats.wordCount } return $0.stats.durationSeconds > $1.stats.durationSeconds } } var topTools: [(name: String, stats: ActivityBarProjectStats)] { let knownTools = ActivityBarCodingTool.allCases.map(\.rawValue) let knownRows = knownTools.compactMap { name -> (name: String, stats: ActivityBarProjectStats)? in guard let stats = perTool[name], stats.durationSeconds > 0 || stats.wordCount > 0 || stats.toolCallCount > 0 else { return nil } return (name: name, stats: stats) } let customRows = perTool .filter { !knownTools.contains($0.key) } .map { (name: $0.key, stats: $0.value) } .sorted { if $0.stats.durationSeconds == $1.stats.durationSeconds { return $0.stats.wordCount > $1.stats.wordCount } return $0.stats.durationSeconds > $1.stats.durationSeconds } return knownRows + customRows } enum CodingKeys: String, CodingKey { case date case durationSeconds case wordCount case toolCallCount case perProject case perTool } init(from decoder: Decoder) throws { let container = try decoder.container(keyedBy: CodingKeys.self) date = try container.decode(String.self, forKey: .date) durationSeconds = try container.decodeIfPresent(TimeInterval.self, forKey: .durationSeconds) ?? 0 wordCount = try container.decodeIfPresent(Int.self, forKey: .wordCount) ?? 0 toolCallCount = try container.decodeIfPresent(Int.self, forKey: .toolCallCount) ?? 0 perProject = try container.decodeIfPresent([String: ActivityBarProjectStats].self, forKey: .perProject) ?? [:] perTool = try container.decodeIfPresent([String: ActivityBarProjectStats].self, forKey: .perTool) ?? [:] } } enum ActivityBarFormatting { static func monthDay(_ date: Date, locale: Locale = PluginRuntimeLocalization.locale) -> String { let formatter = DateFormatter() formatter.locale = locale formatter.setLocalizedDateFormatFromTemplate("MMM d") return formatter.string(from: date) } static func decimal(_ value: Int) -> String { let formatter = NumberFormatter() formatter.numberStyle = .decimal formatter.locale = PluginRuntimeLocalization.locale return formatter.string(from: NSNumber(value: value)) ?? "\(value)" } static func count(_ value: Int) -> String { if value >= 10_000 { return "\(value / 1_000)k" } return "\(value)" } static func duration(_ seconds: TimeInterval) -> String { let total = max(Int(seconds), 0) let hours = total / 3_600 let minutes = (total % 3_600) / 60 let secs = total % 60 if hours > 0 { return "\(hours)h \(minutes)m" } if minutes > 0 { return "\(minutes)m \(secs)s" } return "\(secs)s" } }