import pty, os, select, time, struct, fcntl, termios, subprocess, json, sys, signal
GITRUB = os.path.expanduser("~/Desktop/gitrub/target/release/gitrub")
REPOS = "/tmp/gitrub-demo-repos"
CAST_FILE = os.path.expanduser("~/Desktop/gitrub/demo.cast")
WIDTH, HEIGHT = 120, 36
master_fd, slave_fd = pty.openpty()
winsize = struct.pack("HHHH", HEIGHT, WIDTH, 0, 0)
fcntl.ioctl(slave_fd, termios.TIOCSWINSZ, winsize)
env = os.environ.copy()
env["TERM"] = "xterm-256color"
env["COLORTERM"] = "truecolor"
env["LANG"] = "en_US.UTF-8"
proc = subprocess.Popen(
[GITRUB, "--noauth", "--root", REPOS, "--recursive"],
stdin=slave_fd, stdout=slave_fd, stderr=slave_fd,
env=env, preexec_fn=os.setsid,
)
os.close(slave_fd)
f = open(CAST_FILE, "w")
header = {
"version": 2, "width": WIDTH, "height": HEIGHT,
"timestamp": int(time.time()),
"title": "gitrub — Local GitHub Replacement",
"env": {"TERM": "xterm-256color", "SHELL": "/bin/bash"},
"theme": {
"fg": "#d4d4d4", "bg": "#1e1e1e",
"palette": "#1e1e1e:#f44747:#6a9955:#d7ba7d:#569cd6:#c586c0:#4ec9b0:#d4d4d4:#808080:#f44747:#6a9955:#d7ba7d:#569cd6:#c586c0:#4ec9b0:#ffffff"
}
}
f.write(json.dumps(header) + "\n")
t0 = time.time()
total_out = 0
def ts():
return round(time.time() - t0, 6)
def drain(secs):
global total_out
end = time.time() + secs
while time.time() < end:
r, _, _ = select.select([master_fd], [], [], min(0.1, max(0.01, end - time.time())))
if r:
try:
data = os.read(master_fd, 65536)
if data:
text = data.decode("utf-8", errors="replace")
f.write(json.dumps([ts(), "o", text]) + "\n")
total_out += len(data)
except OSError:
break
def send_key(b):
os.write(master_fd, b if isinstance(b, bytes) else b.encode())
KEY = {
"Enter": b"\r", "Esc": b"\x1b", "Tab": b"\t",
"Up": b"\x1b[A", "Down": b"\x1b[B",
"Left": b"\x1b[D", "Right": b"\x1b[C",
"Home": b"\x1b[H", "End": b"\x1b[F",
"PgUp": b"\x1b[5~", "PgDn": b"\x1b[6~",
}
def press(name):
send_key(KEY.get(name, name.encode()))
def type_chars(text, delay=0.08):
for ch in text:
send_key(ch.encode())
drain(delay)
def annotate(text):
bar = f" ▶ {text}"
pad = max(0, WIDTH - len(bar))
esc = f"\x1b7\x1b[1;1H\x1b[48;2;0;80;0m\x1b[38;2;255;255;255m\x1b[1m{bar}{' ' * pad}\x1b[0m\x1b8"
f.write(json.dumps([ts(), "o", esc]) + "\n")
try:
drain(3.0)
annotate("gitrub TUI — server auto-starts, shows listening URLs")
drain(3.0)
annotate("Navigate repos with ↑/↓ — 6 repos detected recursively")
drain(1.5)
for _ in range(5):
press("Down"); drain(0.35)
for _ in range(3):
press("Up"); drain(0.35)
drain(0.5)
annotate("/ to search — filters repos in real time")
drain(1.0)
press("/"); drain(0.3)
type_chars("acme", delay=0.1)
annotate("Filtered to 'acme' — only matching repos shown")
drain(2.0)
press("Enter"); drain(0.3)
press("/"); drain(0.1)
press("Esc"); drain(0.5)
press("Home"); drain(0.3)
annotate("Enter opens repo detail view")
drain(1.0)
press("Enter"); drain(2.0)
annotate("Commits tab — git log with hash, author, message, time")
drain(2.5)
annotate("Tab → Files — repository file tree with sizes")
drain(0.5)
press("Tab"); drain(1.0)
drain(2.0)
annotate("Tab → Branches — all branches with active marker")
drain(0.5)
press("Tab"); drain(1.0)
drain(2.0)
annotate("Tab → Contributors — commit counts per author")
drain(0.5)
press("Tab"); drain(1.0)
drain(2.0)
annotate("Tab → Languages — file types breakdown")
drain(0.5)
press("Tab"); drain(1.0)
drain(2.0)
press("Esc"); drain(0.5)
annotate("c opens command palette — git commands ready to copy")
drain(1.0)
press("c"); drain(1.5)
annotate("Clone, Remote, Push, Pull, Branch, Tag, Archive, LFS commands")
drain(2.0)
for _ in range(4):
press("Down"); drain(0.3)
annotate("Enter copies to clipboard via OSC 52")
drain(1.0)
press("Enter"); drain(0.5)
drain(1.5)
annotate("PgDn jumps between command sections")
press("PgDn"); drain(0.5)
drain(1.0)
press("PgDn"); drain(0.5)
drain(1.0)
press("Esc"); drain(0.5)
annotate("Tab switches to Settings panel")
drain(0.8)
press("Tab"); drain(0.5)
annotate("Configure root, ports, auth, SSH, hooks — all from TUI")
drain(1.0)
for _ in range(6):
press("Down"); drain(0.25)
annotate("Enter on toggle fields switches Auth/SSH/Recursive")
drain(2.0)
press("Tab"); drain(0.5)
annotate("o cycles sort: name → date → size")
drain(1.0)
press("o"); drain(0.5); drain(1.0)
press("o"); drain(0.5); drain(1.0)
annotate("O (shift) toggles ascending ↔ descending")
press("O"); drain(0.5); drain(1.0)
press("o"); drain(0.4)
annotate("? shows keybinding reference")
drain(0.8)
press("?"); drain(1.0)
drain(3.0)
press("Esc"); drain(0.5)
annotate("s toggles server on/off")
drain(1.0)
press("s"); drain(1.0)
annotate("Server stopped — indicator turns red")
drain(1.5)
press("s"); drain(2.5)
annotate("Server restarted — repos auto-refresh every 3s")
drain(2.5)
annotate("cargo install gitrub — github.com/eugenehp/gitrub")
drain(4.0)
press("q"); drain(1.0)
finally:
proc.terminate()
try:
proc.wait(timeout=3)
except:
proc.kill()
try:
os.close(master_fd)
except:
pass
f.close()
duration = round(time.time() - t0, 1)
size_kb = os.path.getsize(CAST_FILE) / 1024
lines = sum(1 for _ in open(CAST_FILE))
print(f"\n✅ Recording saved: {CAST_FILE}")
print(f" Duration: {duration}s")
print(f" Frames: {lines - 1}")
print(f" Size: {WIDTH}x{HEIGHT}")
print(f" File: {size_kb:.1f} KB")
print(f" Output: {total_out} bytes captured")