import sys
import tempfile
from pathlib import Path
sys.path.insert(0, str(Path(__file__).resolve().parent.parent / "e2e"))
from PIL import Image, ImageDraw, ImageFont
from conftest import FS_PROVIDER, make_fs_root, open_fs_repo
from tui import ROOT, Tui, build
IMG = ROOT / "doc" / "img"
FONT = "/usr/share/fonts/truetype/dejavu/DejaVuSansMono.ttf"
FONT_BOLD = "/usr/share/fonts/truetype/dejavu/DejaVuSansMono-Bold.ttf"
FONT_FALLBACK = "/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf"
FONT_FALLBACK_BOLD = "/usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf"
def covered_chars(path: str) -> set[int]:
from fontTools.ttLib import TTFont
with TTFont(path) as tt:
return {cp for table in tt["cmap"].tables for cp in table.cmap}
DEFAULT_FG = (0xCD, 0xD6, 0xF4)
DEFAULT_BG = (0x1E, 0x1E, 0x2E)
NAMED = {
"black": (0, 0, 0), "red": (0xF3, 0x8B, 0xA8), "green": (0xA6, 0xE3, 0xA1),
"yellow": (0xF9, 0xE2, 0xAF), "blue": (0x89, 0xB4, 0xFA),
"magenta": (0xCB, 0xA6, 0xF7), "cyan": (0x94, 0xE2, 0xD5),
"white": (0xCD, 0xD6, 0xF4),
"brightblack": (0x6C, 0x70, 0x86), "brightred": (0xF3, 0x8B, 0xA8),
"brightgreen": (0xA6, 0xE3, 0xA1), "brightyellow": (0xF9, 0xE2, 0xAF),
"brightblue": (0x89, 0xB4, 0xFA), "brightmagenta": (0xCB, 0xA6, 0xF7),
"brightcyan": (0x94, 0xE2, 0xD5), "brightwhite": (0xFF, 0xFF, 0xFF),
}
CUBE = [0, 95, 135, 175, 215, 255]
def color(value: str, default: tuple) -> tuple:
if not value or value == "default":
return default
if value in NAMED:
return NAMED[value]
if value.isdigit() and int(value) <= 255:
n = int(value)
if n < 8:
return NAMED[["black", "red", "green", "yellow", "blue", "magenta", "cyan", "white"][n]]
if n < 16:
return NAMED[["brightblack", "brightred", "brightgreen", "brightyellow",
"brightblue", "brightmagenta", "brightcyan", "brightwhite"][n - 8]]
if n < 232:
n -= 16
return (CUBE[n // 36], CUBE[(n // 6) % 6], CUBE[n % 6])
gray = 8 + (n - 232) * 10
return (gray, gray, gray)
if len(value) == 6:
return tuple(int(value[i : i + 2], 16) for i in (0, 2, 4))
return default
class Renderer:
def __init__(self, cols: int, rows: int, cell_px: int = 18) -> None:
self.font = ImageFont.truetype(FONT, cell_px)
self.font_bold = ImageFont.truetype(FONT_BOLD, cell_px)
self.fallback = ImageFont.truetype(FONT_FALLBACK, cell_px)
self.fallback_bold = ImageFont.truetype(FONT_FALLBACK_BOLD, cell_px)
self.mono_cover = covered_chars(FONT) | covered_chars(FONT_BOLD)
self.fallback_cover = covered_chars(FONT_FALLBACK) | covered_chars(
FONT_FALLBACK_BOLD
)
ascent, descent = self.font.getmetrics()
self.ch = ascent + descent - 1 self.cw = int(
max(self.font.getlength("M"), self.font_bold.getlength("M"))
)
self.cols, self.rows = cols, rows
self.img = Image.new("RGB", (cols * self.cw, rows * self.ch), DEFAULT_BG)
self.draw = ImageDraw.Draw(self.img)
def render(self, tui: Tui, path: Path) -> None:
self.img = Image.new("RGB", (self.cols * self.cw, self.rows * self.ch), DEFAULT_BG)
self.draw = ImageDraw.Draw(self.img)
screen = tui._screen
for y in range(self.rows):
for x in range(self.cols):
c = screen.buffer[y][x]
fg = color(c.fg, DEFAULT_FG)
bg = color(c.bg, DEFAULT_BG)
if c.reverse:
fg, bg = bg, fg
px, py = x * self.cw, y * self.ch
if bg != DEFAULT_BG:
self.draw.rectangle([px, py, px + self.cw, py + self.ch], fill=bg)
if c.data != " ":
font = self.font_bold if c.bold else self.font
if ord(c.data) not in self.mono_cover and ord(c.data) in self.fallback_cover:
font = self.fallback_bold if c.bold else self.fallback
self.draw.text((px, py), c.data, font=font, fill=fg)
self.img.save(path)
def main() -> None:
IMG.mkdir(exist_ok=True)
tmp = Path(tempfile.mkdtemp(prefix="rootle-shots-"))
root = make_fs_root(tmp)
(root / "alpha" / "src" / "render.rs").write_text(
"//! Rendering pipeline.\n"
"pub fn render(view: &View) -> Frame {\n"
" let frame = Frame::new();\n"
" view.draw(&frame);\n"
" frame\n"
"}\n"
)
config = tmp / "provider.toml"
config.write_text(
f'[provider]\nkind = "stdio"\n'
f'command = ["python3", "{FS_PROVIDER}", "{root}"]\n'
)
tui = Tui(build(), cols=100, rows=28, args=["--config", str(config)])
themes = Path(tui._home.name) / "config" / "rootle" / "themes"
themes.mkdir(parents=True, exist_ok=True)
(themes / "gruvbox-dark.toml").write_text(
'[semantic]\n'
'text = "#ebdbb2"\nbase = "#282828"\nmantle = "#1d2021"\n'
'crust = "#151313"\nsurface0 = "#3c3836"\n'
'border_focused = "#b8bb26"\nselection_fg = "#b8bb26"\n'
)
tui.start()
r = Renderer(100, 28)
def shot(name: str) -> None:
r.render(tui, IMG / name)
print(f" {name}")
try:
tui.expect("search github")
shot("01-launch-search.png")
tui.type_query("alpha")
tui.key("ENTER")
tui.expect("local/alpha")
shot("02-repo-results.png")
tui.key("ENTER")
tui.expect("render.rs")
tui.send("l") tui.expect("fn render")
shot("03-browse.png")
tui.send(" ")
tui.send("g")
tui.type_query("render")
tui.key("ENTER")
tui.expect("matches")
shot("04-grep.png")
tui.send("/")
tui.type_query("main")
tui.expect("SEARCH")
shot("05-results-filter.png")
tui.key("ESC")
tui.key("TAB")
tui.key("TAB")
tui.key("ENTER")
tui.expect("current repo")
shot("06-scope-popup.png")
tui.key("ESC") tui.key("ESC")
tui.send(" ")
tui.send("y")
tui.expect("yanked")
shot("07-yank.png")
tui.send("?")
tui.expect("keybindings")
shot("08-keybinds.png")
tui.key("ESC")
tui.send(":")
tui.type_query("settings")
tui.key("ENTER")
tui.expect("read_only")
shot("09-settings.png")
tui.send("l")
tui.expect("catppuccin-mocha")
shot("10-settings-theme.png")
tui.key("ESC")
tui.send("h")
tui.send("h")
tui.send("h")
tui.send("l") tui.expect("beta/")
tui.send("v")
tui.send(" ")
tui.send("j")
tui.send(" ")
tui.expect("●")
shot("11-visual.png")
tui.send("v")
tui.send(":")
tui.type_query("clone")
tui.key("ENTER")
tui.expect("1/3 repos")
shot("12-clone-repos.png")
tui.key("TAB")
tui.key("ENTER")
tui.expect("2/3 destination")
shot("13-clone-destination.png")
tui.key("TAB")
tui.key("ENTER")
tui.expect("3/3 summary")
shot("14-clone-summary.png")
tui.key("ESC")
tui.send("q")
finally:
tui.stop()
print(f"done → {IMG}")
if __name__ == "__main__":
main()