kdcube-cli 0.0.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.
kdcube_cli/__init__.py ADDED
@@ -0,0 +1 @@
1
+ # SPDX-License-Identifier: MIT
kdcube_cli/banner.py ADDED
@@ -0,0 +1,476 @@
1
+ import random
2
+ import sys
3
+ import os
4
+ import shutil
5
+
6
+ # Enable ANSI escape sequences on Windows
7
+ if os.name == 'nt':
8
+ os.system('color')
9
+
10
+ # --- Terminal color capability detection -----------------------------------
11
+ #
12
+ # macOS Terminal.app supports 256 colors but NOT 24-bit truecolor.
13
+ # Modern terminals (VS Code, JetBrains, iTerm2, Windows Terminal, Linux)
14
+ # advertise truecolor support via COLORTERM=truecolor or COLORTERM=24bit.
15
+ #
16
+ # Detection priority:
17
+ # 1. COLORTERM=truecolor / 24bit → truecolor confirmed
18
+ # 2. TERM_PROGRAM=Apple_Terminal → 256-color fallback
19
+ # 3. Anything else → assume truecolor (safe for modern terminals)
20
+
21
+ def _detect_truecolor() -> bool:
22
+ """Return True if the terminal supports 24-bit truecolor ANSI escape codes."""
23
+ colorterm = os.environ.get("COLORTERM", "").lower()
24
+ if colorterm in ("truecolor", "24bit"):
25
+ return True
26
+ if os.environ.get("TERM_PROGRAM") == "Apple_Terminal":
27
+ return False
28
+ return True # VS Code, JetBrains, iTerm2, Windows Terminal, etc.
29
+
30
+ _TRUECOLOR = _detect_truecolor()
31
+
32
+ # --- 256-color fallback helper ----------------------------------------------
33
+ _CUBE_VALS = [0, 95, 135, 175, 215, 255]
34
+
35
+ def _rgb_to_256(r: int, g: int, b: int) -> int:
36
+ """Convert an RGB triple to the nearest ANSI 256-color palette index."""
37
+ def _nearest(v):
38
+ return min(range(6), key=lambda i: abs(_CUBE_VALS[i] - v))
39
+
40
+ ri, gi, bi = _nearest(r), _nearest(g), _nearest(b)
41
+ cube_idx = 16 + 36 * ri + 6 * gi + bi
42
+ cr, cg, cb = _CUBE_VALS[ri], _CUBE_VALS[gi], _CUBE_VALS[bi]
43
+ cube_dist = (cr - r) ** 2 + (cg - g) ** 2 + (cb - b) ** 2
44
+
45
+ # Also compare against the 24-step grayscale ramp (indices 232–255)
46
+ luma = int(0.299 * r + 0.587 * g + 0.114 * b)
47
+ gray_n = max(0, min(23, round((luma - 8) / 10)))
48
+ gray_idx = 232 + gray_n
49
+ gv = 8 + 10 * gray_n
50
+ gray_dist = (gv - r) ** 2 + (gv - g) ** 2 + (gv - b) ** 2
51
+
52
+ return cube_idx if cube_dist <= gray_dist else gray_idx
53
+
54
+ # --- ANSI Truecolor Definitions ---
55
+ RESET = "\033[0m"
56
+ RESET_BG = "\033[49m" # reset background only (keep foreground)
57
+
58
+ def rgb_fg(r, g, b):
59
+ if _TRUECOLOR:
60
+ return f"\033[38;2;{r};{g};{b}m"
61
+ return f"\033[38;5;{_rgb_to_256(r, g, b)}m"
62
+
63
+ def rgb_bg(r, g, b):
64
+ if _TRUECOLOR:
65
+ return f"\033[48;2;{r};{g};{b}m"
66
+ return f"\033[48;5;{_rgb_to_256(r, g, b)}m"
67
+
68
+ # Background-color palette (main robot + label)
69
+ COLORS = {
70
+ "0": RESET,
71
+ "1": rgb_bg(198, 243, 241), # Light Cyan
72
+ "2": rgb_bg(1, 190, 178), # Teal
73
+ "3": rgb_bg(67, 114, 195), # Blue
74
+ "4": rgb_bg(255, 255, 255), # White
75
+ "5": rgb_bg(6, 16, 30), # Dark Navy
76
+ "6": rgb_bg(107, 99, 254), # Purple
77
+ "t": rgb_bg(198, 243, 241), # label top face (light cyan)
78
+ "f": rgb_bg(1, 190, 178), # label front face (teal)
79
+ "s": rgb_bg(67, 114, 195), # label side face (blue)
80
+ }
81
+
82
+ # Foreground-color palette (half-block upper pixel)
83
+ FG = {
84
+ "0": "", # transparent — no colour set
85
+ "1": rgb_fg(198, 243, 241),
86
+ "2": rgb_fg(1, 190, 178),
87
+ "3": rgb_fg(67, 114, 195),
88
+ "4": rgb_fg(255, 255, 255),
89
+ "5": rgb_fg(6, 16, 30),
90
+ "6": rgb_fg(107, 99, 254),
91
+ }
92
+
93
+ def _halfblock_char(top: str, bot: str) -> str:
94
+ """
95
+ Combine two pixel values into one terminal character using half-blocks.
96
+ ▀ (U+2580) = upper half filled with FG colour, lower half = BG colour
97
+ ▄ (U+2584) = lower half filled with FG colour, upper half = BG colour
98
+ Each resulting glyph is exactly 1 char wide × ½ row tall → square pixel.
99
+ """
100
+ if top == "0" and bot == "0":
101
+ return RESET + " "
102
+ if top != "0" and bot == "0":
103
+ return FG[top] + RESET_BG + "▀" + RESET
104
+ if top == "0" and bot != "0":
105
+ return FG[bot] + RESET_BG + "▄" + RESET
106
+ # both coloured: upper half = top (fg), lower half = bot (bg)
107
+ return FG[top] + COLORS[bot] + "▀" + RESET
108
+
109
+ # --- Block font (all glyphs are 7 rows tall) ---
110
+ # Uppercase = full 7-row cap height.
111
+ # Lowercase = x-height letters sit at the bottom; top rows are blank ("0000").
112
+ # 'b' has an ascender so it uses all 7 rows like a capital.
113
+ FONT_3D = {
114
+ # ── uppercase ──────────────────────────────────────────────────────────
115
+ "K": [
116
+ "10001",
117
+ "10010",
118
+ "10100",
119
+ "11000",
120
+ "10100",
121
+ "10010",
122
+ "10001",
123
+ ],
124
+ "D": [
125
+ "11110",
126
+ "10001",
127
+ "10001",
128
+ "10001",
129
+ "10001",
130
+ "10001",
131
+ "11110",
132
+ ],
133
+ "C": [
134
+ "01111",
135
+ "10000",
136
+ "10000",
137
+ "10000",
138
+ "10000",
139
+ "10000",
140
+ "01111",
141
+ ],
142
+ # ── lowercase (x-height: top 3 rows blank, letter occupies rows 3-6) ──
143
+ "u": [
144
+ "0000",
145
+ "0000",
146
+ "1001", # starts at row 2 — in sync with 'e' and bottom of 'b'
147
+ "1001",
148
+ "1001",
149
+ "1001",
150
+ "0111",
151
+ ],
152
+ "b": [ # ascender → full 7 rows
153
+ "1000",
154
+ "1000",
155
+ "1110",
156
+ "1001",
157
+ "1001",
158
+ "1001",
159
+ "1110",
160
+ ],
161
+ "e": [
162
+ "00000",
163
+ "00000",
164
+ "01110", # top arc
165
+ "10001", # inner opening ← the visible hole of the 'e'
166
+ "11110", # crossbar (open right — distinguishes 'e' from 'c')
167
+ "10000", # left stem
168
+ "01110", # bottom arc
169
+ ],
170
+ }
171
+ FONT_3D_GAP = 2 # columns of blank space between letters
172
+
173
+
174
+ # --- Pixel Art Grids (16 columns x 13 rows) ---
175
+ BOX_ROBOT = [
176
+ "0000100000006000",
177
+ "0000300000006000",
178
+ "0000300000006000",
179
+ "0011111111111100",
180
+ "0011111111111100",
181
+ "0022222222222200",
182
+ "0024442224442200",
183
+ "3324542224542233",
184
+ "3324442224442233",
185
+ "0022222222222200",
186
+ "0022255555222200",
187
+ "0022222222222200",
188
+ "0033333333333300",
189
+ ]
190
+
191
+ ISO_ROBOT = [
192
+ "0000100000060000",
193
+ "0000300000060000",
194
+ "0000300000060000",
195
+ "0000111111110000",
196
+ "0001111111122000",
197
+ "0011111111222200",
198
+ "0033222222222200",
199
+ "0033244224422233",
200
+ "0033245224522233",
201
+ "0033222222222200",
202
+ "0033222555222200",
203
+ "0033222222222200",
204
+ "0000333333333300",
205
+ ]
206
+
207
+
208
+ # --- Mini Robot (8 cols × 8 pixel-rows, rendered via half-blocks) -----------
209
+ # Half-block rendering packs 2 pixel-rows into 1 terminal row.
210
+ # 8 pixel-cols × 8 pixel-rows → 8 terminal chars × 4 terminal rows.
211
+ # At ~2:1 char aspect ratio each half-block pixel is square → 1:1 robot.
212
+ # Features pinned to cols 1 & 6 (symmetric).
213
+ # Pairing plan (each pair → 1 terminal row via half-blocks):
214
+ # pair (0,1): antenna tips on transparent + empty → tiny ▀ marks floating above head
215
+ # pair (2,3): solid head + solid head → fully SOLID light-cyan bar
216
+ # pair (4,5): white eyes + dark pupils → eye+pupil in one half-char height
217
+ # pair (6,7): teal body + blue legs → body top, leg stubs at cols 1 & 6
218
+ MINI_ROBOT = [
219
+ # Half-block pairing: every 2 pixel rows → 1 terminal row.
220
+ # Same-colour pairs → solid BG space (no ▀ seam artefact).
221
+ # Mixed-colour pairs → ▀ half-block (upper=FG, lower=BG).
222
+ #
223
+ # Rule: same colour in both rows of a pair → solid BG space (no ▀, no gaps).
224
+ # different colours → ▀ half-block (eyes only).
225
+ #
226
+ # Layout (14 pixel rows → 7 terminal rows):
227
+ # pair (0,1) : SOLID light-cyan tip — LEFT antenna only (col1); right = transparent
228
+ # pair (2,3) : SOLID shafts — blue(3) col1, purple(6) col6 (two distinct colours)
229
+ # pair (4,5) : SOLID light-cyan head bar
230
+ # pair (6,7) : 2-wide eyes cols 1-2 & 5-6 — white upper / dark lower
231
+ # pair (8,9) : SOLID dark(5) mouth — 2 pixels centered at cols 3-4
232
+ # pair (10,11): SOLID teal body
233
+ # pair (12,13): SOLID blue leg squares — cols 1 & 6
234
+ "01000000", # row 0 — left tip: light-cyan(1) col1 only, right antenna has no tip
235
+ "01000000", # row 1 — same → SOLID light-cyan tip (left only)
236
+ "03000060", # row 2 — shafts: blue(3) col1, purple(6) col6
237
+ "03000060", # row 3 — same → SOLID shafts, distinct colours
238
+ "11111111", # row 4 — head
239
+ "11111111", # row 5 — same → SOLID light-cyan
240
+ "24422442", # row 6 — face: white(4) eyes at cols 1-2 & 5-6
241
+ "25522552", # row 7 — dark(5) pupils → ▀ white upper / dark lower
242
+ "22255222", # row 8 — mouth: dark(5) at cols 3-4 (centered smile)
243
+ "22222222", # row 9 — same → SOLID dark mouth
244
+ "22222222", # row 10 — body
245
+ "22222222", # row 11 — same → SOLID teal
246
+ "03000030", # row 12 — legs: blue(3) at cols 1 & 6
247
+ "03000030", # row 13 — same → SOLID blue legs
248
+ ]
249
+
250
+
251
+ def _build_front_mask(text: str) -> list:
252
+ """Build a 2-D binary mask (list-of-lists) for the given text string."""
253
+ rows = [[] for _ in range(7)]
254
+ first = True
255
+ for ch in text: # preserve case so lowercase glyphs are looked up correctly
256
+ glyph = FONT_3D.get(ch)
257
+ if glyph is None:
258
+ continue
259
+ if not first:
260
+ for r in range(7):
261
+ rows[r].extend([0] * FONT_3D_GAP)
262
+ first = False
263
+ for r in range(7):
264
+ rows[r].extend([1 if px == "1" else 0 for px in glyph[r]])
265
+ return rows
266
+
267
+
268
+ def _build_label_3d(text: str) -> tuple:
269
+ """
270
+ Render 'text' as a 3-D extruded pixel label.
271
+
272
+ Each lit pixel becomes a tiny cube viewed from upper-left:
273
+ • front face ('f') – main teal colour, drawn at (x, y)
274
+ • top face ('t') – light cyan, drawn at (x, y-1) only for
275
+ pixels whose top neighbour is empty
276
+ • side face ('s') – blue, drawn at (x+1, y) only for
277
+ pixels whose right neighbour is empty
278
+
279
+ Draw order: side → top → front (front is never overwritten).
280
+ Returns (bitmap_rows, baseline_row).
281
+ """
282
+ front = _build_front_mask(text)
283
+ h = len(front)
284
+ w = len(front[0]) if front else 0
285
+
286
+ cells: dict = {}
287
+
288
+ # ── side faces (right-edge pixels only) ──────────────────────────────────
289
+ for y in range(h):
290
+ for x in range(w):
291
+ if front[y][x]:
292
+ right_empty = (x + 1 >= w) or (not front[y][x + 1])
293
+ if right_empty:
294
+ cells[(x + 1, y)] = "s"
295
+
296
+ # ── top faces (top-edge pixels only) ─────────────────────────────────────
297
+ for y in range(h):
298
+ for x in range(w):
299
+ if front[y][x]:
300
+ top_empty = (y - 1 < 0) or (not front[y - 1][x])
301
+ if top_empty:
302
+ # only place top face if that cell isn't already a front face
303
+ if (x, y - 1) not in cells or cells[(x, y - 1)] != "f":
304
+ cells[(x, y - 1)] = "t"
305
+
306
+ # ── front faces (drawn last – highest priority) ───────────────────────────
307
+ for y in range(h):
308
+ for x in range(w):
309
+ if front[y][x]:
310
+ cells[(x, y)] = "f"
311
+
312
+ if not cells:
313
+ return [], 0
314
+
315
+ min_x = min(x for x, _ in cells)
316
+ max_x = max(x for x, _ in cells)
317
+ min_y = min(y for _, y in cells)
318
+ max_y = max(y for _, y in cells)
319
+
320
+ out_w = max_x - min_x + 1
321
+ out_h = max_y - min_y + 1
322
+ canvas = [["." for _ in range(out_w)] for _ in range(out_h)]
323
+
324
+ for (x, y), key in cells.items():
325
+ canvas[y - min_y][x - min_x] = key
326
+
327
+ # baseline = row index of the bottom of the front faces inside the bitmap
328
+ baseline_row = (h - 1) - min_y
329
+ return ["".join(row) for row in canvas], baseline_row
330
+
331
+
332
+ def _trim_bitmap_rows(rows: list[str], blank: str = "0") -> list[str]:
333
+ if not rows:
334
+ return rows
335
+ min_x = None
336
+ max_x = None
337
+ for row in rows:
338
+ for idx, ch in enumerate(row):
339
+ if ch != blank:
340
+ min_x = idx if min_x is None else min(min_x, idx)
341
+ max_x = idx if max_x is None else max(max_x, idx)
342
+ if min_x is None or max_x is None:
343
+ return rows
344
+ return [row[min_x : max_x + 1] for row in rows]
345
+
346
+
347
+ def _terminal_columns(default: int = 120) -> int:
348
+ try:
349
+ return shutil.get_terminal_size((default, 24)).columns
350
+ except Exception:
351
+ return default
352
+
353
+
354
+ def _banner_layout(
355
+ label_width: int,
356
+ main_width: int,
357
+ mini_width: int,
358
+ ) -> tuple[str, str, str]:
359
+ """Choose a pixel width that fits the current terminal."""
360
+ columns = _terminal_columns()
361
+
362
+ full_width_double = label_width * 2 + main_width * 2 + 2 + mini_width
363
+ full_width_single = label_width + main_width + 2 + mini_width
364
+
365
+ if columns >= full_width_double:
366
+ return "full", " ", " "
367
+ if columns >= full_width_single:
368
+ return "full", " ", " "
369
+ return "stacked", " ", " "
370
+
371
+
372
+ def _render_label_row(row: str, pixel_fill: str, pixel_blank: str) -> str:
373
+ parts: list[str] = []
374
+ for ch in row:
375
+ if ch == ".":
376
+ parts.append(pixel_blank)
377
+ else:
378
+ parts.append(COLORS[ch] + pixel_fill + RESET)
379
+ return "".join(parts)
380
+
381
+
382
+ def _center_pad(rendered_width: int) -> str:
383
+ columns = _terminal_columns()
384
+ return " " * max(0, (columns - rendered_width) // 2)
385
+
386
+
387
+ def print_cli_banner():
388
+ """Print the KDCube label, main robot, and mini robot side-by-side."""
389
+ selected_robot = _trim_bitmap_rows(random.choice([BOX_ROBOT, ISO_ROBOT]))
390
+ robot_height = len(selected_robot)
391
+
392
+ label_bitmap, label_baseline = _build_label_3d("KDCube")
393
+ label_height = len(label_bitmap)
394
+ label_width = len(label_bitmap[0]) if label_bitmap else 0
395
+
396
+ mini_pix_rows = len(MINI_ROBOT) # pixel rows (8)
397
+ mini_term_rows = (mini_pix_rows + 1) // 2 # terminal rows (4, via half-blocks)
398
+ mini_width = len(MINI_ROBOT[0])
399
+ main_width = len(selected_robot[0]) if selected_robot else 0
400
+ layout_mode, pixel_fill, pixel_blank = _banner_layout(label_width, main_width, mini_width)
401
+
402
+ # Align label baseline with the last robot row
403
+ text_start = (robot_height - 1) - label_baseline
404
+ # Align mini robot bottom with main robot bottom
405
+ mini_start = robot_height - mini_term_rows
406
+
407
+ sys.stdout.write("\n")
408
+
409
+ if layout_mode == "stacked":
410
+ if label_bitmap:
411
+ label_render_width = label_width * len(pixel_fill)
412
+ label_pad = _center_pad(label_render_width)
413
+ for row in label_bitmap:
414
+ sys.stdout.write(label_pad + _render_label_row(row, pixel_fill, pixel_blank) + "\n")
415
+ sys.stdout.write("\n")
416
+
417
+ robot_render_width = main_width * len(pixel_fill) + 2 + mini_width
418
+ robot_pad = _center_pad(robot_render_width)
419
+ for i in range(robot_height):
420
+ sys.stdout.write(robot_pad)
421
+
422
+ for pixel in selected_robot[i]:
423
+ if pixel == "0":
424
+ sys.stdout.write(RESET + pixel_blank)
425
+ else:
426
+ sys.stdout.write(COLORS[pixel] + pixel_fill + RESET)
427
+
428
+ sys.stdout.write(" ")
429
+ term_rel = i - mini_start
430
+ if 0 <= term_rel < mini_term_rows:
431
+ pix = term_rel * 2
432
+ row_top = MINI_ROBOT[pix]
433
+ row_bot = MINI_ROBOT[pix + 1] if pix + 1 < mini_pix_rows else "0" * mini_width
434
+ for t, b in zip(row_top, row_bot):
435
+ sys.stdout.write(_halfblock_char(t, b))
436
+ else:
437
+ sys.stdout.write(" " * mini_width)
438
+ sys.stdout.write("\n")
439
+
440
+ sys.stdout.write("\n")
441
+ return
442
+
443
+ for i in range(robot_height):
444
+ # ── 3-D label ────────────────────────────────────────────────────────
445
+ rel = i - text_start
446
+ if 0 <= rel < label_height:
447
+ sys.stdout.write(_render_label_row(label_bitmap[rel], pixel_fill, pixel_blank))
448
+ else:
449
+ sys.stdout.write(pixel_blank * label_width)
450
+
451
+ # ── main robot ───────────────────────────────────────────────────────
452
+ for pixel in selected_robot[i]:
453
+ if pixel == "0":
454
+ sys.stdout.write(RESET + pixel_blank)
455
+ else:
456
+ sys.stdout.write(COLORS[pixel] + pixel_fill + RESET)
457
+
458
+ # ── mini robot (half-block, 1:1 square pixels) ───────────────────────
459
+ sys.stdout.write(" ")
460
+ term_rel = i - mini_start
461
+ if 0 <= term_rel < mini_term_rows:
462
+ pix = term_rel * 2
463
+ row_top = MINI_ROBOT[pix]
464
+ row_bot = MINI_ROBOT[pix + 1] if pix + 1 < mini_pix_rows else "0" * mini_width
465
+ for t, b in zip(row_top, row_bot):
466
+ sys.stdout.write(_halfblock_char(t, b))
467
+ else:
468
+ sys.stdout.write(" " * mini_width)
469
+
470
+ sys.stdout.write("\n")
471
+
472
+ sys.stdout.write("\n")
473
+
474
+
475
+ if __name__ == "__main__":
476
+ print_cli_banner()