Skip to content

Commit a967b0e

Browse files
shepardxiaclaude
andcommitted
chore: project maintenance and element system
Maintenance: - Update .gitignore (organize, add .serena/, logs/) - Export new managers from core/__init__.py - Configure pytest-asyncio mode in pyproject.toml - Remove obsolete canvas.py and test_canvas.py New element system: - Add archetypes/ for complex visual behaviors (face, weather, progress) - Add elements/ with 76 YAML definitions for animations, borders, eyes, mouths, particles, substrates, and weather effects - Simplify renderer.py (-862 lines) using new element system Widget: - Update binary (324KB -> 306KB) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
1 parent f791df7 commit a967b0e

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

93 files changed

+1754
-1901
lines changed

.gitignore

Lines changed: 21 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,35 @@
1+
# Virtual environment
12
.venv/
23
.env
4+
5+
# Python
36
__pycache__/
47
*.egg-info/
5-
.DS_Store
68
*.pyc
7-
docs/api/
8-
uv.lock
9+
10+
# macOS
11+
.DS_Store
12+
13+
# IDE/Tools
14+
.serena/
915
.hypothesis/
1016
.pytest_cache/
1117
.coverage
1218
htmlcov/
19+
20+
# Logs
21+
logs/
22+
23+
# Lock files (regenerated by uv)
24+
uv.lock
25+
26+
# Legacy/unused
1327
widget/ClaudeStatusOverlay
1428
control-server.py
1529
control-panel.html
30+
31+
# Generated docs
1632
docs/
33+
34+
# Project instructions (user-specific)
1735
CLAUDE.md

ClarvisWidget/ClarvisWidget

-18.3 KB
Binary file not shown.

clarvis/archetypes/__init__.py

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
"""
2+
Archetypes - Python classes for complex visual behaviors.
3+
4+
Archetypes consume element definitions from YAML but add runtime logic
5+
like physics simulation, animation state machines, etc.
6+
"""
7+
8+
from .base import Archetype, Renderable
9+
from .face import FaceArchetype
10+
from .weather import WeatherArchetype
11+
from .progress import ProgressArchetype
12+
13+
__all__ = [
14+
"Archetype",
15+
"Renderable",
16+
"FaceArchetype",
17+
"WeatherArchetype",
18+
"ProgressArchetype",
19+
]

clarvis/archetypes/base.py

Lines changed: 153 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,153 @@
1+
"""
2+
Base protocols and classes for archetypes.
3+
"""
4+
5+
from abc import ABC, abstractmethod
6+
from typing import Protocol, Any, Optional
7+
8+
from ..widget.pipeline import Layer
9+
from ..elements.registry import ElementRegistry
10+
11+
12+
class Renderable(Protocol):
13+
"""Protocol for anything that can render to a layer."""
14+
15+
def render(self, layer: Layer, x: int, y: int, color: int) -> None:
16+
"""Render this element to a layer at position."""
17+
...
18+
19+
20+
class Archetype(ABC):
21+
"""
22+
Base class for complex visual behaviors.
23+
24+
Archetypes load configuration and element definitions from the registry,
25+
then provide runtime behavior (animation, physics, state machines).
26+
27+
Subclasses must implement:
28+
- render(): Draw to a layer
29+
- _on_element_change(): Handle hot-reload notifications
30+
"""
31+
32+
def __init__(self, registry: ElementRegistry, config_name: str):
33+
"""
34+
Initialize archetype with registry and config name.
35+
36+
Args:
37+
registry: Element registry for loading definitions
38+
config_name: Name of archetype config in elements/archetypes/
39+
"""
40+
self.registry = registry
41+
self.config_name = config_name
42+
self._load_config()
43+
registry.on_change(self._handle_change)
44+
45+
def _load_config(self) -> None:
46+
"""Load archetype configuration from registry."""
47+
self.config = self.registry.get('archetypes', self.config_name) or {}
48+
49+
def _handle_change(self, kind: str, name: str) -> None:
50+
"""Handle element change notification."""
51+
if kind == 'archetypes' and name == self.config_name:
52+
self._load_config()
53+
self._on_element_change(kind, name)
54+
55+
@abstractmethod
56+
def _on_element_change(self, kind: str, name: str) -> None:
57+
"""
58+
Handle element change notification.
59+
60+
Called when any element in the registry changes.
61+
Subclasses should check if the change is relevant and rebuild caches.
62+
"""
63+
...
64+
65+
@abstractmethod
66+
def render(self, layer: Layer, **kwargs) -> None:
67+
"""Render this archetype to a layer."""
68+
...
69+
70+
def tick(self) -> None:
71+
"""Advance animation/simulation state. Override if needed."""
72+
pass
73+
74+
75+
class SimpleElement:
76+
"""
77+
A simple renderable element loaded from YAML.
78+
79+
Renders a single character or pattern at a position.
80+
"""
81+
82+
def __init__(self, definition: dict):
83+
"""
84+
Initialize from element definition dict.
85+
86+
Expected keys:
87+
- char: Single character to render
88+
- pattern: Multi-line pattern string (alternative to char)
89+
- position: Optional [l, g, r] for eye positioning
90+
"""
91+
self.char = definition.get('char', ' ')
92+
self.pattern = definition.get('pattern')
93+
self.position = definition.get('position', [0, 0, 0])
94+
95+
# Parse multi-line pattern if present
96+
if self.pattern and '\n' in str(self.pattern):
97+
self.lines = [line for line in str(self.pattern).split('\n') if line]
98+
else:
99+
self.lines = [self.pattern] if self.pattern else [self.char]
100+
101+
@property
102+
def width(self) -> int:
103+
return max(len(line) for line in self.lines) if self.lines else 1
104+
105+
@property
106+
def height(self) -> int:
107+
return len(self.lines)
108+
109+
def render(self, layer: Layer, x: int, y: int, color: int) -> None:
110+
"""Render element to layer."""
111+
for dy, line in enumerate(self.lines):
112+
for dx, char in enumerate(line):
113+
if char != ' ':
114+
layer.put(x + dx, y + dy, char, color)
115+
116+
117+
class Composite:
118+
"""
119+
A composite element that renders multiple children.
120+
121+
Children are positioned relative to the composite's origin.
122+
"""
123+
124+
def __init__(self, definition: dict, registry: ElementRegistry):
125+
"""
126+
Initialize from composite definition dict.
127+
128+
Expected keys:
129+
- children: Dict mapping names to {element: "kind/name", position: [x, y]}
130+
- width: Optional composite width
131+
- height: Optional composite height
132+
"""
133+
self.registry = registry
134+
self.width = definition.get('width', 0)
135+
self.height = definition.get('height', 0)
136+
self.children: dict[str, dict] = {}
137+
138+
for name, child_def in definition.get('children', {}).items():
139+
element_path = child_def.get('element', '')
140+
if '/' in element_path:
141+
kind, elem_name = element_path.split('/', 1)
142+
elem_def = registry.get(kind, elem_name)
143+
if elem_def:
144+
self.children[name] = {
145+
'element': SimpleElement(elem_def),
146+
'position': child_def.get('position', [0, 0])
147+
}
148+
149+
def render(self, layer: Layer, x: int, y: int, color: int) -> None:
150+
"""Render all children to layer."""
151+
for name, child in self.children.items():
152+
cx, cy = child['position']
153+
child['element'].render(layer, x + cx, y + cy, color)

0 commit comments

Comments
 (0)