SIGN IN SIGN UP
HKUDS / DeepTutor UNCLAIMED

"DeepTutor: AI-Powered Personalized Learning Assistant"

0 0 1 Python
"""Pure-ANSI terminal UI toolkit — zero external dependencies.
Provides banner, inline arrow-key selector, confirm, text input, step
headers, coloured log helpers, spinner, and countdown. Designed for a
minimal / industrial aesthetic inspired by Claude Code.
"""
from __future__ import annotations
import getpass
import os
import shutil
import sys
import textwrap
import time
from contextlib import contextmanager
from typing import Generator
# ---------------------------------------------------------------------------
# Colour helpers
# ---------------------------------------------------------------------------
def _is_color() -> bool:
return sys.stdout.isatty() and os.environ.get("TERM", "dumb") != "dumb"
def _wrap(text: str, code: str) -> str:
if not _is_color():
return text
return f"\033[{code}m{text}\033[0m"
def accent(text: str) -> str:
return _wrap(text, "38;5;111")
def success(text: str) -> str:
return _wrap(text, "38;5;78")
def warn(text: str) -> str:
return _wrap(text, "38;5;214")
def error(text: str) -> str:
return _wrap(text, "38;5;203")
def dim(text: str) -> str:
return _wrap(text, "2")
def bold(text: str) -> str:
return _wrap(text, "1")
# ---------------------------------------------------------------------------
# Terminal geometry
# ---------------------------------------------------------------------------
def term_width() -> int:
return min(max(shutil.get_terminal_size((80, 24)).columns, 60), 100)
# ---------------------------------------------------------------------------
# Low-level key reading (Unix / Windows)
# ---------------------------------------------------------------------------
def _read_key() -> str:
"""Read a single keypress. Returns 'up', 'down', 'enter', or char."""
try:
import tty
import termios
fd = sys.stdin.fileno()
old = termios.tcgetattr(fd)
try:
tty.setraw(fd)
ch = sys.stdin.read(1)
if ch == "\x1b":
seq = sys.stdin.read(2)
if seq == "[A":
return "up"
if seq == "[B":
return "down"
return "esc"
if ch in ("\r", "\n"):
return "enter"
if ch == "\x03":
raise KeyboardInterrupt
return ch
finally:
termios.tcsetattr(fd, termios.TCSADRAIN, old)
except ImportError:
# Windows fallback
import msvcrt # type: ignore[import-untyped]
ch = msvcrt.getwch()
if ch == "\xe0":
code = msvcrt.getwch()
if code == "H":
return "up"
if code == "P":
return "down"
return "esc"
if ch in ("\r", "\n"):
return "enter"
if ch == "\x03":
raise KeyboardInterrupt
return ch
# ---------------------------------------------------------------------------
# Banner
# ---------------------------------------------------------------------------
def banner(title: str, lines: list[str] | None = None) -> None:
w = term_width()
inner = w - 4 # usable chars between "│ " and " │"
print()
dashes = max(w - 4 - len(title), 0)
print(f"{accent(title)} " + "" * dashes + "")
if lines:
print(f"{' ' * (w - 2)}")
for line in lines:
for wrapped in textwrap.wrap(line, width=inner) or [""]:
print(f"{dim(wrapped.ljust(inner))}")
print(f"{' ' * (w - 2)}")
print("" + "" * (w - 2) + "")
print()
# ---------------------------------------------------------------------------
# Inline arrow-key selector
# ---------------------------------------------------------------------------
def select(prompt: str, options: list[tuple[str, str, str]]) -> str:
"""Arrow-key selector. *options* is a list of ``(value, label, desc)``.
Returns the *value* of the chosen option. Falls back to numbered
input when stdin is not a tty.
"""
if not sys.stdin.isatty():
return _select_fallback(prompt, options)
idx = 0
total = len(options)
print(dim(f" {prompt}"))
print()
def _render() -> None:
for i, (_, label, desc) in enumerate(options):
if i == idx:
marker = accent("> ")
text = f"{bold(label)} {dim(desc)}" if desc else bold(label)
else:
marker = " "
text = dim(f"{label} {desc}") if desc else dim(label)
print(f" {marker}{text}")
_render()
while True:
key = _read_key()
if key == "up":
idx = (idx - 1) % total
elif key == "down":
idx = (idx + 1) % total
elif key == "enter":
# Move past the option block
sys.stdout.write(f"\033[{total}A")
sys.stdout.write(f"\033[J")
chosen_label = options[idx][1]
chosen_desc = options[idx][2]
print(f" {accent('>')} {bold(chosen_label)} {dim(chosen_desc)}")
print()
return options[idx][0]
elif key == "esc":
continue
else:
continue
# Redraw: move cursor up by `total` lines, clear, re-render
sys.stdout.write(f"\033[{total}A")
sys.stdout.write(f"\033[J")
_render()
def _select_fallback(prompt: str, options: list[tuple[str, str, str]]) -> str:
print(f" {prompt}")
for i, (_, label, desc) in enumerate(options, 1):
print(f" {i}. {label} {desc}")
while True:
answer = input(" > ").strip()
if answer.isdigit() and 1 <= int(answer) <= len(options):
return options[int(answer) - 1][0]
for value, label, _ in options:
if answer in (value, label):
return value
print(warn(" Please choose a valid option."))
# ---------------------------------------------------------------------------
# Confirm (Y/n)
# ---------------------------------------------------------------------------
def confirm(prompt: str, default: bool = True) -> bool:
hint = "[Y/n]" if default else "[y/N]"
while True:
answer = input(f" {prompt} {dim(hint)} ").strip().lower()
if not answer:
return default
if answer in ("y", "yes"):
return True
if answer in ("n", "no"):
return False
print(warn(" Please answer y or n."))
# ---------------------------------------------------------------------------
# Text input
# ---------------------------------------------------------------------------
def text_input(prompt: str, default: str = "", secret: bool = False) -> str:
display = f" {prompt}"
if default and not secret:
display += f" {dim(f'[{default}]')}"
display += dim(": ")
if secret:
value = getpass.getpass(display)
else:
value = input(display).strip()
return value or default
# ---------------------------------------------------------------------------
# Step header
# ---------------------------------------------------------------------------
def step(current: int, total: int | str, title: str) -> None:
print()
w = term_width()
label = f" {current}/{total} "
left = (w - len(label)) // 2
right = w - left - len(label)
print(dim("" * left + label + "" * right))
print(f" {bold(title)}")
print()
# ---------------------------------------------------------------------------
# Log helpers
# ---------------------------------------------------------------------------
def log_info(msg: str) -> None:
print(f" {accent('·')} {msg}")
def log_success(msg: str) -> None:
print(f" {success('')} {msg}")
def log_warn(msg: str) -> None:
print(f" {warn('!')} {msg}")
def log_error(msg: str) -> None:
print(f" {error('')} {msg}")
# ---------------------------------------------------------------------------
# Spinner (context manager)
# ---------------------------------------------------------------------------
_SPINNER_FRAMES = ("", "", "", "", "", "", "", "", "", "")
@contextmanager
def spinner(label: str) -> Generator[None, None, None]:
"""Simple braille-dot spinner shown while the body executes."""
import threading
stop = threading.Event()
def _spin() -> None:
i = 0
while not stop.is_set():
frame = _SPINNER_FRAMES[i % len(_SPINNER_FRAMES)]
sys.stdout.write(f"\r {accent(frame)} {label}")
sys.stdout.flush()
i += 1
stop.wait(0.08)
sys.stdout.write("\r" + " " * (len(label) + 6) + "\r")
sys.stdout.flush()
t = threading.Thread(target=_spin, daemon=True)
t.start()
try:
yield
finally:
stop.set()
t.join()
# ---------------------------------------------------------------------------
# Countdown
# ---------------------------------------------------------------------------
def countdown(seconds: int, label: str = "Starting in") -> None:
for remaining in range(seconds, 0, -1):
sys.stdout.write(f"\r {dim(label)} {accent(str(remaining))}s ")
sys.stdout.flush()
time.sleep(1)
sys.stdout.write("\r" + " " * (len(label) + 12) + "\r")
sys.stdout.flush()