Skip to content

Commit 1a844c8

Browse files
shepardxiaclaude
andcommitted
refactor: centralize colors + remove debug UI + fix display issues
- Add central_hub/core/colors.py as single source of truth for colors - Add colors section to config.json for Swift to read - Remove debug_ui.py (unused) - Remove pixel_avatar.py (unused legacy) - Fix Swift display line wrapping by disabling text wrap - Remove all cyan colors, use blue instead - Add StateStore observer exception logging - Add reset_socket_server() for test isolation - Add IPC unit tests (19 new tests) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
1 parent f20adb8 commit 1a844c8

File tree

21 files changed

+1310
-1240
lines changed

21 files changed

+1310
-1240
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,3 +13,4 @@ widget/ClaudeStatusOverlay
1313
control-server.py
1414
control-panel.html
1515
docs/
16+
CLAUDE.md

ClarvisWidget/ClarvisWidget

73.2 KB
Binary file not shown.

ClarvisWidget/main.swift

Lines changed: 98 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -3,24 +3,100 @@ import Foundation
33

44
// MARK: - Configuration
55

6+
struct RGBColor: Codable {
7+
var r: CGFloat = 0.5
8+
var g: CGFloat = 0.5
9+
var b: CGFloat = 0.5
10+
11+
func toNSColor() -> NSColor {
12+
return NSColor(red: r, green: g, blue: b, alpha: 1)
13+
}
14+
}
15+
16+
struct StaticConfig: Codable {
17+
var window_width: CGFloat = 280
18+
var window_height: CGFloat = 220
19+
var corner_radius: CGFloat = 24
20+
var bg_alpha: CGFloat = 0.75
21+
var font_size: CGFloat = 14
22+
var border_width: CGFloat = 2
23+
var pulse_speed: Double = 0.1
24+
}
25+
26+
struct WidgetConfigFile: Codable {
27+
var `static`: StaticConfig = StaticConfig()
28+
var colors: [String: RGBColor]? = nil
29+
}
30+
31+
struct WidgetConfig {
32+
var windowWidth: CGFloat = 280
33+
var windowHeight: CGFloat = 220
34+
var cornerRadius: CGFloat = 24
35+
var bgAlpha: CGFloat = 0.75
36+
var fontSize: CGFloat = 14
37+
var borderWidth: CGFloat = 2
38+
var pulseSpeed: Double = 0.1
39+
var statusColors: [String: NSColor] = [:]
40+
41+
// Default colors (used if not in config)
42+
static let defaultColors: [String: NSColor] = [
43+
"idle": NSColor(red: 0.53, green: 0.53, blue: 0.53, alpha: 1),
44+
"resting": NSColor(red: 0.4, green: 0.4, blue: 0.45, alpha: 1),
45+
"thinking": NSColor(red: 1.0, green: 0.87, blue: 0, alpha: 1),
46+
"running": NSColor(red: 0, green: 1.0, blue: 0.67, alpha: 1),
47+
"executing": NSColor(red: 0, green: 1.0, blue: 0.67, alpha: 1),
48+
"awaiting": NSColor(red: 0.4, green: 0.5, blue: 1.0, alpha: 1),
49+
"reading": NSColor(red: 0.4, green: 0.5, blue: 1.0, alpha: 1),
50+
"writing": NSColor(red: 0.4, green: 0.5, blue: 1.0, alpha: 1),
51+
"reviewing": NSColor(red: 1.0, green: 0, blue: 1.0, alpha: 1),
52+
"offline": NSColor(red: 0.53, green: 0.53, blue: 0.53, alpha: 1),
53+
]
54+
55+
static func load() -> WidgetConfig {
56+
let binaryPath = CommandLine.arguments[0]
57+
let projectRoot = (binaryPath as NSString).deletingLastPathComponent + "/.."
58+
let configPath = (projectRoot as NSString).appendingPathComponent("config.json")
59+
60+
guard let data = FileManager.default.contents(atPath: configPath),
61+
let file = try? JSONDecoder().decode(WidgetConfigFile.self, from: data) else {
62+
return WidgetConfig(statusColors: defaultColors)
63+
}
64+
65+
// Convert config colors to NSColor
66+
var colors = defaultColors
67+
if let configColors = file.colors {
68+
for (status, rgb) in configColors {
69+
colors[status] = rgb.toNSColor()
70+
}
71+
}
72+
73+
return WidgetConfig(
74+
windowWidth: file.static.window_width,
75+
windowHeight: file.static.window_height,
76+
cornerRadius: file.static.corner_radius,
77+
bgAlpha: file.static.bg_alpha,
78+
fontSize: file.static.font_size,
79+
borderWidth: file.static.border_width,
80+
pulseSpeed: file.static.pulse_speed,
81+
statusColors: colors
82+
)
83+
}
84+
}
85+
686
struct Config {
787
static let socketPath = "/tmp/clarvis-widget.sock"
888
static let jsonPath = "/tmp/widget-display.json" // Fallback
989
static let pollInterval: TimeInterval = 0.2
10-
static let windowSize = NSSize(width: 280, height: 220)
11-
static let cornerRadius: CGFloat = 24
12-
static let bgAlpha: CGFloat = 0.75
13-
static let fontSize: CGFloat = 14
14-
static let borderWidth: CGFloat = 2
15-
static let pulseSpeed: Double = 0.1
16-
17-
static let statusColors: [String: NSColor] = [
18-
"idle": NSColor(red: 0.53, green: 0.53, blue: 0.6, alpha: 1),
19-
"thinking": NSColor(red: 1.0, green: 0.87, blue: 0, alpha: 1),
20-
"running": NSColor(red: 0, green: 1.0, blue: 0.67, alpha: 1),
21-
"awaiting": NSColor(red: 0, green: 0.8, blue: 1.0, alpha: 1),
22-
"resting": NSColor(red: 0.4, green: 0.4, blue: 0.53, alpha: 1),
23-
]
90+
91+
// Loaded from config file or defaults
92+
static let widgetConfig = WidgetConfig.load()
93+
static var windowSize: NSSize { NSSize(width: widgetConfig.windowWidth, height: widgetConfig.windowHeight) }
94+
static var cornerRadius: CGFloat { widgetConfig.cornerRadius }
95+
static var bgAlpha: CGFloat { widgetConfig.bgAlpha }
96+
static var fontSize: CGFloat { widgetConfig.fontSize }
97+
static var borderWidth: CGFloat { widgetConfig.borderWidth }
98+
static var pulseSpeed: Double { widgetConfig.pulseSpeed }
99+
static var statusColors: [String: NSColor] { widgetConfig.statusColors }
24100
}
25101

26102
// MARK: - Data Model
@@ -235,6 +311,14 @@ class WidgetWindowController: NSWindowController {
235311
textField.font = NSFont.monospacedSystemFont(ofSize: Config.fontSize, weight: .medium)
236312
textField.alignment = .left
237313
textField.stringValue = "Connecting..."
314+
315+
// CRITICAL: Prevent line wrapping which causes display corruption
316+
textField.cell?.wraps = false
317+
textField.cell?.isScrollable = false
318+
textField.lineBreakMode = .byClipping
319+
textField.maximumNumberOfLines = 0 // No limit, but won't wrap
320+
textField.usesSingleLineMode = false // Allow newlines in content
321+
238322
borderView.addSubview(textField)
239323
}
240324

central_hub/core/__init__.py

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,18 @@
1-
"""Core utilities - caching, time services, and state management."""
1+
"""Core utilities - caching, time services, state management, IPC, and colors."""
22

33
from .cache import read_hub_data, write_hub_section, get_hub_section
44
from .time_service import get_current_time, TimeData, DEFAULT_TIMEZONE
55
from .state import StateStore, get_state_store, reset_state_store
6+
from .ipc import DaemonServer, DaemonClient, get_daemon_client
7+
from .colors import (
8+
ColorDef,
9+
Palette,
10+
StatusColors,
11+
STATUS_MAP,
12+
ANSI_COLORS,
13+
STATUS_ANSI,
14+
get_status_colors_for_config,
15+
)
616

717
__all__ = [
818
"read_hub_data",
@@ -14,4 +24,15 @@
1424
"StateStore",
1525
"get_state_store",
1626
"reset_state_store",
27+
"DaemonServer",
28+
"DaemonClient",
29+
"get_daemon_client",
30+
# Colors
31+
"ColorDef",
32+
"Palette",
33+
"StatusColors",
34+
"STATUS_MAP",
35+
"ANSI_COLORS",
36+
"STATUS_ANSI",
37+
"get_status_colors_for_config",
1738
]

central_hub/core/colors.py

Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
"""Centralized color definitions for Clarvis.
2+
3+
Single source of truth for all colors used across Python and Swift.
4+
Colors are defined with both ANSI 256 codes (for terminal) and RGB (for Swift).
5+
"""
6+
7+
from dataclasses import dataclass
8+
from typing import Tuple
9+
10+
11+
@dataclass(frozen=True)
12+
class ColorDef:
13+
"""Color definition with ANSI and RGB values."""
14+
ansi: int # ANSI 256 color code
15+
rgb: Tuple[float, float, float] # RGB values 0.0-1.0 for Swift
16+
17+
@property
18+
def hex(self) -> str:
19+
"""Get hex color string."""
20+
r, g, b = self.rgb
21+
return f"#{int(r*255):02x}{int(g*255):02x}{int(b*255):02x}"
22+
23+
def ansi_fg(self) -> str:
24+
"""Get ANSI foreground escape code."""
25+
return f"\033[38;5;{self.ansi}m"
26+
27+
def ansi_bg(self) -> str:
28+
"""Get ANSI background escape code."""
29+
return f"\033[48;5;{self.ansi}m"
30+
31+
32+
# =============================================================================
33+
# Base Palette
34+
# =============================================================================
35+
36+
class Palette:
37+
"""Base color palette."""
38+
# Neutrals
39+
BLACK = ColorDef(0, (0.0, 0.0, 0.0))
40+
GRAY = ColorDef(8, (0.53, 0.53, 0.53))
41+
DARK_GRAY = ColorDef(8, (0.4, 0.4, 0.45))
42+
WHITE = ColorDef(15, (1.0, 1.0, 1.0))
43+
44+
# Primary colors
45+
YELLOW = ColorDef(11, (1.0, 0.87, 0.0))
46+
GREEN = ColorDef(10, (0.0, 1.0, 0.67))
47+
BLUE = ColorDef(12, (0.4, 0.5, 1.0))
48+
MAGENTA = ColorDef(13, (1.0, 0.0, 1.0))
49+
RED = ColorDef(9, (1.0, 0.33, 0.33))
50+
51+
52+
# =============================================================================
53+
# Status Colors
54+
# =============================================================================
55+
56+
class StatusColors:
57+
"""Maps status names to colors."""
58+
IDLE = Palette.GRAY
59+
RESTING = Palette.DARK_GRAY
60+
THINKING = Palette.YELLOW
61+
RUNNING = Palette.GREEN
62+
EXECUTING = Palette.GREEN
63+
AWAITING = Palette.BLUE
64+
READING = Palette.BLUE
65+
WRITING = Palette.BLUE
66+
REVIEWING = Palette.MAGENTA
67+
OFFLINE = Palette.GRAY
68+
69+
@classmethod
70+
def get(cls, status: str) -> ColorDef:
71+
"""Get color for a status string."""
72+
return STATUS_MAP.get(status, cls.IDLE)
73+
74+
75+
# String-keyed lookup for runtime use
76+
STATUS_MAP: dict[str, ColorDef] = {
77+
"idle": StatusColors.IDLE,
78+
"resting": StatusColors.RESTING,
79+
"thinking": StatusColors.THINKING,
80+
"running": StatusColors.RUNNING,
81+
"executing": StatusColors.EXECUTING,
82+
"awaiting": StatusColors.AWAITING,
83+
"reading": StatusColors.READING,
84+
"writing": StatusColors.WRITING,
85+
"reviewing": StatusColors.REVIEWING,
86+
"offline": StatusColors.OFFLINE,
87+
}
88+
89+
90+
# =============================================================================
91+
# Legacy Compatibility - ANSI codes for existing code
92+
# =============================================================================
93+
94+
# Simple dict of name -> ANSI code (for renderer.py)
95+
ANSI_COLORS = {
96+
"gray": Palette.GRAY.ansi,
97+
"white": Palette.WHITE.ansi,
98+
"yellow": Palette.YELLOW.ansi,
99+
"green": Palette.GREEN.ansi,
100+
"blue": Palette.BLUE.ansi,
101+
"magenta": Palette.MAGENTA.ansi,
102+
}
103+
104+
# Status -> ANSI code (for renderer.py)
105+
STATUS_ANSI = {status: color.ansi for status, color in STATUS_MAP.items()}
106+
107+
108+
# =============================================================================
109+
# Config Export - For Swift/JSON
110+
# =============================================================================
111+
112+
def get_status_colors_for_config() -> dict[str, dict]:
113+
"""Get status colors in format suitable for config.json."""
114+
return {
115+
status: {
116+
"r": color.rgb[0],
117+
"g": color.rgb[1],
118+
"b": color.rgb[2],
119+
}
120+
for status, color in STATUS_MAP.items()
121+
}

0 commit comments

Comments
 (0)