@@ -0,0 +1,396 @@
import SwiftUI
import AppKit
import Darwin
// MARK: - F u n c p i l l ( t o p - r i g h t s l o t o f t h e h o m e v i e w )
//
// S m a l l t o o l s , o n e i c o n e a c h :
// • П и п е т к а — p i c k a n y p i x e l o n s c r e e n ; a n e w b r o v - c h a t t a b e x p l a i n s t h e c o l o u r i n m a n y
// f o r m a t s ( c o m p u t e d l o c a l l y , i n s t a n t ) a n d t h e H E X g o e s t o t h e c l i p b o a r d . T h e c o l o u r i s
// p a s s e d t o c l a u d e a s c o n t e x t f o r f o l l o w - u p q u e s t i o n s i n t h a t t a b .
// • М а к — l i v e c a r d w i t h C P U / c o r e s / R A M / S S D o f t h i s M a c .
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: - E y e d r o p p e r
@ 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 f r e s h b r o v - c h a t t a b ( o r t h e c u r r e n t c h a t o n e w h e n a l l 6 t a b s a r e t a k e n ) .
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 s R G B
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
}
// / R e l a t i v e l u m i n a n c e → w h i c h t e x t c o l o u r r e a d s o n t o p o f i t .
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 ( ) ) ` · ** 0 x : ** ` 0 x \ ( hex . dropFirst ( ) ) `
- ** К о н т р а с т : ** с б е л ы м \ ( String ( format : " %.1f " , contrastWhite ) ) : 1 , с ч ё р н ы м \ ( String ( format : " %.1f " , contrastBlack ) ) : 1 — т е к с т л у ч ш е д е л а т ь \ ( text )
" " "
}
}
// / C S S n a m e d c o l o u r s ( s u b s e t i s e n o u g h t o g i v e a r e c o g n i s a b l e n a m e ) .
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 ) {
// W e i g h t e d R G B d i s t a n c e ( " r e d m e a n " ) , g o o d e n o u g h f o r n a m i n g .
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: - S y s t e m i n f o
struct SystemSnapshot {
var chip = " "
var model = " "
var pCores = 0 , eCores = 0 , logical = 0
var cpuLoad : Double = 0 // 0 … 1 o v e r a l l
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 < Int64 > . 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
}
// / U s e d = w h a t A c t i v i t y M o n i t o r c a l l s " M e m o r y U s e d " : a p p ( a c t i v e + i n a c t i v e − p u r g e a b l e … ≈ i n t e r n a l ) + w i r e d + c o m p r e s s e d .
private static func memory ( ) -> ( UInt64 , UInt64 ) {
let total = ProcessInfo . processInfo . physicalMemory
var stats = vm_statistics64 ( )
var count = mach_msg_type_number_t ( MemoryLayout < vm_statistics64 > . size / MemoryLayout < integer_t > . 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 < host_cpu_load_info > . size / MemoryLayout < integer_t > . 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 )
}
}
}
}