import SwiftUI import AppKit import Darwin // MARK: - Func pill (top-right slot of the home view) // // Small tools, one icon each: // • Пипетка — pick any pixel on screen; a new brov-chat tab explains the colour in many // formats (computed locally, instant) and the HEX goes to the clipboard. The colour is // passed to claude as context for follow-up questions in that tab. // • Мак — live card with CPU / cores / RAM / SSD of this Mac. struct FuncPill: View { @ObservedObject var state: AppState @Binding var showSystem: Bool var body: some View { HStack(spacing: 6) { Text("Func") .font(.system(size: 12, weight: .semibold, design: .monospaced)) .foregroundColor(Color(hex: "#8E939C")) .padding(.leading, 12) Spacer(minLength: 4) FuncButton(symbol: "eyedropper", title: "Пипетка") { ColorPicking.start(state: state) } FuncButton(symbol: "cpu", title: "Мак") { withAnimation(.easeOut(duration: 0.18)) { showSystem.toggle() } } } .padding(.trailing, 6) .frame(maxWidth: .infinity, maxHeight: .infinity) .background(RoundedRectangle(cornerRadius: 14, style: .continuous).fill(Color(hex: "#0E0F11"))) .overlay(RoundedRectangle(cornerRadius: 14, style: .continuous).stroke(Color.white.opacity(0.10), lineWidth: 1)) } } struct FuncButton: View { let symbol: String let title: String let action: () -> Void @State private var hovered = false var body: some View { Button(action: action) { HStack(spacing: 5) { Image(systemName: symbol).font(.system(size: 11, weight: .semibold)) Text(title).font(.system(size: 11, weight: .medium)) } .foregroundColor(Color(hex: hovered ? "#F5F6F8" : "#B0B5BE")) .padding(.horizontal, 9) .frame(height: 26) .background(Capsule().fill(Color.white.opacity(hovered ? 0.12 : 0.06))) .contentShape(Capsule()) } .buttonStyle(.plain) .onHover { hovered = $0 } } } // MARK: - Eyedropper @MainActor enum ColorPicking { private static var sampler: NSColorSampler? static func start(state: AppState) { SoundEngine.shared.play("blip") let s = NSColorSampler() sampler = s s.show { picked in Task { @MainActor in sampler = nil guard let picked, let c = picked.usingColorSpace(.sRGB) else { return } deliver(ColorFormats(c), state: state) } } } private static func deliver(_ f: ColorFormats, state: AppState) { NSPasteboard.general.clearContents() NSPasteboard.general.setString(f.hex, forType: .string) // A fresh brov-chat tab (or the current chat one when all 6 tabs are taken). if state.tabs.count < AppState.maxTabs { state.openTab(.chat) } else if let chat = state.tabs.first(where: { $0.kind == .chat }) { state.activateTab(chat.id) } if let tab = state.activeTabId { ChatTabs.shared.seed(tab, "Пользователь выбрал пипеткой цвет \(f.hex) (rgb \(f.rgbTriple)). Вопросы ниже — про этот цвет.") } state.chatHistory.append(ChatMessage(role: .assistant, content: f.markdown)) NotificationCenter.default.post(name: .hookExpand, object: IslandView.prompt) NotificationCenter.default.post(name: .triggerEmote, object: BotEmote.surprised) } } struct ColorFormats { let r: Double, g: Double, b: Double // 0…1 sRGB init(_ c: NSColor) { r = Double(c.redComponent); g = Double(c.greenComponent); b = Double(c.blueComponent) } var R: Int { Int((r * 255).rounded()) } var G: Int { Int((g * 255).rounded()) } var B: Int { Int((b * 255).rounded()) } var hex: String { String(format: "#%02X%02X%02X", R, G, B) } var rgbTriple: String { "\(R), \(G), \(B)" } var hsl: (h: Int, s: Int, l: Int) { let mx = max(r, g, b), mn = min(r, g, b), l = (mx + mn) / 2 guard mx != mn else { return (0, 0, Int((l * 100).rounded())) } let d = mx - mn let s = l > 0.5 ? d / (2 - mx - mn) : d / (mx + mn) return (hue(mx: mx, d: d), Int((s * 100).rounded()), Int((l * 100).rounded())) } var hsb: (h: Int, s: Int, b: Int) { let mx = max(r, g, b), mn = min(r, g, b), d = mx - mn let s = mx == 0 ? 0 : d / mx return (d == 0 ? 0 : hue(mx: mx, d: d), Int((s * 100).rounded()), Int((mx * 100).rounded())) } var cmyk: (c: Int, m: Int, y: Int, k: Int) { let k = 1 - max(r, g, b) guard k < 1 else { return (0, 0, 0, 100) } func p(_ v: Double) -> Int { Int(((1 - v - k) / (1 - k) * 100).rounded()) } return (p(r), p(g), p(b), Int((k * 100).rounded())) } private func hue(mx: Double, d: Double) -> Int { var h: Double if mx == r { h = (g - b) / d + (g < b ? 6 : 0) } else if mx == g { h = (b - r) / d + 2 } else { h = (r - g) / d + 4 } h *= 60 return Int(h.rounded()) % 360 } /// Relative luminance → which text colour reads on top of it. var luminance: Double { func lin(_ v: Double) -> Double { v <= 0.03928 ? v / 12.92 : pow((v + 0.055) / 1.055, 2.4) } return 0.2126 * lin(r) + 0.7152 * lin(g) + 0.0722 * lin(b) } var contrastWhite: Double { 1.05 / (luminance + 0.05) } var contrastBlack: Double { (luminance + 0.05) / 0.05 } var nearestName: (en: String, ru: String) { ColorNames.nearest(R, G, B) } var markdown: String { let (h1, s1, l1) = hsl, (h2, s2, b2) = hsb, (c, m, y, k) = cmyk let name = nearestName let text = contrastWhite >= contrastBlack ? "белый" : "чёрный" return """ **\(hex)** — \(name.ru) (ближе всего к `\(name.en)`). HEX уже в буфере обмена. - **HEX:** `\(hex)` · без решётки `\(hex.dropFirst())` · с альфой `\(hex)FF` - **RGB:** `rgb(\(R), \(G), \(B))` · дроби `\(String(format: "%.3f, %.3f, %.3f", r, g, b))` - **HSL:** `hsl(\(h1), \(s1)%, \(l1)%)` - **HSB/HSV:** `\(h2)°, \(s2)%, \(b2)%` - **CMYK:** `\(c)%, \(m)%, \(y)%, \(k)%` - **SwiftUI:** `Color(red: \(String(format: "%.3f", r)), green: \(String(format: "%.3f", g)), blue: \(String(format: "%.3f", b)))` - **NSColor/UIColor:** `NSColor(srgbRed: \(String(format: "%.3f", r)), green: \(String(format: "%.3f", g)), blue: \(String(format: "%.3f", b)), alpha: 1)` - **Android:** `0xFF\(hex.dropFirst())` · **0x:** `0x\(hex.dropFirst())` - **Контраст:** с белым \(String(format: "%.1f", contrastWhite)):1, с чёрным \(String(format: "%.1f", contrastBlack)):1 — текст лучше делать \(text) """ } } /// CSS named colours (subset is enough to give a recognisable name). enum ColorNames { static let table: [(String, String, Int, Int, Int)] = [ ("black", "чёрный", 0, 0, 0), ("white", "белый", 255, 255, 255), ("gray", "серый", 128, 128, 128), ("silver", "серебристый", 192, 192, 192), ("dimgray", "тёмно-серый", 105, 105, 105), ("gainsboro", "светло-серый", 220, 220, 220), ("red", "красный", 255, 0, 0), ("darkred", "тёмно-красный", 139, 0, 0), ("crimson", "малиновый", 220, 20, 60), ("firebrick", "кирпичный", 178, 34, 34), ("salmon", "лососевый", 250, 128, 114), ("tomato", "томатный", 255, 99, 71), ("coral", "коралловый", 255, 127, 80), ("orangered", "красно-оранжевый", 255, 69, 0), ("orange", "оранжевый", 255, 165, 0), ("darkorange", "тёмно-оранжевый", 255, 140, 0), ("gold", "золотой", 255, 215, 0), ("yellow", "жёлтый", 255, 255, 0), ("khaki", "хаки", 240, 230, 140), ("beige", "бежевый", 245, 245, 220), ("wheat", "пшеничный", 245, 222, 179), ("tan", "песочный", 210, 180, 140), ("peru", "перу (коричнево-рыжий)", 205, 133, 63), ("chocolate", "шоколадный", 210, 105, 30), ("sienna", "сиена", 160, 82, 45), ("saddlebrown", "коричневый", 139, 69, 19), ("maroon", "бордовый", 128, 0, 0), ("olive", "оливковый", 128, 128, 0), ("yellowgreen", "жёлто-зелёный", 154, 205, 50), ("lime", "лаймовый", 0, 255, 0), ("limegreen", "лаймово-зелёный", 50, 205, 50), ("green", "зелёный", 0, 128, 0), ("darkgreen", "тёмно-зелёный", 0, 100, 0), ("forestgreen", "лесной зелёный", 34, 139, 34), ("seagreen", "морской зелёный", 46, 139, 87), ("mediumseagreen", "изумрудный", 60, 179, 113), ("springgreen", "весенний зелёный", 0, 255, 127), ("teal", "бирюзово-зелёный (teal)", 0, 128, 128), ("turquoise", "бирюзовый", 64, 224, 208), ("cyan", "голубой (циан)", 0, 255, 255), ("lightblue", "светло-голубой", 173, 216, 230), ("skyblue", "небесный", 135, 206, 235), ("deepskyblue", "ярко-голубой", 0, 191, 255), ("dodgerblue", "синий доджер", 30, 144, 255), ("steelblue", "стальной синий", 70, 130, 180), ("royalblue", "королевский синий", 65, 105, 225), ("blue", "синий", 0, 0, 255), ("mediumblue", "средне-синий", 0, 0, 205), ("navy", "тёмно-синий (navy)", 0, 0, 128), ("midnightblue", "полуночно-синий", 25, 25, 112), ("slateblue", "сланцево-синий", 106, 90, 205), ("indigo", "индиго", 75, 0, 130), ("purple", "фиолетовый", 128, 0, 128), ("darkviolet", "тёмно-фиолетовый", 148, 0, 211), ("mediumpurple", "лавандово-фиолетовый", 147, 112, 219), ("orchid", "орхидея", 218, 112, 214), ("violet", "фиалковый", 238, 130, 238), ("plum", "сливовый", 221, 160, 221), ("lavender", "лавандовый", 230, 230, 250), ("magenta", "пурпурный (маджента)", 255, 0, 255), ("deeppink", "ярко-розовый", 255, 20, 147), ("hotpink", "горячий розовый", 255, 105, 180), ("pink", "розовый", 255, 192, 203), ("lightpink", "светло-розовый", 255, 182, 193), ("mistyrose", "туманно-розовый", 255, 228, 225), ("ivory", "слоновая кость", 255, 255, 240), ("linen", "льняной", 250, 240, 230), ("slategray", "сланцево-серый", 112, 128, 144), ("darkslategray", "тёмный сланец", 47, 79, 79), ("cadetblue", "кадетский синий", 95, 158, 160), ("aquamarine", "аквамарин", 127, 255, 212), ] static func nearest(_ r: Int, _ g: Int, _ b: Int) -> (en: String, ru: String) { // Weighted RGB distance ("redmean"), good enough for naming. var best = table[0], bestD = Double.infinity for c in table { let rm = Double(r + c.2) / 2 let dr = Double(r - c.2), dg = Double(g - c.3), db = Double(b - c.4) let d = (2 + rm / 256) * dr * dr + 4 * dg * dg + (2 + (255 - rm) / 256) * db * db if d < bestD { bestD = d; best = c } } return (best.0, best.1) } } // MARK: - System info struct SystemSnapshot { var chip = "" var model = "" var pCores = 0, eCores = 0, logical = 0 var cpuLoad: Double = 0 // 0…1 overall var ramTotal: UInt64 = 0, ramUsed: UInt64 = 0 var diskTotal: Int64 = 0, diskFree: Int64 = 0 var uptime: TimeInterval = 0 var osVersion = "" } @MainActor final class SystemMonitor: ObservableObject { static let shared = SystemMonitor() @Published var snap = SystemSnapshot() private var timer: Timer? private var prevTicks: (user: UInt64, sys: UInt64, idle: UInt64, nice: UInt64)? func start() { guard timer == nil else { return } snap = Self.staticInfo() refresh() timer = Timer.scheduledTimer(withTimeInterval: 2, repeats: true) { _ in Task { @MainActor in SystemMonitor.shared.refresh() } } } func stop() { timer?.invalidate(); timer = nil; prevTicks = nil } private func refresh() { var s = snap s.cpuLoad = cpuLoad() ?? s.cpuLoad (s.ramTotal, s.ramUsed) = Self.memory() if let v = try? URL(fileURLWithPath: "/").resourceValues(forKeys: [.volumeTotalCapacityKey, .volumeAvailableCapacityForImportantUsageKey]) { s.diskTotal = Int64(v.volumeTotalCapacity ?? 0) s.diskFree = v.volumeAvailableCapacityForImportantUsage ?? 0 } s.uptime = ProcessInfo.processInfo.systemUptime snap = s } private static func sysctlString(_ name: String) -> String { var size = 0 sysctlbyname(name, nil, &size, nil, 0) guard size > 0 else { return "" } var buf = [CChar](repeating: 0, count: size) sysctlbyname(name, &buf, &size, nil, 0) return String(cString: buf) } private static func sysctlInt(_ name: String) -> Int { var v: Int64 = 0 var size = MemoryLayout.size return sysctlbyname(name, &v, &size, nil, 0) == 0 ? Int(v) : 0 } private static func staticInfo() -> SystemSnapshot { var s = SystemSnapshot() s.chip = sysctlString("machdep.cpu.brand_string") s.model = sysctlString("hw.model") s.pCores = sysctlInt("hw.perflevel0.physicalcpu") s.eCores = sysctlInt("hw.perflevel1.physicalcpu") s.logical = sysctlInt("hw.logicalcpu") let v = ProcessInfo.processInfo.operatingSystemVersion s.osVersion = "macOS \(v.majorVersion).\(v.minorVersion).\(v.patchVersion)" return s } /// Used = what Activity Monitor calls "Memory Used": app (active+inactive−purgeable… ≈ internal) + wired + compressed. private static func memory() -> (UInt64, UInt64) { let total = ProcessInfo.processInfo.physicalMemory var stats = vm_statistics64() var count = mach_msg_type_number_t(MemoryLayout.size / MemoryLayout.size) let kr = withUnsafeMutablePointer(to: &stats) { $0.withMemoryRebound(to: integer_t.self, capacity: Int(count)) { host_statistics64(mach_host_self(), HOST_VM_INFO64, $0, &count) } } guard kr == KERN_SUCCESS else { return (total, 0) } let page = UInt64(getpagesize()) let app = UInt64(stats.internal_page_count) - UInt64(stats.purgeable_count) let used = (app + UInt64(stats.wire_count) + UInt64(stats.compressor_page_count)) * page return (total, min(total, used)) } private func cpuLoad() -> Double? { var info = host_cpu_load_info() var count = mach_msg_type_number_t(MemoryLayout.size / MemoryLayout.size) let kr = withUnsafeMutablePointer(to: &info) { $0.withMemoryRebound(to: integer_t.self, capacity: Int(count)) { host_statistics(mach_host_self(), HOST_CPU_LOAD_INFO, $0, &count) } } guard kr == KERN_SUCCESS else { return nil } let t = (user: UInt64(info.cpu_ticks.0), sys: UInt64(info.cpu_ticks.1), idle: UInt64(info.cpu_ticks.2), nice: UInt64(info.cpu_ticks.3)) defer { prevTicks = t } guard let p = prevTicks else { return nil } let busy = Double((t.user - p.user) + (t.sys - p.sys) + (t.nice - p.nice)) let all = busy + Double(t.idle - p.idle) return all > 0 ? busy / all : nil } } struct SystemCard: View { @ObservedObject private var mon = SystemMonitor.shared let onClose: () -> Void private func gb(_ b: Int64) -> String { String(format: "%.0f ГБ", Double(b) / 1_000_000_000) } private func gib(_ b: UInt64) -> String { String(format: "%.1f ГБ", Double(b) / 1_073_741_824) } var body: some View { let s = mon.snap let diskUsed = max(0, s.diskTotal - s.diskFree) ZStack(alignment: .topTrailing) { CardBackground(wash: nil) VStack(alignment: .leading, spacing: 7) { HStack(spacing: 6) { Text(s.chip.isEmpty ? "Mac" : s.chip).font(.system(size: 12.5, weight: .semibold)) Text("· \(s.model) · \(s.osVersion)").font(.system(size: 10.5)).foregroundColor(Color(hex: "#8E939C")) } SysRow(title: "CPU", value: "\(Int((s.cpuLoad * 100).rounded()))% · ядер \(s.pCores) произв. + \(s.eCores) эфф. (\(s.logical) потоков)", fraction: s.cpuLoad) SysRow(title: "ОЗУ", value: "занято \(gib(s.ramUsed)) из \(gib(s.ramTotal)) · свободно \(gib(s.ramTotal - s.ramUsed))", fraction: s.ramTotal > 0 ? Double(s.ramUsed) / Double(s.ramTotal) : 0) SysRow(title: "SSD", value: "занято \(gb(diskUsed)) из \(gb(s.diskTotal)) · свободно \(gb(s.diskFree))", fraction: s.diskTotal > 0 ? Double(diskUsed) / Double(s.diskTotal) : 0) } .padding(.leading, 104) .padding(.trailing, 36) .padding(.vertical, 8) .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .leading) Button(action: onClose) { Image(systemName: "xmark").font(.system(size: 8, weight: .bold)) .foregroundColor(Color(hex: "#8E939C")) .frame(width: 18, height: 18) .background(Circle().fill(Color.white.opacity(0.08))) } .buttonStyle(.plain) .padding(10) } .onAppear { SystemMonitor.shared.start() } .onDisappear { SystemMonitor.shared.stop() } } } private struct SysRow: View { let title: String let value: String let fraction: Double var body: some View { HStack(spacing: 8) { Text(title).font(.system(size: 10.5, weight: .bold, design: .monospaced)) .foregroundColor(Color(hex: "#8E939C")).frame(width: 30, alignment: .leading) VStack(alignment: .leading, spacing: 3) { Text(value).font(.system(size: 11)).lineLimit(1).minimumScaleFactor(0.8) GeometryReader { g in ZStack(alignment: .leading) { Capsule().fill(Color.white.opacity(0.08)) Capsule().fill(Color(hex: ClaudePlanGauge.color(for: fraction * 100))) .frame(width: max(3, g.size.width * min(1, fraction))) } } .frame(height: 4) } } } }