forgecommandline 0.8.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 (113) hide show
  1. forgecli/__init__.py +2 -0
  2. forgecli/ads.py +322 -0
  3. forgecli/agent.py +1093 -0
  4. forgecli/art.py +574 -0
  5. forgecli/attach.py +246 -0
  6. forgecli/brief.py +215 -0
  7. forgecli/browsercheck.py +99 -0
  8. forgecli/checkpoints.py +81 -0
  9. forgecli/city.py +833 -0
  10. forgecli/cli.py +1668 -0
  11. forgecli/config.py +194 -0
  12. forgecli/courier.py +177 -0
  13. forgecli/courier_data.py +164 -0
  14. forgecli/courier_hd.py +431 -0
  15. forgecli/courier_hd_data.bin +0 -0
  16. forgecli/courier_hd_data.py +19 -0
  17. forgecli/ghlogin.py +76 -0
  18. forgecli/github.py +54 -0
  19. forgecli/guards.py +403 -0
  20. forgecli/hooks.py +50 -0
  21. forgecli/httpapi.py +434 -0
  22. forgecli/images.py +110 -0
  23. forgecli/keys.py +152 -0
  24. forgecli/keyvault.py +320 -0
  25. forgecli/mcp.py +674 -0
  26. forgecli/memory.py +42 -0
  27. forgecli/openapi.py +331 -0
  28. forgecli/parallel.py +308 -0
  29. forgecli/prices.py +72 -0
  30. forgecli/procs.py +144 -0
  31. forgecli/providers.py +758 -0
  32. forgecli/quality.py +192 -0
  33. forgecli/repomap.py +78 -0
  34. forgecli/roster.py +227 -0
  35. forgecli/safety.py +93 -0
  36. forgecli/sandbox.py +83 -0
  37. forgecli/sessions.py +97 -0
  38. forgecli/skills.py +446 -0
  39. forgecli/tools.py +1184 -0
  40. forgecli/ui.py +860 -0
  41. forgecli/worktrees.py +79 -0
  42. forgecommandline-0.8.0.data/data/skills/commit/SKILL.md +14 -0
  43. forgecommandline-0.8.0.data/data/skills/debug/SKILL.md +17 -0
  44. forgecommandline-0.8.0.data/data/skills/explain/SKILL.md +14 -0
  45. forgecommandline-0.8.0.data/data/skills/init/SKILL.md +19 -0
  46. forgecommandline-0.8.0.data/data/skills/library/LICENSES/agent-skills-MIT.txt +21 -0
  47. forgecommandline-0.8.0.data/data/skills/library/LICENSES/wondelai-MIT.txt +23 -0
  48. forgecommandline-0.8.0.data/data/skills/library/README.md +9 -0
  49. forgecommandline-0.8.0.data/data/skills/library/_references/accessibility-checklist.md +160 -0
  50. forgecommandline-0.8.0.data/data/skills/library/_references/definition-of-done.md +67 -0
  51. forgecommandline-0.8.0.data/data/skills/library/_references/observability-checklist.md +92 -0
  52. forgecommandline-0.8.0.data/data/skills/library/_references/orchestration-patterns.md +370 -0
  53. forgecommandline-0.8.0.data/data/skills/library/_references/performance-checklist.md +238 -0
  54. forgecommandline-0.8.0.data/data/skills/library/_references/security-checklist.md +248 -0
  55. forgecommandline-0.8.0.data/data/skills/library/_references/testing-patterns.md +235 -0
  56. forgecommandline-0.8.0.data/data/skills/library/api-and-interface-design/SKILL.md +367 -0
  57. forgecommandline-0.8.0.data/data/skills/library/architecture-optimization/SKILL.md +279 -0
  58. forgecommandline-0.8.0.data/data/skills/library/architecture-optimization/references/artifact-templates.md +157 -0
  59. forgecommandline-0.8.0.data/data/skills/library/architecture-optimization/references/methods.md +87 -0
  60. forgecommandline-0.8.0.data/data/skills/library/awwwards-tier-web-application/SKILL.md +877 -0
  61. forgecommandline-0.8.0.data/data/skills/library/browser-testing-with-devtools/SKILL.md +317 -0
  62. forgecommandline-0.8.0.data/data/skills/library/ci-cd-and-automation/SKILL.md +390 -0
  63. forgecommandline-0.8.0.data/data/skills/library/code-review-and-quality/SKILL.md +398 -0
  64. forgecommandline-0.8.0.data/data/skills/library/code-simplification/SKILL.md +331 -0
  65. forgecommandline-0.8.0.data/data/skills/library/constraint-driven-development/SKILL.md +311 -0
  66. forgecommandline-0.8.0.data/data/skills/library/constraint-driven-development/references/floor-guard.md +182 -0
  67. forgecommandline-0.8.0.data/data/skills/library/context-engineering/SKILL.md +353 -0
  68. forgecommandline-0.8.0.data/data/skills/library/debugging-and-error-recovery/SKILL.md +300 -0
  69. forgecommandline-0.8.0.data/data/skills/library/deprecation-and-migration/SKILL.md +247 -0
  70. forgecommandline-0.8.0.data/data/skills/library/documentation-and-adrs/SKILL.md +297 -0
  71. forgecommandline-0.8.0.data/data/skills/library/doubt-driven-development/SKILL.md +243 -0
  72. forgecommandline-0.8.0.data/data/skills/library/frontend-ui-engineering/SKILL.md +340 -0
  73. forgecommandline-0.8.0.data/data/skills/library/git-workflow-and-versioning/SKILL.md +355 -0
  74. forgecommandline-0.8.0.data/data/skills/library/idea-refine/SKILL.md +193 -0
  75. forgecommandline-0.8.0.data/data/skills/library/idea-refine/examples.md +238 -0
  76. forgecommandline-0.8.0.data/data/skills/library/idea-refine/frameworks.md +99 -0
  77. forgecommandline-0.8.0.data/data/skills/library/idea-refine/refinement-criteria.md +113 -0
  78. forgecommandline-0.8.0.data/data/skills/library/idea-refine/scripts/idea-refine.sh +15 -0
  79. forgecommandline-0.8.0.data/data/skills/library/improve-code-quality/SKILL.md +275 -0
  80. forgecommandline-0.8.0.data/data/skills/library/improve-code-quality/references/artifact-templates.md +101 -0
  81. forgecommandline-0.8.0.data/data/skills/library/incremental-implementation/SKILL.md +249 -0
  82. forgecommandline-0.8.0.data/data/skills/library/interview-me/SKILL.md +235 -0
  83. forgecommandline-0.8.0.data/data/skills/library/observability-and-instrumentation/SKILL.md +238 -0
  84. forgecommandline-0.8.0.data/data/skills/library/performance-optimization/SKILL.md +267 -0
  85. forgecommandline-0.8.0.data/data/skills/library/performance-optimization/references/optimization-patterns.md +250 -0
  86. forgecommandline-0.8.0.data/data/skills/library/planning-and-task-breakdown/SKILL.md +257 -0
  87. forgecommandline-0.8.0.data/data/skills/library/remove-technical-debt/SKILL.md +203 -0
  88. forgecommandline-0.8.0.data/data/skills/library/remove-technical-debt/references/artifact-templates.md +79 -0
  89. forgecommandline-0.8.0.data/data/skills/library/security-and-hardening/SKILL.md +216 -0
  90. forgecommandline-0.8.0.data/data/skills/library/security-and-hardening/references/hardening-patterns.md +324 -0
  91. forgecommandline-0.8.0.data/data/skills/library/shipping-and-launch/SKILL.md +336 -0
  92. forgecommandline-0.8.0.data/data/skills/library/source-driven-development/SKILL.md +216 -0
  93. forgecommandline-0.8.0.data/data/skills/library/spec-driven-development/SKILL.md +259 -0
  94. forgecommandline-0.8.0.data/data/skills/library/system-design/SKILL.md +207 -0
  95. forgecommandline-0.8.0.data/data/skills/library/system-design/references/building-blocks.md +272 -0
  96. forgecommandline-0.8.0.data/data/skills/library/system-design/references/common-designs.md +257 -0
  97. forgecommandline-0.8.0.data/data/skills/library/system-design/references/database-scaling.md +295 -0
  98. forgecommandline-0.8.0.data/data/skills/library/system-design/references/estimation-numbers.md +250 -0
  99. forgecommandline-0.8.0.data/data/skills/library/system-design/references/four-step-process.md +286 -0
  100. forgecommandline-0.8.0.data/data/skills/library/system-design/references/reliability-operations.md +373 -0
  101. forgecommandline-0.8.0.data/data/skills/library/test-driven-development/SKILL.md +398 -0
  102. forgecommandline-0.8.0.data/data/skills/library/using-agent-skills/SKILL.md +192 -0
  103. forgecommandline-0.8.0.data/data/skills/review/SKILL.md +16 -0
  104. forgecommandline-0.8.0.data/data/skills/run/SKILL.md +17 -0
  105. forgecommandline-0.8.0.data/data/skills/security-review/SKILL.md +20 -0
  106. forgecommandline-0.8.0.data/data/skills/simplify/SKILL.md +18 -0
  107. forgecommandline-0.8.0.data/data/skills/test/SKILL.md +16 -0
  108. forgecommandline-0.8.0.data/data/skills/web-build/SKILL.md +25 -0
  109. forgecommandline-0.8.0.dist-info/METADATA +357 -0
  110. forgecommandline-0.8.0.dist-info/RECORD +113 -0
  111. forgecommandline-0.8.0.dist-info/WHEEL +5 -0
  112. forgecommandline-0.8.0.dist-info/entry_points.txt +2 -0
  113. forgecommandline-0.8.0.dist-info/top_level.txt +1 -0
forgecli/art.py ADDED
@@ -0,0 +1,574 @@
1
+ """Forge's pixel-art engine: the neon-city look.
2
+
3
+ A tiny canvas with soft-edged shapes, ordered dithering and neon bloom, and two ways to put it
4
+ on screen:
5
+ * half blocks (▀ with a foreground and a background colour = two pixels per character), which
6
+ work in any truecolor terminal and are what the live city scene uses;
7
+ * sixel, real pixels, for the startup banner where the terminal supports it (Windows Terminal
8
+ 1.22+, WezTerm, foot, mlterm, iTerm2...). Pick with FORGE_GRAPHICS=auto|sixel|blocks|plain.
9
+
10
+ Everything is drawn in code from shapes and small sprite strings, so it is crisp at any size and
11
+ ships no image files. Pure Python, no dependencies.
12
+ """
13
+ from __future__ import annotations
14
+
15
+ import math
16
+ import os
17
+ import random
18
+ import sys
19
+
20
+ # The night palette. One ground (night indigo), one ink (lavender white), two neons: magenta is
21
+ # Forge itself, cyan is the machine. Amber is kept for warm, human things: windows, cargo, lamps.
22
+ NIGHT = (11, 7, 30)
23
+ INK = (236, 228, 255)
24
+ MAGENTA = (255, 79, 216)
25
+ CYAN = (62, 240, 255)
26
+ AMBER = (255, 200, 87)
27
+ GREEN = (91, 255, 157)
28
+ RED = (255, 77, 109)
29
+ LAVENDER = (160, 150, 210)
30
+ SKY = [(8, 5, 26), (20, 9, 54), (44, 13, 84), (92, 22, 112), (150, 36, 128)]
31
+
32
+ BAYER4 = [[0, 8, 2, 10], [12, 4, 14, 6], [3, 11, 1, 9], [15, 7, 13, 5]]
33
+
34
+
35
+ def mix(a, b, t: float):
36
+ return (int(a[0] + (b[0] - a[0]) * t), int(a[1] + (b[1] - a[1]) * t), int(a[2] + (b[2] - a[2]) * t))
37
+
38
+
39
+ def scale(c, k: float):
40
+ return (min(255, int(c[0] * k)), min(255, int(c[1] * k)), min(255, int(c[2] * k)))
41
+
42
+
43
+ def hash32(*parts: int) -> int:
44
+ """Cheap deterministic integer hash (same input, same answer, every run)."""
45
+ h = 2166136261
46
+ for p in parts:
47
+ h ^= p & 0xFFFFFFFF
48
+ h = (h * 16777619) & 0xFFFFFFFF
49
+ h ^= h >> 13
50
+ return h
51
+
52
+
53
+ class Canvas:
54
+ __slots__ = ("w", "h", "px", "glow")
55
+
56
+ def __init__(self, w: int, h: int, bg=NIGHT):
57
+ self.w, self.h = w, h
58
+ self.px = [[bg] * w for _ in range(h)]
59
+ self.glow = None
60
+
61
+ def copy(self) -> "Canvas":
62
+ c = Canvas.__new__(Canvas)
63
+ c.w, c.h = self.w, self.h
64
+ c.px = [row[:] for row in self.px]
65
+ c.glow = None
66
+ return c
67
+
68
+ # pixels -------------------------------------------------------------------------
69
+ def put(self, x: int, y: int, c, a: float = 1.0) -> None:
70
+ if 0 <= x < self.w and 0 <= y < self.h:
71
+ if a >= 0.999:
72
+ self.px[y][x] = c
73
+ elif a > 0.004:
74
+ self.px[y][x] = mix(self.px[y][x], c, a)
75
+
76
+ def light(self, x: int, y: int, c, s: float = 1.0) -> None:
77
+ """Add neon light at a pixel; bloom() spreads it into a glow."""
78
+ if 0 <= x < self.w and 0 <= y < self.h:
79
+ if self.glow is None:
80
+ self.glow = [[0.0] * (self.w * 3) for _ in range(self.h)]
81
+ g = self.glow[y]
82
+ i = x * 3
83
+ g[i] += c[0] * s
84
+ g[i + 1] += c[1] * s
85
+ g[i + 2] += c[2] * s
86
+
87
+ def bloom(self, radius: int = 1, strength: float = 0.6) -> None:
88
+ """Blur the light layer (box blur, twice) and add it on top: the neon glow."""
89
+ if self.glow is None:
90
+ return
91
+ w, h, g = self.w, self.h, self.glow
92
+ for _ in range(2):
93
+ # horizontal
94
+ for y in range(h):
95
+ row = g[y]
96
+ out = [0.0] * (w * 3)
97
+ for ch in range(3):
98
+ acc = 0.0
99
+ win = []
100
+ vals = row[ch::3]
101
+ pref = [0.0]
102
+ for v in vals:
103
+ pref.append(pref[-1] + v)
104
+ for x in range(w):
105
+ lo, hi = max(0, x - radius), min(w, x + radius + 1)
106
+ out[x * 3 + ch] = (pref[hi] - pref[lo]) / (2 * radius + 1)
107
+ g[y] = out
108
+ # vertical
109
+ for x3 in range(w * 3):
110
+ col = [g[y][x3] for y in range(h)]
111
+ pref = [0.0]
112
+ for v in col:
113
+ pref.append(pref[-1] + v)
114
+ for y in range(h):
115
+ lo, hi = max(0, y - radius), min(h, y + radius + 1)
116
+ g[y][x3] = (pref[hi] - pref[lo]) / (2 * radius + 1)
117
+ for y in range(h):
118
+ row, gr = self.px[y], g[y]
119
+ for x in range(w):
120
+ i = x * 3
121
+ r, gg, b = gr[i], gr[i + 1], gr[i + 2]
122
+ if r + gg + b > 3:
123
+ c = row[x]
124
+ row[x] = (min(255, int(c[0] + r * strength)), min(255, int(c[1] + gg * strength)),
125
+ min(255, int(c[2] + b * strength)))
126
+ self.glow = None
127
+
128
+ # shapes (float pixel coordinates, soft one-pixel edges) ------------------------------
129
+ def rect(self, x: int, y: int, w: int, h: int, c, a: float = 1.0) -> None:
130
+ for yy in range(max(0, y), min(self.h, y + h)):
131
+ for xx in range(max(0, x), min(self.w, x + w)):
132
+ self.put(xx, yy, c, a)
133
+
134
+ def ellipse(self, cx: float, cy: float, rx: float, ry: float, c, a: float = 1.0, glow: float = 0.0) -> None:
135
+ if rx <= 0 or ry <= 0:
136
+ return
137
+ r = min(rx, ry)
138
+ for y in range(max(0, int(cy - ry - 1)), min(self.h, int(cy + ry + 2))):
139
+ py = (y + 0.5 - cy) / ry
140
+ for x in range(max(0, int(cx - rx - 1)), min(self.w, int(cx + rx + 2))):
141
+ d = math.hypot((x + 0.5 - cx) / rx, py)
142
+ cov = 0.5 - (d - 1.0) * r
143
+ if cov > 0:
144
+ self.put(x, y, c, a * min(1.0, cov))
145
+ if glow:
146
+ self.light(x, y, c, glow * min(1.0, cov))
147
+
148
+ def poly(self, pts, c, a: float = 1.0, glow: float = 0.0) -> None:
149
+ xs = [p[0] for p in pts]
150
+ ys = [p[1] for p in pts]
151
+ edges = list(zip(pts, pts[1:] + pts[:1]))
152
+ for y in range(max(0, int(min(ys)) - 1), min(self.h, int(max(ys)) + 2)):
153
+ py = y + 0.5
154
+ for x in range(max(0, int(min(xs)) - 1), min(self.w, int(max(xs)) + 2)):
155
+ px = x + 0.5
156
+ inside = False
157
+ dist = 1e9
158
+ for (x1, y1), (x2, y2) in edges:
159
+ if (y1 > py) != (y2 > py) and px < (x2 - x1) * (py - y1) / ((y2 - y1) or 1e-9) + x1:
160
+ inside = not inside
161
+ dx, dy = x2 - x1, y2 - y1
162
+ t = max(0.0, min(1.0, ((px - x1) * dx + (py - y1) * dy) / ((dx * dx + dy * dy) or 1e-9)))
163
+ dist = min(dist, math.hypot(px - x1 - t * dx, py - y1 - t * dy))
164
+ cov = 0.5 + dist if inside else 0.5 - dist
165
+ if cov > 0:
166
+ self.put(x, y, c, a * min(1.0, cov))
167
+ if glow:
168
+ self.light(x, y, c, glow * min(1.0, cov))
169
+
170
+ def gradient(self, stops, y0: int, y1: int, dither: bool = True) -> None:
171
+ """Vertical sky gradient in hard bands with ordered (Bayer) dithering between them."""
172
+ n = len(stops) - 1
173
+ span = max(1, y1 - y0)
174
+ for y in range(max(0, y0), min(self.h, y1)):
175
+ t = (y - y0) / span * n
176
+ i = min(n - 1, int(t))
177
+ f = t - i
178
+ row = self.px[y]
179
+ for x in range(self.w):
180
+ th = (BAYER4[y % 4][x % 4] + 0.5) / 16 if dither else 0.5
181
+ row[x] = stops[i + 1] if f > th else stops[i]
182
+
183
+ def sprite(self, rows, palette: dict, x: int, y: int, flip: bool = False, glow: dict | None = None,
184
+ a: float = 1.0) -> None:
185
+ """Draw a pixel sprite given as strings ('.' is transparent)."""
186
+ for dy, row in enumerate(rows):
187
+ if flip:
188
+ row = row[::-1]
189
+ for dx, ch in enumerate(row):
190
+ if ch == "." or ch == " ":
191
+ continue
192
+ c = palette.get(ch)
193
+ if c is None:
194
+ continue
195
+ self.put(x + dx, y + dy, c, a)
196
+ if glow and ch in glow:
197
+ self.light(x + dx, y + dy, c, glow[ch])
198
+
199
+ def text(self, word: str, x: int, y: int, c, font=None, k: int = 1, glow: float = 0.0, gap: int = 1) -> int:
200
+ """Pixel text; returns the width drawn."""
201
+ font = font or FONT3
202
+ cx = x
203
+ for chr_ in word.upper():
204
+ glyph = font.get(chr_)
205
+ if glyph is None:
206
+ cx += 2 * k
207
+ continue
208
+ for gy, row in enumerate(glyph):
209
+ for gx, bit in enumerate(row):
210
+ if bit != ".":
211
+ for sy in range(k):
212
+ for sx in range(k):
213
+ self.put(cx + gx * k + sx, y + gy * k + sy, c)
214
+ if glow:
215
+ self.light(cx + gx * k + sx, y + gy * k + sy, c, glow)
216
+ cx += (len(glyph[0]) + gap) * k
217
+ return cx - x - gap * k
218
+
219
+ # output -------------------------------------------------------------------------
220
+ def ansi(self, x0: int = 0, w: int | None = None) -> str:
221
+ """Half-block rendering: each character shows two pixels stacked."""
222
+ w = self.w if w is None else w
223
+ out = []
224
+ esc_fg, esc_bg = {}, {}
225
+ for y in range(0, self.h - 1, 2):
226
+ top, bot = self.px[y], self.px[y + 1]
227
+ line = []
228
+ fg = bg = None
229
+ for x in range(x0, x0 + w):
230
+ t, b = top[x], bot[x]
231
+ if b != bg:
232
+ s = esc_bg.get(b)
233
+ if s is None:
234
+ s = esc_bg[b] = f"\x1b[48;2;{b[0]};{b[1]};{b[2]}m"
235
+ line.append(s)
236
+ bg = b
237
+ if t == b:
238
+ line.append(" ")
239
+ continue
240
+ if t != fg:
241
+ s = esc_fg.get(t)
242
+ if s is None:
243
+ s = esc_fg[t] = f"\x1b[38;2;{t[0]};{t[1]};{t[2]}m"
244
+ line.append(s)
245
+ fg = t
246
+ line.append("▀")
247
+ line.append("\x1b[0m")
248
+ out.append("".join(line))
249
+ return "\n".join(out)
250
+
251
+ def sixel(self, k: int = 3) -> str:
252
+ """Real pixels: each canvas pixel becomes a k x k square. Colours are quantised to a
253
+ 6x6x6 cube with ordered dithering, which reads as deliberate pixel-art shading."""
254
+ w, h = self.w, self.h
255
+ pal: dict = {}
256
+ grid = []
257
+ for y in range(h):
258
+ row = []
259
+ for x in range(w):
260
+ c = self.px[y][x]
261
+ th = (BAYER4[y % 4][x % 4] + 0.5) / 16 - 0.5
262
+ q = tuple(max(0, min(5, int(v * 5 / 255 + 0.5 + th * 0.9))) for v in c)
263
+ i = pal.get(q)
264
+ if i is None:
265
+ i = pal[q] = len(pal)
266
+ row.append(i)
267
+ grid.append(row)
268
+ W, H = w * k, h * k
269
+ out = ["\x1bP0;1;0q", f'"1;1;{W};{H}']
270
+ for q, i in pal.items():
271
+ out.append(f"#{i};2;{q[0] * 20};{q[1] * 20};{q[2] * 20}")
272
+ for band in range(0, H, 6):
273
+ per: dict = {}
274
+ for x in range(w):
275
+ for kk in range(6):
276
+ yy = band + kk
277
+ if yy >= H:
278
+ break
279
+ ci = grid[yy // k][x]
280
+ bits = per.setdefault(ci, [0] * w)
281
+ bits[x] |= 1 << kk
282
+ chunks = []
283
+ for ci, bits in per.items():
284
+ s = [f"#{ci}"]
285
+ x = 0
286
+ while x < w:
287
+ b = bits[x]
288
+ run = 1
289
+ while x + run < w and bits[x + run] == b:
290
+ run += 1
291
+ n = run * k
292
+ ch = chr(63 + b)
293
+ s.append(f"!{n}{ch}" if n > 3 else ch * n)
294
+ x += run
295
+ chunks.append("".join(s))
296
+ out.append("$".join(chunks) + "-")
297
+ out.append("\x1b\\")
298
+ return "".join(out)
299
+
300
+
301
+ # fonts: '#' is a lit pixel ---------------------------------------------------------------
302
+ FONT3 = {
303
+ "A": ["###", "#.#", "###", "#.#", "#.#"], "C": ["###", "#..", "#..", "#..", "###"],
304
+ "D": ["##.", "#.#", "#.#", "#.#", "##."], "E": ["###", "#..", "##.", "#..", "###"],
305
+ "G": ["###", "#..", "#.#", "#.#", "###"], "H": ["#.#", "#.#", "###", "#.#", "#.#"],
306
+ "I": ["###", ".#.", ".#.", ".#.", "###"], "K": ["#.#", "#.#", "##.", "#.#", "#.#"],
307
+ "L": ["#..", "#..", "#..", "#..", "###"], "M": ["#.#", "###", "###", "#.#", "#.#"],
308
+ "N": ["##.", "#.#", "#.#", "#.#", "#.#"], "O": ["###", "#.#", "#.#", "#.#", "###"],
309
+ "P": ["###", "#.#", "###", "#..", "#.."], "Q": ["###", "#.#", "#.#", "###", "..#"],
310
+ "R": ["##.", "#.#", "##.", "#.#", "#.#"], "S": ["###", "#..", "###", "..#", "###"],
311
+ "T": ["###", ".#.", ".#.", ".#.", ".#."], "U": ["#.#", "#.#", "#.#", "#.#", "###"],
312
+ "V": ["#.#", "#.#", "#.#", ".#.", ".#."], "W": ["#.#", "#.#", "###", "###", "#.#"],
313
+ "F": ["###", "#..", "##.", "#..", "#.."], "!": ["#", "#", "#", ".", "#"],
314
+ "?": ["##.", "..#", ".#.", "...", ".#."], ".": [".", ".", ".", ".", "#"],
315
+ "0": ["###", "#.#", "#.#", "#.#", "###"], "1": [".#.", "##.", ".#.", ".#.", "###"],
316
+ "2": ["##.", "..#", ".#.", "#..", "###"], "3": ["##.", "..#", ".#.", "..#", "##."],
317
+ "4": ["#.#", "#.#", "###", "..#", "..#"], "5": ["###", "#..", "##.", "..#", "##."],
318
+ "6": [".##", "#..", "###", "#.#", "###"], "7": ["###", "..#", ".#.", ".#.", ".#."],
319
+ "8": ["###", "#.#", "###", "#.#", "###"], "9": ["###", "#.#", "###", "..#", "##."],
320
+ "V_": ["#.#", "#.#", "#.#", ".#.", ".#."],
321
+ }
322
+ FONT5 = {
323
+ "F": ["#####", "#....", "#....", "####.", "#....", "#....", "#...."],
324
+ "O": [".###.", "#...#", "#...#", "#...#", "#...#", "#...#", ".###."],
325
+ "R": ["####.", "#...#", "#...#", "####.", "#.#..", "#..#.", "#...#"],
326
+ "G": [".###.", "#...#", "#....", "#.###", "#...#", "#...#", ".###."],
327
+ "E": ["#####", "#....", "#....", "####.", "#....", "#....", "#####"],
328
+ }
329
+
330
+
331
+ # the mascot: a tiger cub's head, drawn from shapes ------------------------------------------
332
+ FUR = (255, 132, 44)
333
+ FUR_LIGHT = (255, 170, 82)
334
+ FUR_SHADE = (222, 88, 30)
335
+ CREAM = (255, 236, 214)
336
+ WHITE = (255, 248, 240)
337
+ STRIPE = (44, 12, 32)
338
+ OUTLINE = (26, 8, 30)
339
+ NOSE = (255, 92, 150)
340
+
341
+
342
+ def tiger(cv: Canvas, cx: float, cy: float, size: float, glow: bool = True) -> None:
343
+ """A tiger cub's head, about `size` pixels tall, centred at (cx, cy), lit by the city:
344
+ magenta neon on its left edge, cyan on its right, eyes glowing cyan."""
345
+ s = size / 84.0
346
+
347
+ def P(x, y):
348
+ return (cx + x * s, cy + y * s)
349
+
350
+ def E(x, y, rx, ry, c, a=1.0, g=0.0):
351
+ cv.ellipse(cx + x * s, cy + y * s, rx * s, ry * s, c, a, g if glow else 0.0)
352
+
353
+ def Q(pts, c, g=0.0):
354
+ cv.poly([P(x, y) for x, y in pts], c, 1.0, g if glow else 0.0)
355
+
356
+ # rim light from the neon city, then the outline, then the fur
357
+ for sx, col in ((-1, MAGENTA), (1, CYAN)):
358
+ E(sx * 27 + sx * 2.6, -26, 14.5, 14.5, col, 1.0, 0.5)
359
+ E(sx * 3.2, 2, 42, 37, col, 1.0, 0.35)
360
+ for sx in (-1, 1):
361
+ E(sx * 27, -26, 14.5, 14.5, OUTLINE)
362
+ E(0, 2, 41, 36, OUTLINE)
363
+ for sx in (-1, 1):
364
+ E(sx * 27, -26, 12.5, 12.5, FUR)
365
+ E(sx * 26, -23.5, 7, 7, CREAM)
366
+ Q([(sx * 34, -34), (sx * 39, -27), (sx * 31, -30)], STRIPE)
367
+ E(0, 2, 39.5, 34.5, FUR)
368
+ E(0, 15, 36, 20, FUR_SHADE)
369
+ E(0, 4, 37, 22, FUR)
370
+ E(-7, -12, 24, 14, FUR_LIGHT)
371
+ # stripes: forehead, brows, sides
372
+ Q([(-3.5, -32), (3.5, -32), (0, -16)], STRIPE)
373
+ Q([(-15, -29), (-9, -31), (-8, -19)], STRIPE)
374
+ Q([(9, -31), (15, -29), (8, -19)], STRIPE)
375
+ for sx in (-1, 1):
376
+ Q([(sx * 40, -4), (sx * 40, 4), (sx * 26, 1)], STRIPE)
377
+ Q([(sx * 38.5, 10), (sx * 36.5, 16), (sx * 27, 11)], STRIPE)
378
+ Q([(sx * 24, -12.5), (sx * 10, -14), (sx * 13, -9.5)], STRIPE)
379
+ # cheek fluff and muzzle
380
+ for sx in (-1, 1):
381
+ E(sx * 19, 18, 14, 10, CREAM)
382
+ Q([(sx * 31, 12), (sx * 43, 19), (sx * 30, 22)], CREAM)
383
+ Q([(sx * 30, 20), (sx * 38, 28), (sx * 24, 26)], CREAM)
384
+ Q([(sx * 41, 13), (sx * 44, 18), (sx * 35, 16)], STRIPE)
385
+ E(0, 19, 10.5, 7.5, WHITE)
386
+ E(0, 29, 8, 4.5, CREAM)
387
+ # eyes: glowing cyan with a slit pupil and a white catchlight
388
+ for sx in (-1, 1):
389
+ E(sx * 15, 1.5, 9, 9.5, OUTLINE)
390
+ E(sx * 15, 2, 7.2, 7.8, CYAN, 1.0, 0.9)
391
+ E(sx * 15, 3.6, 4.6, 4.4, (24, 170, 210))
392
+ E(sx * 15, 2.5, 2.2, 6.2, (12, 4, 20))
393
+ E(sx * 15 - 2.6, -1.8, 2.1, 2.1, (255, 255, 255))
394
+ # nose and mouth
395
+ Q([(-7, 10.5), (7, 10.5), (0, 17.5)], NOSE)
396
+ E(-1.8, 12.3, 1.6, 1.0, (255, 170, 205))
397
+ Q([(-0.6, 17), (0.6, 17), (0.6, 21), (-0.6, 21)], STRIPE)
398
+ Q([(-0.5, 20.2), (0.3, 21.2), (-6, 24), (-7.4, 22.6)], STRIPE)
399
+ Q([(0.5, 20.2), (-0.3, 21.2), (6, 24), (7.4, 22.6)], STRIPE)
400
+ for sx in (-1, 1):
401
+ for dx, dy in ((7, 18), (10, 20), (8.5, 22)):
402
+ E(sx * dx, dy, 0.9, 0.9, (200, 160, 150))
403
+
404
+
405
+ # the neon city behind the banner ------------------------------------------------------------
406
+ def skyline(cv: Canvas, horizon: int, seed: int = 7, neon: bool = True, stars: bool = True,
407
+ moon=None) -> list:
408
+ """Sky, stars, moon and two rows of buildings down to `horizon`. Returns lit windows as
409
+ (x, y, colour) so animated scenes can switch them on and off."""
410
+ rnd = random.Random(seed)
411
+ w = cv.w
412
+ cv.gradient(SKY, 0, horizon + 1)
413
+ if stars:
414
+ for _ in range(max(4, w * horizon // 90)):
415
+ x, y = rnd.randrange(w), rnd.randrange(max(1, horizon * 2 // 3))
416
+ cv.put(x, y, mix(cv.px[y][x], (255, 230, 255), rnd.choice((0.35, 0.55, 0.8))))
417
+ if moon is not False:
418
+ mx, my = moon or (int(w * 0.9), max(2, horizon // 5))
419
+ mr = max(1.2, horizon / 11)
420
+ cv.ellipse(mx, my, mr * 1.9, mr * 1.9, (110, 40, 130), 0.35)
421
+ cv.ellipse(mx, my, mr, mr, (255, 220, 248), 1.0, 0.25)
422
+ cv.ellipse(mx + mr * 0.35, my - mr * 0.2, mr * 0.25, mr * 0.25, (230, 190, 225), 1.0)
423
+ windows = []
424
+ for layer, (lo, hi, col, lit_col, chance) in enumerate((
425
+ (0.35, 0.75, (34, 16, 72), (120, 80, 170), 0.25),
426
+ (0.15, 0.5, (17, 9, 40), None, 0.33))):
427
+ x = -rnd.randrange(4)
428
+ while x < w:
429
+ bw = rnd.randrange(5, 12) if horizon > 14 else rnd.randrange(4, 9)
430
+ top = horizon - int(horizon * rnd.uniform(lo, hi))
431
+ cv.rect(x, top, bw, horizon - top + 1, col)
432
+ if layer == 1 and rnd.random() < 0.3 and top > 2:
433
+ ax = x + bw // 2
434
+ cv.rect(ax, top - 2, 1, 2, (60, 40, 90))
435
+ windows.append((ax, top - 2, RED, "blink"))
436
+ step = 2
437
+ for wy in range(top + 2, horizon - 1, step):
438
+ for wx in range(x + 1, x + bw - 1, 2):
439
+ if rnd.random() < chance:
440
+ c = lit_col or rnd.choice((AMBER, AMBER, AMBER, CYAN, MAGENTA, (255, 170, 120)))
441
+ windows.append((wx, wy, c, "win" if layer else "far"))
442
+ if neon and layer == 1 and rnd.random() < 0.22 and horizon - top > 6:
443
+ nx = x + (0 if rnd.random() < 0.5 else bw - 1)
444
+ ncol = rnd.choice((MAGENTA, CYAN, (255, 120, 60)))
445
+ for ny in range(top + 2, min(horizon - 1, top + 2 + rnd.randrange(3, 7))):
446
+ windows.append((nx, ny, ncol, "neon"))
447
+ x += bw + (rnd.randrange(0, 2) if layer else rnd.randrange(-1, 2))
448
+ return windows
449
+
450
+
451
+ def windows_on(cv: Canvas, windows, now: float, motion: bool = True) -> None:
452
+ """Light the windows; some switch on and off slowly, antenna lights blink, neon flickers."""
453
+ t = now if motion else 0.0
454
+ for i, (x, y, c, kind) in enumerate(windows):
455
+ if kind == "blink":
456
+ if int(t * 1.2 + i) % 2 == 0:
457
+ cv.put(x, y, c)
458
+ cv.light(x, y, c, 0.8)
459
+ continue
460
+ if kind == "neon":
461
+ flick = hash32(i, int(t * 6)) % 41 == 0
462
+ if not flick:
463
+ cv.put(x, y, c)
464
+ cv.light(x, y, c, 0.7)
465
+ continue
466
+ period = 4 + (hash32(i, 3) % 9)
467
+ on = hash32(i, x, int((t + i * 0.37) / period)) % 100 < (78 if kind == "far" else 70)
468
+ if on:
469
+ cv.put(x, y, scale(c, 0.55) if kind == "far" else c)
470
+ if kind == "win":
471
+ cv.light(x, y, c, 0.18)
472
+
473
+
474
+ def street(cv: Canvas, y0: int, now: float, motion: bool = True) -> None:
475
+ """Wet street from row y0 down: a neon curb, then the city mirrored in the puddles."""
476
+ if y0 >= cv.h:
477
+ return
478
+ t = now if motion else 0.0
479
+ curb = mix((60, 30, 110), CYAN, 0.35)
480
+ for x in range(cv.w):
481
+ cv.px[y0][x] = curb
482
+ for k, y in enumerate(range(y0 + 1, cv.h)):
483
+ src = y0 - 1 - k
484
+ if src < 0:
485
+ break
486
+ shift = int(round(1.2 * math.sin(t * 2.1 + y * 1.7)))
487
+ srow = cv.px[src]
488
+ row = cv.px[y]
489
+ for x in range(cv.w):
490
+ c = srow[min(cv.w - 1, max(0, x + shift))]
491
+ shimmer = 0.34 + 0.1 * math.sin(x * 0.9 + t * 3.0 + y * 2.3) - k * 0.04
492
+ row[x] = mix((9, 6, 24), c, max(0.12, shimmer))
493
+
494
+
495
+ def neon_word(cv: Canvas, word: str, x: int, y: int, k: int, tube=MAGENTA, core=(255, 214, 246),
496
+ font=None) -> int:
497
+ """A neon-tube sign: continuous tubes with a hot pale core where the stroke is thick enough,
498
+ and bloom around them."""
499
+ font = font or FONT5
500
+ lit = set()
501
+ cx = x
502
+ for ch in word:
503
+ glyph = font[ch]
504
+ for gy, row in enumerate(glyph):
505
+ for gx, bit in enumerate(row):
506
+ if bit == "#":
507
+ for sy in range(k):
508
+ for sx in range(k):
509
+ lit.add((cx + gx * k + sx, y + gy * k + sy))
510
+ cx += (len(glyph[0]) + 1) * k
511
+ for (px, py) in lit:
512
+ inner = k >= 3 and all(n in lit for n in ((px - 1, py), (px + 1, py), (px, py - 1), (px, py + 1),
513
+ (px - 1, py - 1), (px + 1, py + 1), (px - 1, py + 1), (px + 1, py - 1)))
514
+ cv.put(px, py, core if inner else tube)
515
+ cv.light(px, py, tube, 1.0)
516
+ return cx - x - k
517
+
518
+
519
+ def banner(w: int, h: int, version: str = "") -> Canvas:
520
+ """The startup scene: a big neon-lit tiger cub over the night city, FORGE as a neon sign."""
521
+ cv = Canvas(w, h)
522
+ horizon = int(h * 0.82)
523
+ s = (horizon - 1) / 80.0 # the head spans about -41..+38 in tiger() units
524
+ size = 84 * s
525
+ tx = 46 * s + 2
526
+ left = int(tx + 46 * s) + 3
527
+ room = w - left - 2
528
+ k = max(1, min(int(h * 0.48 / 7), room // 30))
529
+ word_w = 5 * 6 * k - k
530
+ wy = max(1, int(horizon * 0.46 - 3.5 * k))
531
+ sx = left + max(0, (room - word_w) // 2)
532
+ windows = skyline(cv, horizon, seed=11, moon=(min(w - 4, sx + word_w + max(4, k * 3)), max(3, wy // 2 + 2))
533
+ if sx + word_w + k * 5 < w else False)
534
+ windows_on(cv, windows, 0.0, motion=False)
535
+ tiger(cv, tx, horizon - 38.5 * s - 1, size)
536
+ neon_word(cv, "FORGE", sx, wy, k)
537
+ if version:
538
+ vy = wy + 7 * k + max(2, k)
539
+ if vy + 5 < horizon:
540
+ cv.text(version, sx + k, vy, CYAN, FONT3, 1, 0.6)
541
+ cv.bloom(radius=max(1, h // 22), strength=0.55)
542
+ street(cv, horizon + 1, 0.0, motion=False)
543
+ return cv
544
+
545
+
546
+ def mascot(h: int) -> Canvas:
547
+ """Just the tiger, on transparent-looking night, for the goodbye screen."""
548
+ w = int(h * 1.25)
549
+ cv = Canvas(w, h)
550
+ cv.gradient([(8, 5, 26), (20, 9, 54), (44, 13, 84)], 0, h)
551
+ tiger(cv, w / 2, h * 0.52, h * 0.94)
552
+ cv.bloom(radius=1, strength=0.5)
553
+ return cv
554
+
555
+
556
+ # what the terminal can show ------------------------------------------------------------------
557
+ def graphics(console=None) -> str:
558
+ """'sixel', 'blocks' or 'plain' for this terminal (FORGE_GRAPHICS overrides)."""
559
+ pick = os.environ.get("FORGE_GRAPHICS", "auto").strip().lower()
560
+ if pick in ("sixel", "blocks", "plain"):
561
+ return pick
562
+ if os.environ.get("NO_COLOR"):
563
+ return "plain"
564
+ if console is not None:
565
+ if not console.is_terminal or console.color_system not in ("truecolor", "256"):
566
+ return "plain"
567
+ elif not sys.stdout.isatty():
568
+ return "plain"
569
+ if os.environ.get("WT_SESSION") and os.environ.get("TERM_PROGRAM", "").lower() != "vscode":
570
+ return "sixel" # Windows Terminal 1.22+ draws sixel images
571
+ term = (os.environ.get("TERM_PROGRAM", "") + " " + os.environ.get("TERM", "")).lower()
572
+ if any(t in term for t in ("wezterm", "foot", "mlterm", "contour", "iterm")):
573
+ return "sixel"
574
+ return "blocks"