gitgotchi 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- gitgotchi-0.1.0.dist-info/METADATA +78 -0
- gitgotchi-0.1.0.dist-info/RECORD +23 -0
- gitgotchi-0.1.0.dist-info/WHEEL +5 -0
- gitgotchi-0.1.0.dist-info/entry_points.txt +2 -0
- gitgotchi-0.1.0.dist-info/licenses/LICENSE +21 -0
- gitgotchi-0.1.0.dist-info/top_level.txt +1 -0
- src/__init__.py +13 -0
- src/cli.py +182 -0
- src/db/__init__.py +1 -0
- src/db/migrations.py +100 -0
- src/db/models.py +92 -0
- src/db/state_manager.py +277 -0
- src/hooks/__init__.py +1 -0
- src/hooks/installer.py +128 -0
- src/hooks/post_commit.py +70 -0
- src/pet/__init__.py +1 -0
- src/pet/evolution.py +5 -0
- src/pet/renderer.py +208 -0
- src/pet/states.py +116 -0
- src/seance/__init__.py +1 -0
- src/seance/git_analyzer.py +5 -0
- src/seance/story_gen.py +177 -0
- src/seance/templates.py +3 -0
src/db/state_manager.py
ADDED
|
@@ -0,0 +1,277 @@
|
|
|
1
|
+
"""State management for pet persistence."""
|
|
2
|
+
from datetime import datetime
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
from typing import Optional
|
|
5
|
+
|
|
6
|
+
from sqlalchemy.orm import Session
|
|
7
|
+
|
|
8
|
+
from src.db.models import (
|
|
9
|
+
CommitHistory,
|
|
10
|
+
PetState,
|
|
11
|
+
StoryMemory,
|
|
12
|
+
get_engine,
|
|
13
|
+
init_db,
|
|
14
|
+
)
|
|
15
|
+
from src.pet.states import PetForm, PetMood, PetStats
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class StateManager:
|
|
19
|
+
"""Manages pet state persistence and retrieval."""
|
|
20
|
+
|
|
21
|
+
def __init__(self, db_path: str = ".gitgotchi/state.db") -> None:
|
|
22
|
+
"""Initialize state manager.
|
|
23
|
+
|
|
24
|
+
Args:
|
|
25
|
+
db_path: Path to SQLite database file
|
|
26
|
+
"""
|
|
27
|
+
self.db_path = db_path
|
|
28
|
+
self._ensure_db_exists()
|
|
29
|
+
self.engine = get_engine(db_path)
|
|
30
|
+
|
|
31
|
+
def _ensure_db_exists(self) -> None:
|
|
32
|
+
"""Ensure database directory and schema exist."""
|
|
33
|
+
db_dir = Path(self.db_path).parent
|
|
34
|
+
db_dir.mkdir(parents=True, exist_ok=True)
|
|
35
|
+
init_db(self.db_path)
|
|
36
|
+
|
|
37
|
+
def get_pet_state(self) -> PetStats:
|
|
38
|
+
"""Load current pet state from database.
|
|
39
|
+
|
|
40
|
+
Returns:
|
|
41
|
+
PetStats instance with current state
|
|
42
|
+
"""
|
|
43
|
+
with Session(self.engine) as session:
|
|
44
|
+
pet_state = session.query(PetState).first()
|
|
45
|
+
|
|
46
|
+
if not pet_state:
|
|
47
|
+
# Create initial state
|
|
48
|
+
pet_state = PetState()
|
|
49
|
+
session.add(pet_state)
|
|
50
|
+
session.commit()
|
|
51
|
+
|
|
52
|
+
# Convert to PetStats
|
|
53
|
+
stats = PetStats()
|
|
54
|
+
stats.pet_name = pet_state.pet_name
|
|
55
|
+
stats.total_commits = pet_state.total_commits
|
|
56
|
+
stats.lines_added = pet_state.lines_added
|
|
57
|
+
stats.lines_deleted = pet_state.lines_deleted
|
|
58
|
+
stats.merge_conflicts = pet_state.merge_conflicts
|
|
59
|
+
stats.reverts = pet_state.reverts
|
|
60
|
+
stats.quality_score = pet_state.quality_score
|
|
61
|
+
stats.evolution_points = pet_state.evolution_points
|
|
62
|
+
stats.friend_level = pet_state.friend_level
|
|
63
|
+
stats.last_fed = pet_state.last_fed
|
|
64
|
+
stats.current_mood = PetMood(pet_state.current_mood)
|
|
65
|
+
stats.current_form = PetForm(pet_state.current_form)
|
|
66
|
+
|
|
67
|
+
return stats
|
|
68
|
+
|
|
69
|
+
def save_pet_state(self, stats: PetStats) -> None:
|
|
70
|
+
"""Save pet state to database.
|
|
71
|
+
|
|
72
|
+
Args:
|
|
73
|
+
stats: PetStats instance to persist
|
|
74
|
+
"""
|
|
75
|
+
with Session(self.engine) as session:
|
|
76
|
+
pet_state = session.query(PetState).first()
|
|
77
|
+
|
|
78
|
+
if not pet_state:
|
|
79
|
+
pet_state = PetState()
|
|
80
|
+
session.add(pet_state)
|
|
81
|
+
|
|
82
|
+
pet_state.pet_name = stats.pet_name
|
|
83
|
+
pet_state.total_commits = stats.total_commits
|
|
84
|
+
pet_state.lines_added = stats.lines_added
|
|
85
|
+
pet_state.lines_deleted = stats.lines_deleted
|
|
86
|
+
pet_state.merge_conflicts = stats.merge_conflicts
|
|
87
|
+
pet_state.reverts = stats.reverts
|
|
88
|
+
pet_state.quality_score = stats.quality_score
|
|
89
|
+
pet_state.evolution_points = stats.evolution_points
|
|
90
|
+
pet_state.friend_level = stats.friend_level
|
|
91
|
+
pet_state.last_fed = stats.last_fed
|
|
92
|
+
pet_state.current_mood = stats.current_mood.value
|
|
93
|
+
pet_state.current_form = stats.current_form.value
|
|
94
|
+
pet_state.updated_at = datetime.now()
|
|
95
|
+
|
|
96
|
+
session.commit()
|
|
97
|
+
|
|
98
|
+
def log_commit(
|
|
99
|
+
self,
|
|
100
|
+
commit_hash: str,
|
|
101
|
+
author: str,
|
|
102
|
+
message: str,
|
|
103
|
+
timestamp: datetime,
|
|
104
|
+
lines_added: int = 0,
|
|
105
|
+
lines_deleted: int = 0,
|
|
106
|
+
files_changed: int = 0,
|
|
107
|
+
is_merge: bool = False,
|
|
108
|
+
is_revert: bool = False,
|
|
109
|
+
quality_impact: float = 0.0,
|
|
110
|
+
) -> None:
|
|
111
|
+
"""Log a commit to history.
|
|
112
|
+
|
|
113
|
+
Args:
|
|
114
|
+
commit_hash: Git commit SHA
|
|
115
|
+
author: Commit author
|
|
116
|
+
message: Commit message
|
|
117
|
+
timestamp: Commit timestamp
|
|
118
|
+
lines_added: Lines added in commit
|
|
119
|
+
lines_deleted: Lines deleted in commit
|
|
120
|
+
files_changed: Number of files changed
|
|
121
|
+
is_merge: Whether this is a merge commit
|
|
122
|
+
is_revert: Whether this is a revert commit
|
|
123
|
+
quality_impact: Impact on quality score
|
|
124
|
+
"""
|
|
125
|
+
with Session(self.engine) as session:
|
|
126
|
+
# Check if already logged
|
|
127
|
+
existing = (
|
|
128
|
+
session.query(CommitHistory)
|
|
129
|
+
.filter_by(commit_hash=commit_hash)
|
|
130
|
+
.first()
|
|
131
|
+
)
|
|
132
|
+
|
|
133
|
+
if existing:
|
|
134
|
+
return
|
|
135
|
+
|
|
136
|
+
commit = CommitHistory(
|
|
137
|
+
commit_hash=commit_hash,
|
|
138
|
+
author=author,
|
|
139
|
+
message=message,
|
|
140
|
+
timestamp=timestamp,
|
|
141
|
+
lines_added=lines_added,
|
|
142
|
+
lines_deleted=lines_deleted,
|
|
143
|
+
files_changed=files_changed,
|
|
144
|
+
is_merge=is_merge,
|
|
145
|
+
is_revert=is_revert,
|
|
146
|
+
quality_impact=quality_impact,
|
|
147
|
+
)
|
|
148
|
+
session.add(commit)
|
|
149
|
+
session.commit()
|
|
150
|
+
|
|
151
|
+
def save_story(
|
|
152
|
+
self,
|
|
153
|
+
commit_hash: str,
|
|
154
|
+
story_text: str,
|
|
155
|
+
story_type: str,
|
|
156
|
+
pet_form: PetForm,
|
|
157
|
+
pet_mood: PetMood,
|
|
158
|
+
tokens_used: Optional[int] = None,
|
|
159
|
+
) -> None:
|
|
160
|
+
"""Save generated story to cache.
|
|
161
|
+
|
|
162
|
+
Args:
|
|
163
|
+
commit_hash: Associated commit hash
|
|
164
|
+
story_text: Generated story content
|
|
165
|
+
story_type: Type of story
|
|
166
|
+
pet_form: Pet form at time of generation
|
|
167
|
+
pet_mood: Pet mood at time of generation
|
|
168
|
+
tokens_used: Number of tokens used
|
|
169
|
+
"""
|
|
170
|
+
with Session(self.engine) as session:
|
|
171
|
+
story = StoryMemory(
|
|
172
|
+
commit_hash=commit_hash,
|
|
173
|
+
story_text=story_text,
|
|
174
|
+
story_type=story_type,
|
|
175
|
+
pet_form_at_time=pet_form.value,
|
|
176
|
+
pet_mood_at_time=pet_mood.value,
|
|
177
|
+
tokens_used=tokens_used,
|
|
178
|
+
)
|
|
179
|
+
session.add(story)
|
|
180
|
+
session.commit()
|
|
181
|
+
|
|
182
|
+
def get_story(self, commit_hash: str) -> Optional[str]:
|
|
183
|
+
"""Retrieve cached story for a commit.
|
|
184
|
+
|
|
185
|
+
Args:
|
|
186
|
+
commit_hash: Git commit SHA
|
|
187
|
+
|
|
188
|
+
Returns:
|
|
189
|
+
Story text if cached, None otherwise
|
|
190
|
+
"""
|
|
191
|
+
with Session(self.engine) as session:
|
|
192
|
+
story = (
|
|
193
|
+
session.query(StoryMemory)
|
|
194
|
+
.filter_by(commit_hash=commit_hash)
|
|
195
|
+
.first()
|
|
196
|
+
)
|
|
197
|
+
return story.story_text if story else None
|
|
198
|
+
|
|
199
|
+
def process_commit_event(
|
|
200
|
+
self,
|
|
201
|
+
commit_hash: str,
|
|
202
|
+
author: str,
|
|
203
|
+
message: str,
|
|
204
|
+
timestamp: datetime,
|
|
205
|
+
lines_added: int = 0,
|
|
206
|
+
lines_deleted: int = 0,
|
|
207
|
+
files_changed: int = 0,
|
|
208
|
+
is_merge: bool = False,
|
|
209
|
+
is_revert: bool = False,
|
|
210
|
+
) -> PetStats:
|
|
211
|
+
"""Process a commit event and update pet state.
|
|
212
|
+
|
|
213
|
+
Args:
|
|
214
|
+
commit_hash: Git commit SHA
|
|
215
|
+
author: Commit author
|
|
216
|
+
message: Commit message
|
|
217
|
+
timestamp: Commit timestamp
|
|
218
|
+
lines_added: Lines added in commit
|
|
219
|
+
lines_deleted: Lines deleted in commit
|
|
220
|
+
files_changed: Number of files changed
|
|
221
|
+
is_merge: Whether this is a merge commit
|
|
222
|
+
is_revert: Whether this is a revert commit
|
|
223
|
+
|
|
224
|
+
Returns:
|
|
225
|
+
Updated PetStats
|
|
226
|
+
"""
|
|
227
|
+
# Load current state
|
|
228
|
+
stats = self.get_pet_state()
|
|
229
|
+
|
|
230
|
+
# Update stats
|
|
231
|
+
stats.total_commits += 1
|
|
232
|
+
stats.lines_added += lines_added
|
|
233
|
+
stats.lines_deleted += lines_deleted
|
|
234
|
+
|
|
235
|
+
if is_merge:
|
|
236
|
+
stats.merge_conflicts += 1
|
|
237
|
+
|
|
238
|
+
if is_revert:
|
|
239
|
+
stats.reverts += 1
|
|
240
|
+
|
|
241
|
+
# Feed the pet (updates last_fed and mood)
|
|
242
|
+
stats.feed()
|
|
243
|
+
|
|
244
|
+
# Update form based on new stats
|
|
245
|
+
stats.update_form()
|
|
246
|
+
|
|
247
|
+
# Calculate quality impact
|
|
248
|
+
quality_impact = 0.0
|
|
249
|
+
if is_revert:
|
|
250
|
+
quality_impact = -5.0
|
|
251
|
+
elif is_merge:
|
|
252
|
+
quality_impact = -2.0
|
|
253
|
+
else:
|
|
254
|
+
quality_impact = 1.0
|
|
255
|
+
|
|
256
|
+
stats.quality_score = max(
|
|
257
|
+
0.0, min(100.0, stats.quality_score + quality_impact)
|
|
258
|
+
)
|
|
259
|
+
|
|
260
|
+
# Log commit
|
|
261
|
+
self.log_commit(
|
|
262
|
+
commit_hash=commit_hash,
|
|
263
|
+
author=author,
|
|
264
|
+
message=message,
|
|
265
|
+
timestamp=timestamp,
|
|
266
|
+
lines_added=lines_added,
|
|
267
|
+
lines_deleted=lines_deleted,
|
|
268
|
+
files_changed=files_changed,
|
|
269
|
+
is_merge=is_merge,
|
|
270
|
+
is_revert=is_revert,
|
|
271
|
+
quality_impact=quality_impact,
|
|
272
|
+
)
|
|
273
|
+
|
|
274
|
+
# Save updated state
|
|
275
|
+
self.save_pet_state(stats)
|
|
276
|
+
|
|
277
|
+
return stats
|
src/hooks/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Git hooks module."""
|
src/hooks/installer.py
ADDED
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
"""Hook installation."""
|
|
2
|
+
import os
|
|
3
|
+
import sys
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from typing import Optional
|
|
6
|
+
|
|
7
|
+
from git import InvalidGitRepositoryError, Repo
|
|
8
|
+
from rich.console import Console
|
|
9
|
+
|
|
10
|
+
console = Console()
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def find_git_root() -> Optional[Path]:
|
|
14
|
+
"""Find git repository root.
|
|
15
|
+
|
|
16
|
+
Returns:
|
|
17
|
+
Path to git root, or None if not in a git repo
|
|
18
|
+
"""
|
|
19
|
+
try:
|
|
20
|
+
repo = Repo(".", search_parent_directories=True)
|
|
21
|
+
return Path(repo.git_dir).parent
|
|
22
|
+
except InvalidGitRepositoryError:
|
|
23
|
+
return None
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def install_hooks() -> bool:
|
|
27
|
+
"""Install git hooks for GitGotchi.
|
|
28
|
+
|
|
29
|
+
Returns:
|
|
30
|
+
True if successful, False otherwise
|
|
31
|
+
"""
|
|
32
|
+
git_root = find_git_root()
|
|
33
|
+
|
|
34
|
+
if not git_root:
|
|
35
|
+
console.print(
|
|
36
|
+
"[red]👻 No git repository found! Initialize git first.[/red]"
|
|
37
|
+
)
|
|
38
|
+
return False
|
|
39
|
+
|
|
40
|
+
hooks_dir = git_root / ".git" / "hooks"
|
|
41
|
+
hooks_dir.mkdir(parents=True, exist_ok=True)
|
|
42
|
+
|
|
43
|
+
post_commit_path = hooks_dir / "post-commit"
|
|
44
|
+
|
|
45
|
+
# GitGotchi hook content
|
|
46
|
+
python_path = sys.executable.replace("\\", "/")
|
|
47
|
+
gitgotchi_hook = f"""#!/bin/sh
|
|
48
|
+
# GitGotchi post-commit hook
|
|
49
|
+
"{python_path}" -m src.hooks.post_commit
|
|
50
|
+
"""
|
|
51
|
+
|
|
52
|
+
# Check if hook already exists
|
|
53
|
+
if post_commit_path.exists():
|
|
54
|
+
existing_content = post_commit_path.read_text()
|
|
55
|
+
|
|
56
|
+
if "GitGotchi" in existing_content:
|
|
57
|
+
console.print("[yellow]👻 GitGotchi hooks already installed![/yellow]")
|
|
58
|
+
return True
|
|
59
|
+
|
|
60
|
+
# Append to existing hook
|
|
61
|
+
console.print(
|
|
62
|
+
"[yellow]⚠️ Existing post-commit hook found. Appending GitGotchi...[/yellow]"
|
|
63
|
+
)
|
|
64
|
+
with open(post_commit_path, "a") as f:
|
|
65
|
+
f.write("\n" + gitgotchi_hook)
|
|
66
|
+
else:
|
|
67
|
+
# Create new hook
|
|
68
|
+
post_commit_path.write_text(gitgotchi_hook)
|
|
69
|
+
|
|
70
|
+
# Make executable
|
|
71
|
+
post_commit_path.chmod(0o755)
|
|
72
|
+
|
|
73
|
+
console.print("[green]✨ GitGotchi hooks installed successfully![/green]")
|
|
74
|
+
console.print(f"[dim]Hook location: {post_commit_path}[/dim]")
|
|
75
|
+
|
|
76
|
+
return True
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def uninstall_hooks() -> bool:
|
|
80
|
+
"""Uninstall GitGotchi git hooks.
|
|
81
|
+
|
|
82
|
+
Returns:
|
|
83
|
+
True if successful, False otherwise
|
|
84
|
+
"""
|
|
85
|
+
git_root = find_git_root()
|
|
86
|
+
|
|
87
|
+
if not git_root:
|
|
88
|
+
console.print("[red]👻 No git repository found![/red]")
|
|
89
|
+
return False
|
|
90
|
+
|
|
91
|
+
post_commit_path = git_root / ".git" / "hooks" / "post-commit"
|
|
92
|
+
|
|
93
|
+
if not post_commit_path.exists():
|
|
94
|
+
console.print("[yellow]👻 No hooks found to uninstall.[/yellow]")
|
|
95
|
+
return True
|
|
96
|
+
|
|
97
|
+
existing_content = post_commit_path.read_text()
|
|
98
|
+
|
|
99
|
+
if "GitGotchi" not in existing_content:
|
|
100
|
+
console.print("[yellow]👻 GitGotchi hooks not found.[/yellow]")
|
|
101
|
+
return True
|
|
102
|
+
|
|
103
|
+
# Remove GitGotchi section
|
|
104
|
+
lines = existing_content.split("\n")
|
|
105
|
+
filtered_lines = []
|
|
106
|
+
skip_next = False
|
|
107
|
+
|
|
108
|
+
for line in lines:
|
|
109
|
+
if "GitGotchi" in line:
|
|
110
|
+
skip_next = True
|
|
111
|
+
continue
|
|
112
|
+
if skip_next and line.strip().startswith("python"):
|
|
113
|
+
skip_next = False
|
|
114
|
+
continue
|
|
115
|
+
filtered_lines.append(line)
|
|
116
|
+
|
|
117
|
+
new_content = "\n".join(filtered_lines).strip()
|
|
118
|
+
|
|
119
|
+
if new_content and new_content != "#!/bin/sh":
|
|
120
|
+
# Other hooks remain, update file
|
|
121
|
+
post_commit_path.write_text(new_content)
|
|
122
|
+
console.print("[green]✨ GitGotchi hooks removed.[/green]")
|
|
123
|
+
else:
|
|
124
|
+
# No other hooks, delete file
|
|
125
|
+
post_commit_path.unlink()
|
|
126
|
+
console.print("[green]✨ GitGotchi hooks removed (file deleted).[/green]")
|
|
127
|
+
|
|
128
|
+
return True
|
src/hooks/post_commit.py
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
"""Git hook handlers."""
|
|
2
|
+
from datetime import datetime
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
|
|
5
|
+
from git import Repo
|
|
6
|
+
|
|
7
|
+
from src.db.state_manager import StateManager
|
|
8
|
+
from src.pet.renderer import render_commit_response, render_error, render_evolution
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def handle_post_commit() -> None:
|
|
12
|
+
"""Handle post-commit hook."""
|
|
13
|
+
try:
|
|
14
|
+
# Get git repo
|
|
15
|
+
repo = Repo(".", search_parent_directories=True)
|
|
16
|
+
git_root = Path(repo.git_dir).parent
|
|
17
|
+
|
|
18
|
+
# Get latest commit
|
|
19
|
+
commit = repo.head.commit
|
|
20
|
+
|
|
21
|
+
# Parse commit metadata
|
|
22
|
+
commit_hash = commit.hexsha
|
|
23
|
+
author = str(commit.author)
|
|
24
|
+
message = commit.message
|
|
25
|
+
timestamp = datetime.fromtimestamp(commit.committed_date)
|
|
26
|
+
|
|
27
|
+
# Calculate stats
|
|
28
|
+
stats = commit.stats.total
|
|
29
|
+
lines_added = stats.get("insertions", 0)
|
|
30
|
+
lines_deleted = stats.get("deletions", 0)
|
|
31
|
+
files_changed = stats.get("files", 0)
|
|
32
|
+
|
|
33
|
+
# Check for merge/revert
|
|
34
|
+
is_merge = len(commit.parents) > 1
|
|
35
|
+
is_revert = message.lower().startswith("revert")
|
|
36
|
+
|
|
37
|
+
# Initialize state manager
|
|
38
|
+
db_path = git_root / ".gitgotchi" / "state.db"
|
|
39
|
+
state_manager = StateManager(str(db_path))
|
|
40
|
+
|
|
41
|
+
# Get old form for evolution check
|
|
42
|
+
old_stats = state_manager.get_pet_state()
|
|
43
|
+
old_form = old_stats.current_form
|
|
44
|
+
|
|
45
|
+
# Process commit event
|
|
46
|
+
new_stats = state_manager.process_commit_event(
|
|
47
|
+
commit_hash=commit_hash,
|
|
48
|
+
author=author,
|
|
49
|
+
message=message,
|
|
50
|
+
timestamp=timestamp,
|
|
51
|
+
lines_added=lines_added,
|
|
52
|
+
lines_deleted=lines_deleted,
|
|
53
|
+
files_changed=files_changed,
|
|
54
|
+
is_merge=is_merge,
|
|
55
|
+
is_revert=is_revert,
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
# Check for evolution
|
|
59
|
+
if new_stats.current_form != old_form:
|
|
60
|
+
render_evolution(old_form, new_stats.current_form)
|
|
61
|
+
|
|
62
|
+
# Render commit response
|
|
63
|
+
render_commit_response(new_stats, lines_added, lines_deleted)
|
|
64
|
+
|
|
65
|
+
except Exception as e:
|
|
66
|
+
render_error(f"Failed to process commit: {str(e)}")
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
if __name__ == "__main__":
|
|
70
|
+
handle_post_commit()
|
src/pet/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Pet module."""
|
src/pet/evolution.py
ADDED
src/pet/renderer.py
ADDED
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
"""ASCII art & terminal UI."""
|
|
2
|
+
import sys
|
|
3
|
+
|
|
4
|
+
from rich.console import Console
|
|
5
|
+
from rich.panel import Panel
|
|
6
|
+
from rich.text import Text
|
|
7
|
+
|
|
8
|
+
from src.pet.states import PetForm, PetMood, PetStats
|
|
9
|
+
|
|
10
|
+
# Force UTF-8 encoding for Windows
|
|
11
|
+
if sys.platform == "win32":
|
|
12
|
+
sys.stdout.reconfigure(encoding="utf-8")
|
|
13
|
+
|
|
14
|
+
console = Console(force_terminal=True, legacy_windows=False)
|
|
15
|
+
|
|
16
|
+
# ASCII art for each pet form
|
|
17
|
+
PET_SPRITES = {
|
|
18
|
+
PetForm.EGG: """
|
|
19
|
+
.-""-.
|
|
20
|
+
/ \\
|
|
21
|
+
| o o |
|
|
22
|
+
\\ __ /
|
|
23
|
+
'-..-'
|
|
24
|
+
""",
|
|
25
|
+
PetForm.GHOST: """
|
|
26
|
+
.-.
|
|
27
|
+
(o.o)
|
|
28
|
+
|=|
|
|
29
|
+
__|__
|
|
30
|
+
/ \\
|
|
31
|
+
| () () |
|
|
32
|
+
\\ /
|
|
33
|
+
`---'
|
|
34
|
+
""",
|
|
35
|
+
PetForm.ANGEL: """
|
|
36
|
+
\\|/
|
|
37
|
+
.-'o'-.
|
|
38
|
+
/ \\_/ \\
|
|
39
|
+
| (o.o) |
|
|
40
|
+
\\ | /
|
|
41
|
+
'--|--'
|
|
42
|
+
|
|
|
43
|
+
/ \\
|
|
44
|
+
""",
|
|
45
|
+
PetForm.DEMON: """
|
|
46
|
+
/\\ /\\
|
|
47
|
+
( o.o )
|
|
48
|
+
> ^ <
|
|
49
|
+
/| |\\
|
|
50
|
+
(_| |_)
|
|
51
|
+
| |
|
|
52
|
+
/| |\\
|
|
53
|
+
""",
|
|
54
|
+
PetForm.ZOMBIE: """
|
|
55
|
+
.---.
|
|
56
|
+
/x x\\
|
|
57
|
+
| ___ |
|
|
58
|
+
\\ \\_/ /
|
|
59
|
+
|||||
|
|
60
|
+
/| |\\
|
|
61
|
+
/ | | \\
|
|
62
|
+
""",
|
|
63
|
+
PetForm.WRAITH: """
|
|
64
|
+
___
|
|
65
|
+
.' '.
|
|
66
|
+
/ o o \\
|
|
67
|
+
| ^ |
|
|
68
|
+
\\ \\_/ /
|
|
69
|
+
'.___.'
|
|
70
|
+
|||||
|
|
71
|
+
~~~~~~~
|
|
72
|
+
""",
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
# Mood-based colors (Stardew Valley inspired pastels)
|
|
76
|
+
MOOD_COLORS = {
|
|
77
|
+
PetMood.JOYFUL: "bright_yellow", # Gold sparkles
|
|
78
|
+
PetMood.HAPPY: "cyan", # Sky blue
|
|
79
|
+
PetMood.CONTENT: "bright_white", # Soft white
|
|
80
|
+
PetMood.HUNGRY: "yellow", # Gentle yellow
|
|
81
|
+
PetMood.TIRED: "magenta", # Lavender
|
|
82
|
+
PetMood.SLEEPING: "blue", # Soft blue
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
# Mood messages (friendly and encouraging)
|
|
86
|
+
MOOD_MESSAGES = {
|
|
87
|
+
PetMood.JOYFUL: "✨ Your friend is glowing with happiness! ✨",
|
|
88
|
+
PetMood.HAPPY: "💛 Your companion is content and cheerful!",
|
|
89
|
+
PetMood.CONTENT: "🌟 Your friend is peaceful and happy",
|
|
90
|
+
PetMood.HUNGRY: "🍂 Your friend misses you... time for a commit?",
|
|
91
|
+
PetMood.TIRED: "💤 Your companion is getting sleepy...",
|
|
92
|
+
PetMood.SLEEPING: "😴 Zzz... Your friend is dreaming of code...",
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def render_pet(stats: PetStats, show_stats: bool = True) -> None:
|
|
97
|
+
"""Render pet to terminal with haunted styling.
|
|
98
|
+
|
|
99
|
+
Args:
|
|
100
|
+
stats: Current pet statistics
|
|
101
|
+
show_stats: Whether to show detailed stats
|
|
102
|
+
"""
|
|
103
|
+
sprite = PET_SPRITES.get(stats.current_form, PET_SPRITES[PetForm.GHOST])
|
|
104
|
+
color = MOOD_COLORS.get(stats.current_mood, "white")
|
|
105
|
+
mood_msg = MOOD_MESSAGES.get(stats.current_mood, "")
|
|
106
|
+
|
|
107
|
+
# Create styled sprite
|
|
108
|
+
styled_sprite = Text(sprite, style=color)
|
|
109
|
+
|
|
110
|
+
# Build title
|
|
111
|
+
title = f"👻 {stats.current_form.value.upper()} - {stats.current_mood.value}"
|
|
112
|
+
|
|
113
|
+
# Build subtitle with mood message
|
|
114
|
+
subtitle = Text(mood_msg, style=color)
|
|
115
|
+
|
|
116
|
+
# Create panel content
|
|
117
|
+
content = styled_sprite
|
|
118
|
+
|
|
119
|
+
if show_stats:
|
|
120
|
+
stats_text = Text("\n")
|
|
121
|
+
stats_text.append(f"💀 Commits: {stats.total_commits}\n", style="cyan")
|
|
122
|
+
stats_text.append(f"✨ Quality: {stats.quality_score:.1f}/100\n", style="cyan")
|
|
123
|
+
stats_text.append(f"➕ Lines Added: {stats.lines_added}\n", style="green")
|
|
124
|
+
stats_text.append(f"➖ Lines Deleted: {stats.lines_deleted}\n", style="red")
|
|
125
|
+
stats_text.append(f"⚔️ Conflicts: {stats.merge_conflicts}\n", style="yellow")
|
|
126
|
+
stats_text.append(f"🔄 Reverts: {stats.reverts}\n", style="magenta")
|
|
127
|
+
|
|
128
|
+
content = Text.assemble(styled_sprite, stats_text)
|
|
129
|
+
|
|
130
|
+
# Render panel
|
|
131
|
+
panel = Panel(
|
|
132
|
+
content,
|
|
133
|
+
title=title,
|
|
134
|
+
subtitle=subtitle,
|
|
135
|
+
border_style=color,
|
|
136
|
+
padding=(1, 2),
|
|
137
|
+
)
|
|
138
|
+
|
|
139
|
+
console.print(panel)
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def render_evolution(old_form: PetForm, new_form: PetForm) -> None:
|
|
143
|
+
"""Render evolution animation.
|
|
144
|
+
|
|
145
|
+
Args:
|
|
146
|
+
old_form: Previous pet form
|
|
147
|
+
new_form: New pet form
|
|
148
|
+
"""
|
|
149
|
+
console.print("\n")
|
|
150
|
+
console.print("✨" * 20, style="bright_yellow")
|
|
151
|
+
console.print(
|
|
152
|
+
f"[bold bright_yellow]Something magical is happening! ✨[/bold bright_yellow]"
|
|
153
|
+
)
|
|
154
|
+
console.print(
|
|
155
|
+
f"[bold bright_cyan]Your {old_form.value} is evolving into a {new_form.value}! 🎉[/bold bright_cyan]"
|
|
156
|
+
)
|
|
157
|
+
console.print("✨" * 20, style="bright_yellow")
|
|
158
|
+
console.print("\n")
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
def render_commit_response(
|
|
162
|
+
stats: PetStats, lines_added: int, lines_deleted: int
|
|
163
|
+
) -> None:
|
|
164
|
+
"""Render response to a commit.
|
|
165
|
+
|
|
166
|
+
Args:
|
|
167
|
+
stats: Updated pet statistics
|
|
168
|
+
lines_added: Lines added in commit
|
|
169
|
+
lines_deleted: Lines deleted in commit
|
|
170
|
+
"""
|
|
171
|
+
console.print("\n")
|
|
172
|
+
|
|
173
|
+
# Friendly commit messages
|
|
174
|
+
messages = [
|
|
175
|
+
"Your friend is happy! Great commit! ✨",
|
|
176
|
+
"Yay! Your companion grows stronger! 🌟",
|
|
177
|
+
"Another step forward together! 💛",
|
|
178
|
+
"Your friend appreciates your hard work! 🍂",
|
|
179
|
+
"Wonderful! Your bond deepens! ❤️",
|
|
180
|
+
]
|
|
181
|
+
|
|
182
|
+
import random
|
|
183
|
+
|
|
184
|
+
msg = random.choice(messages)
|
|
185
|
+
console.print(f"[cyan]{msg}[/cyan]")
|
|
186
|
+
|
|
187
|
+
# Show quick stats
|
|
188
|
+
console.print(
|
|
189
|
+
f"[green]+{lines_added}[/green] [red]-{lines_deleted}[/red] lines"
|
|
190
|
+
)
|
|
191
|
+
|
|
192
|
+
# Render pet
|
|
193
|
+
render_pet(stats, show_stats=False)
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
def render_error(message: str) -> None:
|
|
197
|
+
"""Render error with friendly flavor.
|
|
198
|
+
|
|
199
|
+
Args:
|
|
200
|
+
message: Error message
|
|
201
|
+
"""
|
|
202
|
+
console.print(
|
|
203
|
+
Panel(
|
|
204
|
+
f"[bold yellow]🌙 Oh no! Something went wrong...[/bold yellow]\n\n{message}",
|
|
205
|
+
border_style="yellow",
|
|
206
|
+
title="⚠️ Oops",
|
|
207
|
+
)
|
|
208
|
+
)
|