| """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] |
|
|
| |
| _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 |
| _DY = 2 |
| _MAX_H = 16 |
| _MIN_H = 1 |
| _COLS = 6 |
| _EXT = 1 |
| _MAX_TILES = 30 |
|
|
| |
| _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 |
|
|
|
|
| |
| |
| |
|
|
|
|
| @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] |
|
|
|
|
| |
| |
| |
|
|
|
|
| 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, |
| ) |
|
|
|
|
| |
| |
| |
|
|
|
|
| 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 |
|
|
|
|
| |
| |
| |
|
|
| _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, "▄" |
|
|
| 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 |
|
|
|
|
| |
| |
| |
|
|
|
|
| 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 |
|
|
|
|
| |
| |
| |
|
|
|
|
| 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 |
|
|
|
|
| |
| |
| |
|
|
|
|
| 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() |
|
|
|
|
| |
| |
| |
|
|
|
|
| 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) |
|
|
| |
| 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) |
|
|