Skip to content

Commit 682679b

Browse files
committed
docs(readme): add terminal demo GIF
1 parent 09eb1b7 commit 682679b

3 files changed

Lines changed: 194 additions & 0 deletions

File tree

README.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,12 @@ A beautiful terminal UI for managing cron jobs, built with [Bubble Tea](https://
55
![Go](https://img.shields.io/badge/Go-1.25+-00ADD8?logo=go&logoColor=white)
66
![License](https://img.shields.io/badge/License-MIT-green)
77

8+
## Demo
9+
10+
![CronTUI native Windows CLI demo](media/demo/crontui-native-windows.gif)
11+
12+
Short native Windows CLI flow: add a managed task, confirm it in Task Scheduler, and create a backup.
13+
814
## Features
915

1016
- **Interactive TUI** — browse, add, edit, delete, and toggle cron jobs visually
92.9 KB
Loading

scripts/render_terminal_demo.py

Lines changed: 188 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,188 @@
1+
#!/usr/bin/env python3
2+
"""
3+
Generate a short README/release terminal demo GIF for CronTUI.
4+
5+
This renderer creates a deterministic terminal-style animation from curated
6+
frames, which keeps the README asset stable even when live terminal capture
7+
tooling is unavailable on the current machine.
8+
"""
9+
10+
from __future__ import annotations
11+
12+
from pathlib import Path
13+
14+
from PIL import Image, ImageDraw, ImageFont
15+
16+
17+
ROOT = Path(__file__).resolve().parent.parent
18+
OUTPUT = ROOT / "media" / "demo" / "crontui-native-windows.gif"
19+
FONT_PATHS = [
20+
Path(r"C:\WINDOWS\Fonts\consola.ttf"),
21+
Path(r"C:\WINDOWS\Fonts\consolab.ttf"),
22+
]
23+
24+
WIDTH = 1400
25+
HEIGHT = 760
26+
PADDING_X = 42
27+
PADDING_Y = 34
28+
LINE_HEIGHT = 32
29+
30+
BG = "#0d1117"
31+
PANEL = "#161b22"
32+
BORDER = "#30363d"
33+
TEXT = "#c9d1d9"
34+
MUTED = "#8b949e"
35+
PROMPT = "#7ee787"
36+
ACCENT = "#58a6ff"
37+
TITLE = "#f0f6fc"
38+
39+
40+
FRAMES = [
41+
{
42+
"duration": 1200,
43+
"command": None,
44+
"body": [
45+
("title", "CronTUI native Windows demo"),
46+
("muted", "Add a managed task, verify it in Task Scheduler, create a backup."),
47+
("blank", ""),
48+
("text", "Environment: native Windows Task Scheduler"),
49+
("text", "Scope: one short CLI workflow for the README and release page"),
50+
],
51+
},
52+
{
53+
"duration": 1800,
54+
"command": r'.\crontui.exe add "@hourly" whoami --desc "hourly identity"',
55+
"body": [
56+
("text", "Added job #1: @hourly whoami"),
57+
],
58+
},
59+
{
60+
"duration": 2200,
61+
"command": r".\crontui.exe list",
62+
"body": [
63+
("text", "ID Status Schedule Command Description"),
64+
("muted", "-- ------ -------- ------- -----------"),
65+
("text", "1 ON @hourly whoami hourly identity"),
66+
],
67+
},
68+
{
69+
"duration": 2200,
70+
"command": r'''powershell -NoProfile -Command "Get-ScheduledTask | Where-Object TaskName -eq 'job-1' | Select-Object TaskName,TaskPath,State"''',
71+
"body": [
72+
("text", "TaskName TaskPath State"),
73+
("muted", "-------- -------- -----"),
74+
("text", r"job-1 \CronTUI-Demo\ Ready"),
75+
],
76+
},
77+
{
78+
"duration": 2200,
79+
"command": r".\crontui.exe backup",
80+
"body": [
81+
("text", r"Backup created: .tmp\readme-demo\taskscheduler_20260319_191321.bak"),
82+
],
83+
},
84+
]
85+
86+
87+
def load_font(size: int) -> ImageFont.FreeTypeFont | ImageFont.ImageFont:
88+
for path in FONT_PATHS:
89+
if path.exists():
90+
return ImageFont.truetype(str(path), size=size)
91+
return ImageFont.load_default()
92+
93+
94+
FONT = load_font(22)
95+
FONT_BOLD = load_font(24)
96+
97+
98+
def terminal_frame(command: str | None, body: list[tuple[str, str]]) -> Image.Image:
99+
image = Image.new("RGB", (WIDTH, HEIGHT), BG)
100+
draw = ImageDraw.Draw(image)
101+
102+
panel_left = 28
103+
panel_top = 28
104+
panel_right = WIDTH - 28
105+
panel_bottom = HEIGHT - 28
106+
107+
draw.rounded_rectangle(
108+
(panel_left, panel_top, panel_right, panel_bottom),
109+
radius=18,
110+
fill=PANEL,
111+
outline=BORDER,
112+
width=2,
113+
)
114+
115+
header_bottom = panel_top + 56
116+
draw.rounded_rectangle(
117+
(panel_left, panel_top, panel_right, header_bottom),
118+
radius=18,
119+
fill="#0f141b",
120+
outline=BORDER,
121+
width=2,
122+
)
123+
draw.rectangle(
124+
(panel_left, header_bottom - 18, panel_right, header_bottom),
125+
fill="#0f141b",
126+
outline="#0f141b",
127+
)
128+
129+
dots = ["#ff5f57", "#febc2e", "#28c840"]
130+
for i, color in enumerate(dots):
131+
cx = panel_left + 24 + i * 22
132+
cy = panel_top + 28
133+
draw.ellipse((cx - 6, cy - 6, cx + 6, cy + 6), fill=color)
134+
135+
draw.text(
136+
(panel_left + 90, panel_top + 16),
137+
"CronTUI terminal demo",
138+
fill=MUTED,
139+
font=FONT,
140+
)
141+
142+
x = panel_left + PADDING_X
143+
y = header_bottom + PADDING_Y
144+
145+
if command is not None:
146+
prompt = r"PS C:\Users\merup\Downloads\crontui> "
147+
draw.text((x, y), prompt, fill=PROMPT, font=FONT)
148+
prompt_width = draw.textlength(prompt, font=FONT)
149+
draw.text((x + prompt_width, y), command, fill=TEXT, font=FONT)
150+
y += LINE_HEIGHT + 6
151+
152+
for kind, line in body:
153+
if kind == "blank":
154+
y += LINE_HEIGHT // 2
155+
continue
156+
fill = TEXT
157+
font = FONT
158+
if kind == "muted":
159+
fill = MUTED
160+
elif kind == "title":
161+
fill = TITLE
162+
font = FONT_BOLD
163+
elif kind == "accent":
164+
fill = ACCENT
165+
draw.text((x, y), line, fill=fill, font=font)
166+
y += LINE_HEIGHT
167+
168+
return image
169+
170+
171+
def main() -> None:
172+
OUTPUT.parent.mkdir(parents=True, exist_ok=True)
173+
frames = [terminal_frame(frame["command"], frame["body"]) for frame in FRAMES]
174+
durations = [frame["duration"] for frame in FRAMES]
175+
frames[0].save(
176+
OUTPUT,
177+
save_all=True,
178+
append_images=frames[1:],
179+
duration=durations,
180+
loop=0,
181+
optimize=False,
182+
disposal=2,
183+
)
184+
print(f"Generated {OUTPUT}")
185+
186+
187+
if __name__ == "__main__":
188+
main()

0 commit comments

Comments
 (0)