anonymous-CAP's picture
Upload folder using huggingface_hub (part 4)
76d6ddf verified
Raw
History Blame Contribute Delete
20.6 kB
"""Interactive isometric city explorer — easter egg for `hf repos ls --explore`."""
import dataclasses
import math
import os
import random
import re
import select
import shutil
import sys
import time
from huggingface_hub.hf_api import RepoStorageInfo
from ._file_listing import format_size
Color = tuple[int, int, int]
# (top_face, left_face, right_face) — lighter to darker for 3D effect
_TYPE_COLORS: dict[str, tuple[Color, Color, Color]] = {
"model": ((175, 148, 240), (138, 112, 208), (105, 80, 180)),
"dataset": ((245, 128, 128), (222, 92, 92), (190, 60, 60)),
"space": ((245, 175, 85), (218, 140, 55), (185, 110, 30)),
"bucket": ((112, 185, 242), (70, 150, 220), (40, 118, 192)),
}
_EXTRA_COLORS: tuple[Color, Color, Color] = ((168, 176, 188), (128, 136, 148), (90, 98, 110))
_GRID_COLOR: Color = (178, 182, 190)
_DX = 4 # isometric half-width (pixels)
_DY = 2 # isometric half-height (pixels)
_MAX_H = 16 # tallest tile (pixels)
_MIN_H = 1
_COLS = 6
_EXT = 1 # grid extension beyond tiles
_MAX_TILES = 30
# Cursor sprite — pixel art arrow pointer
_OUTLINE: Color = (30, 30, 30)
_FILL: Color = (255, 255, 255)
_CURSOR_GRID = [
" X ",
" XWX ",
"XWWWX",
" XWX ",
" X ",
]
_CURSOR_PALETTE: dict[str, Color] = {
"X": _OUTLINE,
"W": _FILL,
}
_CURSOR_H = len(_CURSOR_GRID)
_MOVE_FRAMES = 8
_MOVE_DELAY = 0.03
_CURSOR_PAD = _CURSOR_H + 16
_GAP = 3
_MIN_TERM_W = 100
_MIN_TERM_H = 24
_SUMMARY_W = 24
# ---------------------------------------------------------------------------
# Data structures
# ---------------------------------------------------------------------------
@dataclasses.dataclass
class TileInfo:
grid_row: int
grid_col: int
height: int
top: Color
left: Color
right: Color
repo: RepoStorageInfo | None
@dataclasses.dataclass
class CityData:
tiles: list[TileInfo]
rows: int
cols: int
x_off: int
y_off: int
buf_w: int
buf_h: int
total_storage: int
extra_count: int
extra_storage: int
all_repos: list[RepoStorageInfo]
# ---------------------------------------------------------------------------
# City layout
# ---------------------------------------------------------------------------
def _prepare_city_data(repos: list[RepoStorageInfo]) -> CityData:
sorted_repos = sorted(repos, key=lambda r: r.storage, reverse=True)
display = sorted_repos[:_MAX_TILES]
extra_count = max(0, len(sorted_repos) - _MAX_TILES)
extra_storage = sum(r.storage for r in sorted_repos[_MAX_TILES:])
total_storage = sum(r.storage for r in repos)
max_storage = max(1, display[0].storage)
n = len(display) + (1 if extra_count > 0 else 0)
cols = min(n, _COLS)
rows = math.ceil(n / cols) if cols > 0 else 1
tiles: list[TileInfo] = []
for i, repo in enumerate(display):
r, c = divmod(i, cols)
h = max(_MIN_H, round(math.sqrt(repo.storage / max_storage) * _MAX_H))
top, left, right = _TYPE_COLORS.get(repo.type, _EXTRA_COLORS)
tiles.append(TileInfo(r, c, h, top, left, right, repo))
if extra_count > 0:
r, c = divmod(len(display), cols)
h = max(_MIN_H, round(math.sqrt(extra_storage / max_storage) * _MAX_H))
tiles.append(TileInfo(r, c, h, *_EXTRA_COLORS, None))
r_lo, r_hi = -_EXT, rows - 1 + _EXT
c_lo, c_hi = -_EXT, cols - 1 + _EXT
xs: list[int] = []
ys: list[int] = []
for rr in range(r_lo, r_hi + 1):
for cc in range(c_lo, c_hi + 1):
cx, cy = (cc - rr) * _DX, (cc + rr) * _DY
xs.extend([cx - _DX, cx + _DX])
ys.extend([cy, cy + 2 * _DY])
for tile in tiles:
ys.append((tile.grid_col + tile.grid_row) * _DY - tile.height)
x_off = -min(xs)
y_off = -min(ys)
buf_w = max(xs) - min(xs) + 1
buf_h = max(ys) - min(ys) + 1
if buf_h % 2:
buf_h += 1
return CityData(
tiles=tiles,
rows=rows,
cols=cols,
x_off=x_off,
y_off=y_off,
buf_w=buf_w,
buf_h=buf_h,
total_storage=total_storage,
extra_count=extra_count,
extra_storage=extra_storage,
all_repos=repos,
)
# ---------------------------------------------------------------------------
# Drawing primitives
# ---------------------------------------------------------------------------
def _draw_diamond_outline(buf: list[list[Color | None]], cx: int, cy: int) -> None:
t = (cx, cy)
r = (cx + _DX, cy + _DY)
b = (cx, cy + 2 * _DY)
ll = (cx - _DX, cy + _DY)
_draw_line(buf, *t, *r, _GRID_COLOR)
_draw_line(buf, *r, *b, _GRID_COLOR)
_draw_line(buf, *b, *ll, _GRID_COLOR)
_draw_line(buf, *ll, *t, _GRID_COLOR)
def _draw_block(
buf: list[list[Color | None]],
cx: int,
cy: int,
h: int,
top: Color,
left: Color,
right: Color,
) -> None:
_fill_poly(
buf,
[(cx - _DX, cy + _DY - h), (cx, cy + 2 * _DY - h), (cx, cy + 2 * _DY), (cx - _DX, cy + _DY)],
left,
)
_fill_poly(
buf,
[(cx, cy + 2 * _DY - h), (cx + _DX, cy + _DY - h), (cx + _DX, cy + _DY), (cx, cy + 2 * _DY)],
right,
)
_fill_poly(
buf,
[(cx, cy - h), (cx + _DX, cy + _DY - h), (cx, cy + 2 * _DY - h), (cx - _DX, cy + _DY - h)],
top,
)
def _fill_poly(buf: list[list[Color | None]], verts: list[tuple[int, int]], color: Color) -> None:
bh = len(buf)
bw = len(buf[0]) if buf else 0
all_y = [v[1] for v in verts]
y0 = max(0, min(all_y))
y1 = min(bh - 1, max(all_y))
n = len(verts)
for y in range(y0, y1 + 1):
xl: float = float("inf")
xr: float = float("-inf")
for i in range(n):
ax, ay = verts[i]
bx, by = verts[(i + 1) % n]
if ay == by:
if y == ay:
xl = min(xl, float(min(ax, bx)))
xr = max(xr, float(max(ax, bx)))
continue
if not (min(ay, by) <= y <= max(ay, by)):
continue
t = (y - ay) / (by - ay)
ix = ax + t * (bx - ax)
xl = min(xl, ix)
xr = max(xr, ix)
if xl <= xr:
for x in range(max(0, round(xl)), min(bw, round(xr) + 1)):
buf[y][x] = color
def _draw_line(buf: list[list[Color | None]], x0: int, y0: int, x1: int, y1: int, color: Color) -> None:
bh = len(buf)
bw = len(buf[0]) if buf else 0
dx = abs(x1 - x0)
dy = abs(y1 - y0)
steps = max(dx, dy)
if steps == 0:
if 0 <= y0 < bh and 0 <= x0 < bw:
buf[y0][x0] = color
return
xi = (x1 - x0) / steps
yi = (y1 - y0) / steps
fx, fy = float(x0), float(y0)
for _ in range(steps + 1):
px, py = round(fx), round(fy)
if 0 <= py < bh and 0 <= px < bw:
buf[py][px] = color
fx += xi
fy += yi
# ---------------------------------------------------------------------------
# Pixel buffer → terminal
# ---------------------------------------------------------------------------
_ANSI_RE = re.compile(r"\033\[[0-9;]*m")
def _strip_ansi(s: str) -> str:
return _ANSI_RE.sub("", s)
def _visible_len(s: str) -> int:
return len(_strip_ansi(s))
def _pixels_to_lines(buf: list[list[Color | None]]) -> list[str]:
height = len(buf)
width = len(buf[0]) if buf else 0
lines: list[str] = []
for row in range(0, height, 2):
last = -1
for col in range(width - 1, -1, -1):
top = buf[row][col]
bot = buf[row + 1][col] if row + 1 < height else None
if top or bot:
last = col
break
if last < 0:
lines.append("")
continue
parts: list[str] = []
cfg: Color | None = None
cbg: Color | None = None
for col in range(last + 1):
top = buf[row][col]
bot = buf[row + 1][col] if row + 1 < height else None
if not top and not bot:
if cfg is not None or cbg is not None:
parts.append("\033[0m")
cfg = cbg = None
parts.append(" ")
continue
if top and bot and top == bot:
nfg, nbg, ch = top, None, "█"
elif top and bot:
nfg, nbg, ch = bot, top, "▄"
elif top:
nfg, nbg, ch = top, None, "▀"
else:
nfg, nbg, ch = bot, None, "▄" # type: ignore[assignment]
esc = ""
if nfg != cfg:
esc += f"\033[38;2;{nfg[0]};{nfg[1]};{nfg[2]}m"
cfg = nfg
if nbg != cbg:
esc += "\033[49m" if nbg is None else f"\033[48;2;{nbg[0]};{nbg[1]};{nbg[2]}m"
cbg = nbg
parts.append(esc + ch)
if cfg is not None or cbg is not None:
parts.append("\033[0m")
lines.append("".join(parts))
return lines
# ---------------------------------------------------------------------------
# Rendering
# ---------------------------------------------------------------------------
def _render_base_buffer(city: CityData) -> list[list[Color | None]]:
buf: list[list[Color | None]] = [[None] * city.buf_w for _ in range(city.buf_h)]
for tile in city.tiles:
cx = city.x_off + (tile.grid_col - tile.grid_row) * _DX
cy = city.y_off + (tile.grid_col + tile.grid_row) * _DY
_draw_diamond_outline(buf, cx, cy)
sorted_tiles = sorted(city.tiles, key=lambda t: (t.grid_row + t.grid_col, t.grid_col))
for tile in sorted_tiles:
cx = city.x_off + (tile.grid_col - tile.grid_row) * _DX
cy = city.y_off + (tile.grid_col + tile.grid_row) * _DY
_draw_block(buf, cx, cy, tile.height, tile.top, tile.left, tile.right)
return buf
# ---------------------------------------------------------------------------
# Summary panel
# ---------------------------------------------------------------------------
def _colored_square(color: Color) -> str:
return f"\033[38;2;{color[0]};{color[1]};{color[2]}m■\033[0m"
def _build_summary(
repos: list[RepoStorageInfo],
total_storage: int,
extra_count: int,
) -> list[str]:
lines: list[str] = [""]
lines.append(" Storage Overview")
lines.append(" " + "─" * 16)
lines.append(f" {format_size(total_storage, human_readable=True)} total")
lines.append("")
order = ["model", "dataset", "space", "bucket"]
labels = {"model": "Models", "dataset": "Datasets", "space": "Spaces", "bucket": "Buckets"}
for rtype in order:
group = [r for r in repos if r.type == rtype]
if not group:
continue
storage = sum(r.storage for r in group)
sq = _colored_square(_TYPE_COLORS[rtype][0])
lines.append(f" {sq} {labels[rtype]}")
lines.append(f" {len(group)} repos · {format_size(storage, human_readable=True)}")
lines.append("")
if extra_count > 0:
sq = _colored_square(_EXTRA_COLORS[0])
lines.append(f" {sq} +{extra_count} more repos")
return lines
# ---------------------------------------------------------------------------
# Cursor
# ---------------------------------------------------------------------------
def _build_cursor() -> list[tuple[int, int, Color]]:
pixels: list[tuple[int, int, Color]] = []
for ri, row in enumerate(_CURSOR_GRID):
for ci, ch in enumerate(row):
if ch in _CURSOR_PALETTE:
pixels.append((ci - len(row) // 2, ri - _CURSOR_H + 1, _CURSOR_PALETTE[ch]))
return pixels
_CURSOR_PIXELS = _build_cursor()
# ---------------------------------------------------------------------------
# Interactive game
# ---------------------------------------------------------------------------
def run_city_game(repos: list[RepoStorageInfo]) -> None:
"""Launch the interactive city explorer."""
if not repos:
print("No repositories found.")
return
try:
import termios
import tty
except ImportError:
print("Interactive mode requires a Unix-like terminal (Linux/macOS).")
return
if not sys.stdin.isatty() or not sys.stdout.isatty():
print("Interactive mode requires a terminal.")
return
term = shutil.get_terminal_size()
if term.columns < _MIN_TERM_W or term.lines < _MIN_TERM_H:
print(f"Your terminal is {term.columns}×{term.lines} characters.")
print(f"Please resize to at least {_MIN_TERM_W}×{_MIN_TERM_H} to explore the city!")
return
city = _prepare_city_data(repos)
tiles_with_repos = [t for t in city.tiles if t.repo is not None]
start_tile = random.choice(tiles_with_repos) if tiles_with_repos else city.tiles[0]
fd = sys.stdin.fileno()
old_settings = termios.tcgetattr(fd)
try:
tty.setraw(fd)
sys.stdout.write("\033[?1049h\033[?25l\033[2J")
sys.stdout.flush()
_game_loop(city, start_tile.grid_row, start_tile.grid_col)
finally:
sys.stdout.write("\033[?25h\033[?1049l")
sys.stdout.flush()
termios.tcsetattr(fd, termios.TCSADRAIN, old_settings)
def _game_loop(city: CityData, cur_row: int, cur_col: int) -> None:
tile_map: dict[tuple[int, int], TileInfo] = {(t.grid_row, t.grid_col): t for t in city.tiles}
city = dataclasses.replace(city, buf_h=city.buf_h + _CURSOR_PAD, y_off=city.y_off + _CURSOR_PAD)
base_buf = _render_base_buffer(city)
summary = _build_summary(city.all_repos, city.total_storage, city.extra_count)
# Intro: cursor drops onto starting tile
tx, ty = _tile_top_center(city, cur_row, cur_col, tile_map)
for i in range(1, _MOVE_FRAMES + 1):
t = i / _MOVE_FRAMES
t = t * t * (3 - 2 * t)
drop_y = ty - 16 * (1 - t)
frame = _copy_buf(base_buf)
_highlight_tile(frame, city, tile_map[(cur_row, cur_col)])
_draw_cursor(frame, tx, round(drop_y))
_present(city, frame, tile_map.get((cur_row, cur_col)), summary)
time.sleep(_MOVE_DELAY)
while True:
cx, cy = _tile_top_center(city, cur_row, cur_col, tile_map)
frame = _copy_buf(base_buf)
_highlight_tile(frame, city, tile_map[(cur_row, cur_col)])
_draw_cursor(frame, cx, cy)
_present(city, frame, tile_map.get((cur_row, cur_col)), summary)
key = _read_key()
if key in ("q", "Q", "esc", "\x03"):
return
dr, dc = _key_to_direction(key)
if dr == 0 and dc == 0:
continue
nr, nc = cur_row + dr, cur_col + dc
if (nr, nc) not in tile_map:
continue
ex, ey = _tile_top_center(city, nr, nc, tile_map)
for i in range(1, _MOVE_FRAMES + 1):
t = i / _MOVE_FRAMES
t = t * t * (3 - 2 * t)
bx = cx + (ex - cx) * t
by = cy + (ey - cy) * t
frame = _copy_buf(base_buf)
_highlight_tile(frame, city, tile_map[(nr, nc)])
_draw_cursor(frame, round(bx), round(by))
_present(city, frame, tile_map.get((nr, nc)), summary)
time.sleep(_MOVE_DELAY)
cur_row, cur_col = nr, nc
def _tile_top_center(city: CityData, row: int, col: int, tile_map: dict[tuple[int, int], TileInfo]) -> tuple[int, int]:
tile = tile_map.get((row, col))
h = tile.height if tile else 1
cx = city.x_off + (col - row) * _DX
cy = city.y_off + (col + row) * _DY
return cx, cy + _DY - h
def _key_to_direction(key: str) -> tuple[int, int]:
match key:
case "w" | "W" | "\x1b[A":
return -1, 0
case "s" | "S" | "\x1b[B":
return 1, 0
case "a" | "A" | "\x1b[D":
return 0, -1
case "d" | "D" | "\x1b[C":
return 0, 1
case _:
return 0, 0
def _draw_cursor(buf: list[list[Color | None]], cx: int, cy: int) -> None:
bh = len(buf)
bw = len(buf[0]) if buf else 0
for dx, dy, color in _CURSOR_PIXELS:
px, py = cx + dx, cy + dy
if 0 <= py < bh and 0 <= px < bw:
buf[py][px] = color
def _highlight_tile(buf: list[list[Color | None]], city: CityData, tile: TileInfo) -> None:
cx = city.x_off + (tile.grid_col - tile.grid_row) * _DX
cy = city.y_off + (tile.grid_col + tile.grid_row) * _DY
h = tile.height
_fill_poly(
buf,
[(cx, cy - h), (cx + _DX, cy + _DY - h), (cx, cy + 2 * _DY - h), (cx - _DX, cy + _DY - h)],
_brighten(tile.top, 35),
)
def _brighten(color: Color, amount: int) -> Color:
return (min(255, color[0] + amount), min(255, color[1] + amount), min(255, color[2] + amount))
def _present(
city: CityData,
buf: list[list[Color | None]],
tile: TileInfo | None,
summary: list[str],
) -> None:
city_lines = _pixels_to_lines(buf)
while city_lines and not _strip_ansi(city_lines[0]).strip():
city_lines.pop(0)
while city_lines and not _strip_ansi(city_lines[-1]).strip():
city_lines.pop()
city_w = max((_visible_len(line) for line in city_lines), default=0)
term = shutil.get_terminal_size()
panel_max_w = max(20, term.columns - city_w - _SUMMARY_W - 2 * _GAP)
info = _build_info_panel(tile, city, panel_max_w)
n = max(len(summary), len(city_lines), len(info))
summary_lo = max(0, (n - len(summary)) // 2)
info_lo = max(0, (n - len(info)) // 2)
lines: list[str] = []
for i in range(n):
si = i - summary_lo
lt = summary[si] if 0 <= si < len(summary) else ""
lpad = max(0, _SUMMARY_W - _visible_len(lt))
ct = city_lines[i] if i < len(city_lines) else ""
cpad = max(0, city_w - _visible_len(ct))
ri = i - info_lo
rt = info[ri] if 0 <= ri < len(info) else ""
lines.append(lt + " " * lpad + " " * _GAP + ct + " " * cpad + " " * _GAP + rt)
lines.append("")
lines.append(" \033[90mWASD/Arrows: move · Q/ESC: quit\033[0m")
while len(lines) < term.lines - 1:
lines.append("")
output = "\033[H"
for line in lines[: term.lines - 1]:
output += line + "\033[K\r\n"
sys.stdout.write(output)
sys.stdout.flush()
def _build_info_panel(tile: TileInfo | None, city: CityData, max_w: int) -> list[str]:
reset = "\033[0m"
gray = "\033[90m"
bold = "\033[1m"
indent = " "
content_w = max_w - len(indent)
lines: list[str] = [""]
lines.append(f"{indent}{bold}City Explorer{reset}")
lines.append(indent + "─" * min(22, content_w))
lines.append("")
if tile is None:
lines.append(f"{indent}{gray}Move to a tile")
lines.append(f"{indent}to see details.{reset}")
return lines
if tile.repo is None:
lines.append(f"{indent}{gray}+{city.extra_count} more repos{reset}")
lines.append(f"{indent}{gray}{format_size(city.extra_storage, human_readable=True)} combined{reset}")
return lines
repo = tile.repo
name = repo.id
if len(name) > content_w:
name = name[: content_w - 3] + "..."
lines.append(f"{indent}{bold}{name}{reset}")
lines.append("")
type_ansi = {
"model": "\033[38;2;175;148;240m",
"dataset": "\033[38;2;245;128;128m",
"space": "\033[38;2;245;175;85m",
"bucket": "\033[38;2;112;185;242m",
}
tc = type_ansi.get(repo.type, "")
lines.append(f"{indent}Type {tc}{repo.type}{reset}")
lines.append(f"{indent}Visibility {repo.visibility}")
lines.append(f"{indent}Storage {format_size(repo.storage, human_readable=True)}")
lines.append(f"{indent}Usage {repo.storage_percent:.1f}%")
lines.append("")
bar_w = min(18, content_w)
filled = max(0, min(bar_w, round(repo.storage_percent / 100 * bar_w)))
lines.append(f"{indent}{tc}{'█' * filled}{gray}{'░' * (bar_w - filled)}{reset}")
return lines
def _copy_buf(buf: list[list[Color | None]]) -> list[list[Color | None]]:
return [row[:] for row in buf]
def _read_key() -> str:
fd = sys.stdin.fileno()
ch = os.read(fd, 1)
if ch == b"\x1b":
if _has_input(fd, 0.05):
ch2 = os.read(fd, 1)
if ch2 == b"[" and _has_input(fd, 0.05):
ch3 = os.read(fd, 1)
return f"\x1b[{ch3.decode()}"
return "esc"
return ch.decode("utf-8", errors="replace")
def _has_input(fd: int, timeout: float) -> bool:
r, _, _ = select.select([fd], [], [], timeout)
return bool(r)