cli-nix 0.3.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.
Files changed (100) hide show
  1. cli_nix-0.3.0.dist-info/METADATA +340 -0
  2. cli_nix-0.3.0.dist-info/RECORD +100 -0
  3. cli_nix-0.3.0.dist-info/WHEEL +5 -0
  4. cli_nix-0.3.0.dist-info/entry_points.txt +2 -0
  5. cli_nix-0.3.0.dist-info/licenses/LICENSE +18 -0
  6. cli_nix-0.3.0.dist-info/top_level.txt +1 -0
  7. nix/__init__.py +1 -0
  8. nix/__main__.py +4 -0
  9. nix/app.py +188 -0
  10. nix/avatar.py +518 -0
  11. nix/body.py +55 -0
  12. nix/brain.py +198 -0
  13. nix/cli.py +27 -0
  14. nix/commands.py +1733 -0
  15. nix/config.py +83 -0
  16. nix/daemon.py +210 -0
  17. nix/git.py +231 -0
  18. nix/i18n.py +734 -0
  19. nix/journal.py +59 -0
  20. nix/logger.py +24 -0
  21. nix/modules/__init__.py +46 -0
  22. nix/modules/bundled/__init__.py +0 -0
  23. nix/modules/bundled/c/__init__.py +0 -0
  24. nix/modules/bundled/c/blocks.json +21 -0
  25. nix/modules/bundled/c/gen/function.tmpl +4 -0
  26. nix/modules/bundled/c/module.json +6 -0
  27. nix/modules/bundled/c/ops.json +19 -0
  28. nix/modules/bundled/c/probe.json +9 -0
  29. nix/modules/bundled/cpp/__init__.py +0 -0
  30. nix/modules/bundled/cpp/blocks.json +30 -0
  31. nix/modules/bundled/cpp/gen/class.tmpl +4 -0
  32. nix/modules/bundled/cpp/gen/function.tmpl +4 -0
  33. nix/modules/bundled/cpp/module.json +6 -0
  34. nix/modules/bundled/cpp/ops.json +25 -0
  35. nix/modules/bundled/cpp/probe.json +14 -0
  36. nix/modules/bundled/csharp/__init__.py +0 -0
  37. nix/modules/bundled/csharp/blocks.json +30 -0
  38. nix/modules/bundled/csharp/gen/class.tmpl +4 -0
  39. nix/modules/bundled/csharp/gen/function.tmpl +4 -0
  40. nix/modules/bundled/csharp/module.json +6 -0
  41. nix/modules/bundled/csharp/ops.json +25 -0
  42. nix/modules/bundled/csharp/probe.json +14 -0
  43. nix/modules/bundled/go/__init__.py +0 -0
  44. nix/modules/bundled/go/blocks.json +17 -0
  45. nix/modules/bundled/go/gen/function.tmpl +4 -0
  46. nix/modules/bundled/go/module.json +6 -0
  47. nix/modules/bundled/go/ops.json +13 -0
  48. nix/modules/bundled/go/probe.json +14 -0
  49. nix/modules/bundled/java/__init__.py +0 -0
  50. nix/modules/bundled/java/blocks.json +30 -0
  51. nix/modules/bundled/java/gen/class.tmpl +4 -0
  52. nix/modules/bundled/java/gen/function.tmpl +4 -0
  53. nix/modules/bundled/java/module.json +6 -0
  54. nix/modules/bundled/java/ops.json +25 -0
  55. nix/modules/bundled/java/probe.json +14 -0
  56. nix/modules/bundled/javascript/__init__.py +0 -0
  57. nix/modules/bundled/javascript/blocks.json +30 -0
  58. nix/modules/bundled/javascript/gen/class.tmpl +4 -0
  59. nix/modules/bundled/javascript/gen/function.tmpl +4 -0
  60. nix/modules/bundled/javascript/module.json +6 -0
  61. nix/modules/bundled/javascript/ops.json +25 -0
  62. nix/modules/bundled/javascript/probe.json +19 -0
  63. nix/modules/bundled/php/__init__.py +0 -0
  64. nix/modules/bundled/php/blocks.json +30 -0
  65. nix/modules/bundled/php/gen/class.tmpl +4 -0
  66. nix/modules/bundled/php/gen/function.tmpl +4 -0
  67. nix/modules/bundled/php/module.json +6 -0
  68. nix/modules/bundled/php/ops.json +25 -0
  69. nix/modules/bundled/php/probe.json +14 -0
  70. nix/modules/bundled/python/__init__.py +0 -0
  71. nix/modules/bundled/python/blocks.json +30 -0
  72. nix/modules/bundled/python/gen/class.tmpl +3 -0
  73. nix/modules/bundled/python/gen/function.tmpl +3 -0
  74. nix/modules/bundled/python/gen/scaffold_crud.tmpl +35 -0
  75. nix/modules/bundled/python/gen/scaffold_model.tmpl +9 -0
  76. nix/modules/bundled/python/gen/scaffold_test.tmpl +41 -0
  77. nix/modules/bundled/python/gen/testgen.tmpl +13 -0
  78. nix/modules/bundled/python/module.json +1 -0
  79. nix/modules/bundled/python/ops.json +25 -0
  80. nix/modules/bundled/python/probe.json +14 -0
  81. nix/modules/bundled/rust/__init__.py +0 -0
  82. nix/modules/bundled/rust/blocks.json +21 -0
  83. nix/modules/bundled/rust/gen/function.tmpl +4 -0
  84. nix/modules/bundled/rust/module.json +6 -0
  85. nix/modules/bundled/rust/ops.json +19 -0
  86. nix/modules/bundled/rust/probe.json +14 -0
  87. nix/modules/bundled/typescript/__init__.py +0 -0
  88. nix/modules/bundled/typescript/blocks.json +30 -0
  89. nix/modules/bundled/typescript/gen/class.tmpl +4 -0
  90. nix/modules/bundled/typescript/gen/function.tmpl +4 -0
  91. nix/modules/bundled/typescript/module.json +6 -0
  92. nix/modules/bundled/typescript/ops.json +25 -0
  93. nix/modules/bundled/typescript/probe.json +19 -0
  94. nix/modules/engine.py +174 -0
  95. nix/modules/loader.py +172 -0
  96. nix/pet.py +103 -0
  97. nix/scaffold.py +251 -0
  98. nix/scanner.py +73 -0
  99. nix/state.py +63 -0
  100. nix/ui.py +1187 -0
nix/avatar.py ADDED
@@ -0,0 +1,518 @@
1
+ from __future__ import annotations
2
+
3
+ import hashlib
4
+ import random
5
+ import time
6
+
7
+ from rich.text import Text
8
+
9
+ BG = "#050505"
10
+
11
+ PALETTES = {
12
+ "mint": {
13
+ "hi": "#8ef0b4", "lo": "#2f9d6e", "belly": "#d9ffe9",
14
+ "cheek": "#ffb3b3", "accent": "#eaffc9", "outline": "#1d5c44",
15
+ },
16
+ "sky": {
17
+ "hi": "#8fd6ff", "lo": "#3a7bd6", "belly": "#dff2ff",
18
+ "cheek": "#ffc2c2", "accent": "#ffffff", "outline": "#1f5a8a",
19
+ },
20
+ "grape": {
21
+ "hi": "#c9a6ff", "lo": "#7c4fd0", "belly": "#efe2ff",
22
+ "cheek": "#ffb3d6", "accent": "#ffe4ff", "outline": "#4a2f7a",
23
+ },
24
+ "rose": {
25
+ "hi": "#ffb3d9", "lo": "#d654a6", "belly": "#ffe4f2",
26
+ "cheek": "#ff8a8a", "accent": "#fff0f7", "outline": "#7a2f5a",
27
+ },
28
+ "slate": {
29
+ "hi": "#b9c4d9", "lo": "#5a6b8f", "belly": "#e8eef7",
30
+ "cheek": "#ffb3b3", "accent": "#cfe4ff", "outline": "#3a4660",
31
+ },
32
+ "acid": {
33
+ "hi": "#c9f56f", "lo": "#6aa520", "belly": "#e9ffb8",
34
+ "cheek": "#ffb3b3", "accent": "#f6ffd0", "outline": "#3d6b12",
35
+ },
36
+ "pink": {
37
+ "hi": "#ffc2e6", "lo": "#d65fa8", "belly": "#ffe9f6",
38
+ "cheek": "#ff8ab4", "accent": "#fff0fa", "outline": "#7a2f5e",
39
+ },
40
+ "slime": {
41
+ "hi": "#7fe8d9", "lo": "#2a9c8f", "belly": "#d0fbf3",
42
+ "cheek": "#ffb3a6", "accent": "#e6fffb", "outline": "#176a60",
43
+ },
44
+ }
45
+
46
+ # --- easter-egg / signature palettes -------------------------------
47
+
48
+ # The Orange Octopus - a nod to Claude Code's mascot.
49
+ OCTOPUS_PAL = {
50
+ "hi": "#ffad5c", "lo": "#e0551c", "belly": "#ffe3b8",
51
+ "cheek": "#ff7f4f", "accent": "#fff3d6", "outline": "#8a2b06",
52
+ }
53
+
54
+ # A cheerful orange one-eyed alien.
55
+ CYCLOPS_PAL = {
56
+ "hi": "#ffb84d", "lo": "#e86f1f", "belly": "#ffe8ad",
57
+ "cheek": "#ff7f4f", "accent": "#fff3c9", "outline": "#8a4506",
58
+ }
59
+
60
+ # Pou - the beige "piece of dough" virtual pet with stretched eyes.
61
+ POU_PAL = {
62
+ "hi": "#e0b386", "lo": "#a97a45", "belly": "#f5e3c0",
63
+ "cheek": "#ff9b7a", "accent": "#fff3d0", "outline": "#6b4a1f",
64
+ }
65
+
66
+ # My Pet Alien (Tommy) - warm brown round alien with big white eyes.
67
+ TOMMY_PAL = {
68
+ "hi": "#d99a63", "lo": "#96602e", "belly": "#f2d6b2",
69
+ "cheek": "#ff8a6b", "accent": "#ffe9c8", "outline": "#5e3a18",
70
+ }
71
+
72
+ # Mint-green slime.
73
+ SLIME_PAL = {
74
+ "hi": "#7fe8d9", "lo": "#2a9c8f", "belly": "#d0fbf3",
75
+ "cheek": "#ffb3a6", "accent": "#e6fffb", "outline": "#176a60",
76
+ }
77
+
78
+ # Lavender antenna alien.
79
+ ANTENNA_PAL = {
80
+ "hi": "#c9a6ff", "lo": "#8a5fd6", "belly": "#efe2ff",
81
+ "cheek": "#ffb3d6", "accent": "#ffe4ff", "outline": "#4a2f7a",
82
+ }
83
+
84
+ # Crimson cone.
85
+ CONE_PAL = {
86
+ "hi": "#ff8f8f", "lo": "#d6453d", "belly": "#ffe1de",
87
+ "cheek": "#ffb37a", "accent": "#fff0e6", "outline": "#7a1f1f",
88
+ }
89
+
90
+ # Teal pancake-creature.
91
+ LEGGY_PAL = {
92
+ "hi": "#4fd8d8", "lo": "#208f9f", "belly": "#d8fbfb",
93
+ "cheek": "#ffb3a6", "accent": "#e6fffb", "outline": "#145f6b",
94
+ }
95
+
96
+ EYES = {
97
+ "dark": "#23262e",
98
+ "amber": "#7a4a1f",
99
+ "blue": "#274b68",
100
+ }
101
+
102
+ W, H = 12, 16
103
+ EMPTY = "............"
104
+
105
+ # Hand-drawn 12x16 pixel masks. '#' = body, 'b' = belly, '.' = empty.
106
+
107
+ # Orange octopus. Claude Code's mascot came home.
108
+ OCTOPUS = [
109
+ "............",
110
+ "............",
111
+ "..########..",
112
+ ".##########.",
113
+ "############",
114
+ "############",
115
+ "############",
116
+ "############",
117
+ "############",
118
+ ".##########.",
119
+ ".##########.",
120
+ "..########..",
121
+ "....####....",
122
+ "..##..##..##",
123
+ "..##..##..##",
124
+ "..##..##..##",
125
+ ]
126
+
127
+ # A round orange alien with a single big eye.
128
+ CYCLOPS = [
129
+ "............",
130
+ "............",
131
+ "...######...",
132
+ "..########..",
133
+ ".##########.",
134
+ "############",
135
+ "############",
136
+ "############",
137
+ "############",
138
+ "############",
139
+ ".##########.",
140
+ "..########..",
141
+ "....####....",
142
+ "............",
143
+ "............",
144
+ "............",
145
+ ]
146
+
147
+ # Pou: a beige blob, a piece of dough with small stretched eyes.
148
+ POU = [
149
+ "............",
150
+ "............",
151
+ ".....##.....",
152
+ "....###.....",
153
+ "...#####....",
154
+ "..#######...",
155
+ ".#########..",
156
+ "############",
157
+ "############",
158
+ "############",
159
+ "############",
160
+ "############",
161
+ ".#########..",
162
+ "..#######...",
163
+ "............",
164
+ "............",
165
+ ]
166
+
167
+ # My Pet Alien (Tommy): round brown head, big white eyes.
168
+ TOMMY = [
169
+ "............",
170
+ "............",
171
+ "....####....",
172
+ "...######...",
173
+ "..########..",
174
+ ".##########.",
175
+ "############",
176
+ "############",
177
+ "############",
178
+ "############",
179
+ ".##########.",
180
+ ".##########.",
181
+ "..########..",
182
+ "...####.....",
183
+ "............",
184
+ "............",
185
+ ]
186
+
187
+ # A wavy-bottomed slime with two goofy eyes.
188
+ SLIME = [
189
+ "............",
190
+ "............",
191
+ "...######...",
192
+ "..########..",
193
+ ".##########.",
194
+ "############",
195
+ "############",
196
+ "############",
197
+ "############",
198
+ "..########..",
199
+ ".####..####.",
200
+ ".##......##.",
201
+ ".##......##.",
202
+ "............",
203
+ "............",
204
+ "............",
205
+ ]
206
+
207
+ # A green alien with two antennae sticking up.
208
+ ANTENNA = [
209
+ "..#......#..",
210
+ "...#....#...",
211
+ "..###..###..",
212
+ "..##....##..",
213
+ ".##########.",
214
+ "############",
215
+ "############",
216
+ "############",
217
+ ".####..####.",
218
+ ".##########.",
219
+ "..########..",
220
+ "...######...",
221
+ "...######...",
222
+ "............",
223
+ "............",
224
+ "............",
225
+ ]
226
+
227
+ # A strangely conical alien.
228
+ CONE = [
229
+ "............",
230
+ "............",
231
+ ".....##.....",
232
+ ".....###....",
233
+ "....#####...",
234
+ "...#######..",
235
+ "..#########.",
236
+ ".##########.",
237
+ ".##########.",
238
+ ".####..####.",
239
+ ".##########.",
240
+ "..########..",
241
+ "...######...",
242
+ "............",
243
+ "............",
244
+ "............",
245
+ ]
246
+
247
+ # A weird floating pancake-creature with little legs.
248
+ LEGGY = [
249
+ "...######...",
250
+ "..########..",
251
+ ".##########.",
252
+ ".##########.",
253
+ "############",
254
+ ".##########.",
255
+ ".##########.",
256
+ "..########..",
257
+ "..####..####",
258
+ "..##..##..##",
259
+ "..##..##..##",
260
+ "............",
261
+ "............",
262
+ "............",
263
+ "............",
264
+ "............",
265
+ ]
266
+
267
+ VARIANTS = [
268
+ {"kind": "octopus", "grid": OCTOPUS, "face_y": 6, "mouth_y": 9,
269
+ "palette": OCTOPUS_PAL},
270
+ {"kind": "cyclops", "grid": CYCLOPS, "face_y": 5, "mouth_y": 9,
271
+ "palette": CYCLOPS_PAL, "single_eye": True},
272
+ {"kind": "pou", "grid": POU, "face_y": 7, "mouth_y": 10,
273
+ "palette": POU_PAL, "pou_eyes": True},
274
+ {"kind": "tommy", "grid": TOMMY, "face_y": 5, "mouth_y": 9,
275
+ "palette": TOMMY_PAL},
276
+ {"kind": "slime", "grid": SLIME, "face_y": 5, "mouth_y": 8,
277
+ "palette": SLIME_PAL},
278
+ {"kind": "antenna", "grid": ANTENNA, "face_y": 5, "mouth_y": 8,
279
+ "palette": ANTENNA_PAL},
280
+ {"kind": "cone", "grid": CONE, "face_y": 6, "mouth_y": 9,
281
+ "palette": CONE_PAL},
282
+ {"kind": "leggy", "grid": LEGGY, "face_y": 3, "mouth_y": 7,
283
+ "palette": LEGGY_PAL},
284
+ ]
285
+
286
+ VARIANT_KINDS = [v["kind"] for v in VARIANTS]
287
+
288
+
289
+ def choose_skin(not_kind: str | None = None) -> str:
290
+ pool = [k for k in VARIANT_KINDS if k != not_kind]
291
+ return random.choice(pool or list(VARIANT_KINDS))
292
+
293
+
294
+ def _lerp(a: str, b: str, t: float) -> str:
295
+ al = [int(a[i : i + 2], 16) for i in (1, 3, 5)]
296
+ bl = [int(b[i : i + 2], 16) for i in (1, 3, 5)]
297
+ return "#" + "".join(
298
+ f"{round(al[k] + (bl[k] - al[k]) * max(0.0, min(1.0, t))) & 255:02x}"
299
+ for k in range(3)
300
+ )
301
+
302
+
303
+ def genes_for(root, pet: dict) -> dict:
304
+ seed = f"{root}:{pet.get('name', 'pet')}"
305
+ h = int.from_bytes(hashlib.sha256(seed.encode("utf-8")).digest()[:8], "big")
306
+ r = random.Random(h)
307
+ return {
308
+ "palette": r.choice(list(PALETTES)),
309
+ "variant": r.choice(VARIANTS)["kind"],
310
+ "eye": r.choice(list(EYES)),
311
+ "seed": seed,
312
+ }
313
+
314
+
315
+ def _variant(kind: str) -> dict:
316
+ for v in VARIANTS:
317
+ if v["kind"] == kind:
318
+ return v
319
+ return VARIANTS[0]
320
+
321
+
322
+ def mutation_count(pet: dict) -> int:
323
+ return min(
324
+ 3,
325
+ pet.get("evolution_level", 0) + pet.get("age", 0) // 12,
326
+ )
327
+
328
+
329
+ def _body_matrix(vm: dict, palette: dict, stage: str):
330
+ m = [["."] * W for _ in range(H)]
331
+ grid = vm["grid"]
332
+
333
+ def inside(x, y) -> bool:
334
+ return 0 <= x < W and 0 <= y < H and grid[y][x] != "."
335
+
336
+ for y in range(H):
337
+ row = grid[y] if len(grid) > y else EMPTY
338
+ for x in range(W):
339
+ ch = row[x]
340
+ if ch == ".":
341
+ continue
342
+ edge = not (
343
+ inside(x - 1, y)
344
+ and inside(x + 1, y)
345
+ and inside(x, y - 1)
346
+ and inside(x, y + 1)
347
+ )
348
+ if ch == "b":
349
+ m[y][x] = palette["outline"] if edge else palette["belly"]
350
+ else:
351
+ if edge:
352
+ m[y][x] = palette["outline"]
353
+ else:
354
+ t = (y - 2.5) / 13.0
355
+ m[y][x] = _lerp(palette["hi"], palette["lo"], t)
356
+
357
+ def paint(x, y, color):
358
+ if 0 <= x < W and 0 <= y < H:
359
+ m[y][x] = color
360
+
361
+ # stage accessory (floating above the head)
362
+ if stage == "sprout":
363
+ paint(5, 1, "#6ce0a0")
364
+ paint(6, 1, "#4ade80")
365
+ elif stage == "bloom":
366
+ paint(5, 0, "#ffd166")
367
+ paint(5, 1, "#ff9db4")
368
+ paint(6, 0, "#ff9db4")
369
+ paint(6, 1, "#ffd166")
370
+ elif stage == "mystic":
371
+ for x in range(4, 8):
372
+ paint(x, 0, "#ffd166")
373
+ paint(4, 1, "#aef3ff")
374
+ paint(7, 1, "#aef3ff")
375
+ return m
376
+
377
+
378
+ LOOKS = ("left", "straight", "right")
379
+
380
+
381
+ def _paint_eyes(m, eyes, face_y, eye, glint, look, blink):
382
+ """Draw open eyes as 2-tall sockets with a glinting pupil.
383
+
384
+ *eyes* is a list of (start_x, width); the pupil (glint) travels
385
+ inside each socket along the gaze direction, so the pet visibly
386
+ looks left / straight / right. Blink/tired/sleepy draw a closed
387
+ horizontal line instead.
388
+ """
389
+ def paint(x, y, color):
390
+ if 0 <= x < W and 0 <= y < H:
391
+ m[y][x] = color
392
+
393
+ if blink:
394
+ for start, width in eyes:
395
+ for x in range(start, start + width):
396
+ paint(x, face_y + 1, eye)
397
+ return
398
+
399
+ for start, width in eyes:
400
+ for yy in (face_y, face_y + 1):
401
+ for x in range(start, start + width):
402
+ paint(x, yy, eye)
403
+
404
+ if look == "left":
405
+ for start, _width in eyes:
406
+ paint(start, face_y, glint)
407
+ elif look == "right":
408
+ for start, width in eyes:
409
+ paint(start + width - 1, face_y, glint)
410
+ else: # straight: wide open — both cells lit for 2-wide eyes
411
+ for start, width in eyes:
412
+ if width == 2:
413
+ paint(start, face_y, glint)
414
+ paint(start + width - 1, face_y, glint)
415
+ else:
416
+ paint(start + width // 2, face_y, glint)
417
+
418
+
419
+ def idle_look(pet_seed: str, now: float | None = None) -> str:
420
+ """Deterministic per-pet glance schedule: the eyes dart side to
421
+ side every few seconds, mostly returning to straight ahead."""
422
+ if now is None:
423
+ now = time.time()
424
+ h = int.from_bytes(hashlib.sha256(pet_seed.encode("utf-8")).digest()[:8],
425
+ "big")
426
+ step = 4.0 + (h % 40) / 10.0
427
+ bucket = int(now // step)
428
+ r = random.Random(h ^ (bucket * 0x9E3779B9 & 0xFFFFFFFF))
429
+ return r.choice(("left", "straight", "straight", "right", "straight"))
430
+
431
+
432
+ def _face(m, vm: dict, genes: dict, pal: dict, mood: str, blink: bool,
433
+ look: str = "straight"):
434
+ eye = EYES[genes["eye"]]
435
+ glint = "#ffffff"
436
+ face_y, mouth_y = vm["face_y"], vm["mouth_y"]
437
+ cheeks_y = mouth_y - 1
438
+ closed = blink or mood in ("tired", "sleepy")
439
+
440
+ def paint(x, y, color):
441
+ if 0 <= x < W and 0 <= y < H:
442
+ m[y][x] = color
443
+
444
+ if vm.get("single_eye"):
445
+ _paint_eyes(m, [(4, 4)], face_y, eye, glint, look, closed)
446
+ paint(2, cheeks_y, pal["cheek"])
447
+ paint(9, cheeks_y, pal["cheek"])
448
+ for x in (5, 6):
449
+ paint(x, mouth_y, pal["outline"])
450
+ return
451
+
452
+ _paint_eyes(m, [(3, 2), (8, 2)], face_y, eye, glint, look, closed)
453
+ paint(1, cheeks_y, pal["cheek"])
454
+ paint(10, cheeks_y, pal["cheek"])
455
+
456
+ mouth = {
457
+ "happy": [3, 4, 5, 6, 7, 8],
458
+ "content": [5, 6],
459
+ "focused": [4, 5, 6, 7],
460
+ "alert": [4, 5, 6, 7],
461
+ "determined": [4, 5, 6],
462
+ "thoughtful": [4, 5, 6],
463
+ "curious": [4, 5, 6],
464
+ "cautious": [5, 6],
465
+ "worried": [5, 6],
466
+ "anxious": [5, 6],
467
+ "relieved": [5, 6],
468
+ "tired": [5],
469
+ }.get(mood, [5, 6])
470
+ for x in mouth:
471
+ paint(x, mouth_y, pal["outline"])
472
+
473
+
474
+ def _to_text(m, scale: int) -> Text:
475
+ text = Text()
476
+ for y in range(0, H, 2):
477
+ top = m[y]
478
+ bot = m[y + 1] if y + 1 < H else None
479
+ for x in range(W):
480
+ t = top[x]
481
+ b = (bot[x] if bot is not None else ".")
482
+ if t != "." and b != ".":
483
+ glyph, seg = "\u2580", f"{t} on {b}" # top half = fg
484
+ elif t != ".":
485
+ glyph, seg = "\u2580", f"{t} on {BG}" # top half only
486
+ elif b != ".":
487
+ glyph, seg = "\u2584", f"{b} on {BG}" # bottom half only
488
+ else:
489
+ glyph, seg = " ", ""
490
+ if scale >= 2:
491
+ if seg:
492
+ text.append(glyph * 2, style=seg)
493
+ else:
494
+ text.append(" ")
495
+ else:
496
+ if seg:
497
+ text.append(glyph, style=seg)
498
+ else:
499
+ text.append(" ", style="")
500
+ text.append("\n")
501
+ return text
502
+
503
+
504
+ def render(pet: dict, root, scale: int = 1, blink: bool = False,
505
+ look: str = "straight") -> Text:
506
+ genes = genes_for(root, pet)
507
+ variant = pet.get("skin")
508
+ if variant not in VARIANT_KINDS:
509
+ variant = genes["variant"]
510
+ stage = pet.get("body_pattern", "seed")
511
+ if stage not in ("seed", "sprout", "bloom", "mystic"):
512
+ stage = "seed"
513
+ mood = pet.get("mood", "curious")
514
+ vm = _variant(variant)
515
+ palette = vm.get("palette") or PALETTES[genes["palette"]]
516
+ m = _body_matrix(vm, palette, stage)
517
+ _face(m, vm, genes, palette, mood, blink, look)
518
+ return _to_text(m, scale)
nix/body.py ADDED
@@ -0,0 +1,55 @@
1
+ """Function body inference for the gen command.
2
+
3
+ Given a function's name, parameter list, and return annotation, this
4
+ derives a sensible body for the honest, conventional cases:
5
+ constructors, getters, and setters. Anything it is not sure about
6
+ stays ``pass`` (the gen default).
7
+ """
8
+ from __future__ import annotations
9
+
10
+ import re
11
+
12
+
13
+ def _param_names(params: str) -> list[str]:
14
+ """Extract plain parameter names from a comma-joined spec.
15
+
16
+ Drops ``self``/``cls`` and variadic parameters (``*args``,
17
+ ``**kwargs``). Handles ``name: str = "x"`` and ``age: int = 0``.
18
+ """
19
+ names: list[str] = []
20
+ for part in params.split(","):
21
+ p = part.strip()
22
+ if not p:
23
+ continue
24
+ base = re.split(r"[:=]", p)[0].strip()
25
+ if not base or base in ("self", "cls") or base.startswith(("*", "**")):
26
+ continue
27
+ names.append(base)
28
+ return names
29
+
30
+
31
+ def infer_body(name: str, params: str, ret: str = "") -> str:
32
+ """Suggest a body for a function, or ``pass`` when unsure.
33
+
34
+ The returned string uses 4-space inner indentation so it can be
35
+ dropped straight into a `` {{body}}`` template line.
36
+ """
37
+ if name == "__init__":
38
+ assigned = _param_names(params)
39
+ if not assigned:
40
+ return "pass"
41
+ return "\n ".join(f"self.{p} = {p}" for p in assigned)
42
+
43
+ if name.startswith("get_"):
44
+ field = name[4:].strip("_")
45
+ if field:
46
+ return f"return self._{field}"
47
+
48
+ if name.startswith("set_"):
49
+ field = name[4:].strip("_")
50
+ if field:
51
+ args = _param_names(params)
52
+ value = args[0] if args else field
53
+ return f"self._{field} = {value}"
54
+
55
+ return "pass"