gitrub 1.1.13

A local git server — push, pull, clone over HTTP and SSH with LFS, hooks, and more
Documentation
#!/usr/bin/env python3
"""
Record gitrub demo using a real PTY. Produces asciicast v2.
"""

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

# ── Setup PTY ──
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)

# ── Cast file ──
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):
    """Inject green annotation bar at top using save/restore cursor."""
    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")

# ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
# DEMO SCRIPT
# ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

try:
    # Wait for TUI startup
    drain(3.0)
    annotate("gitrub TUI — server auto-starts, shows listening URLs")
    drain(3.0)

    # ── Navigate repos ──
    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)

    # ── Search ──
    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)

    # ── Repo detail ──
    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)

    # Files
    annotate("Tab → Files — repository file tree with sizes")
    drain(0.5)
    press("Tab"); drain(1.0)
    drain(2.0)

    # Branches
    annotate("Tab → Branches — all branches with active marker")
    drain(0.5)
    press("Tab"); drain(1.0)
    drain(2.0)

    # Contributors
    annotate("Tab → Contributors — commit counts per author")
    drain(0.5)
    press("Tab"); drain(1.0)
    drain(2.0)

    # Languages
    annotate("Tab → Languages — file types breakdown")
    drain(0.5)
    press("Tab"); drain(1.0)
    drain(2.0)

    # Close detail
    press("Esc"); drain(0.5)

    # ── Command palette ──
    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)

    # ── Settings ──
    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)

    # ── Sorting ──
    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)

    # ── Help ──
    annotate("? shows keybinding reference")
    drain(0.8)
    press("?"); drain(1.0)
    drain(3.0)
    press("Esc"); drain(0.5)

    # ── Server toggle ──
    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)

    # ── Outro ──
    annotate("cargo install gitrub — github.com/eugenehp/gitrub")
    drain(4.0)

    # Quit
    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")