Skip to content

Commit 1b78960

Browse files
feat(cli): show animated GT logo intro during interactive init
When running `gt init` interactively (no --src or --config flags), plays a brief 2-rotation animation of the GT logo before the setup wizard begins. Falls back to the static logo in non-TTY environments or when config flags are provided. Also exports playIntroAnimation() from art.ts for reuse.
1 parent c0ad77c commit 1b78960

File tree

2 files changed

+83
-20
lines changed

2 files changed

+83
-20
lines changed

packages/cli/src/cli/base.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,7 @@ import {
6969
import { determineLibrary } from '../fs/determineFramework.js';
7070
import { INLINE_LIBRARIES } from '../types/libraries.js';
7171
import { handleEnqueue } from './commands/enqueue.js';
72-
import { handleArt } from './commands/art.js';
72+
import { handleArt, playIntroAnimation } from './commands/art.js';
7373

7474
export type UploadOptions = {
7575
config?: string;
@@ -371,6 +371,13 @@ export class BaseCLI {
371371
)
372372
.action(async (options: SetupOptions) => {
373373
const settings = await generateSettings(options);
374+
375+
// Show animated intro when running interactively without pre-set configs
376+
const hasConfigFlags = options.src || options.config;
377+
if (!hasConfigFlags && process.stdout.isTTY) {
378+
await playIntroAnimation(2);
379+
}
380+
374381
displayHeader('Running setup wizard...');
375382

376383
const framework = await detectFramework();

packages/cli/src/cli/commands/art.ts

Lines changed: 75 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@ const LOGO_LINES = [
1818
' --===-- -=- -= ',
1919
];
2020

21+
const FRAME_COUNT = 48;
22+
2123
/**
2224
* Horizontally scale a line of text by a factor (0..1).
2325
* At scale=1, the full line is shown. At scale=0, it collapses to center.
@@ -45,18 +47,37 @@ function scaleLineHorizontally(line: string, scale: number): string {
4547
* by cos(angle) to simulate spinning around the Y axis.
4648
*/
4749
function generateFrames(): string[][] {
48-
const frameCount = 48;
4950
const frames: string[][] = [];
50-
for (let i = 0; i < frameCount; i++) {
51-
const angle = (i / frameCount) * 2 * Math.PI;
51+
for (let i = 0; i < FRAME_COUNT; i++) {
52+
const angle = (i / FRAME_COUNT) * 2 * Math.PI;
5253
const scale = Math.abs(Math.cos(angle));
5354
frames.push(LOGO_LINES.map((line) => scaleLineHorizontally(line, scale)));
5455
}
5556
return frames;
5657
}
5758

59+
/**
60+
* Render a single frame of the animation.
61+
*/
62+
function renderFrame(
63+
frame: string[],
64+
lineCount: number,
65+
isFirst: boolean,
66+
color: (s: string) => string
67+
): void {
68+
if (!isFirst) {
69+
process.stdout.write(`\x1B[${lineCount}A`);
70+
}
71+
for (const line of frame) {
72+
process.stdout.write(`\x1B[2K${color(line)}\n`);
73+
}
74+
}
75+
76+
/**
77+
* Interactive art command — loops until user presses q/Escape.
78+
* Used by `gt art`.
79+
*/
5880
export async function handleArt(): Promise<void> {
59-
// Require interactive terminal to avoid infinite loop in non-TTY contexts
6081
if (!process.stdin.isTTY) {
6182
console.log(chalk.yellow(' gt art requires an interactive terminal (TTY).'));
6283
return;
@@ -77,10 +98,7 @@ export async function handleArt(): Promise<void> {
7798
};
7899

79100
process.stdin.on('data', onKey);
80-
81-
// Hide cursor
82101
process.stdout.write('\x1B[?25l');
83-
84102
console.log(chalk.dim('\n Press q or Escape to exit\n'));
85103

86104
const lineCount = LOGO_LINES.length;
@@ -93,7 +111,6 @@ export async function handleArt(): Promise<void> {
93111
console.log();
94112
};
95113

96-
// Ensure cleanup runs on unexpected termination
97114
const sigHandler = () => {
98115
cleanup();
99116
process.exit();
@@ -103,18 +120,13 @@ export async function handleArt(): Promise<void> {
103120

104121
try {
105122
while (running) {
106-
const frame = frames[frameIndex % frames.length];
107-
108-
if (frameIndex > 0) {
109-
process.stdout.write(`\x1B[${lineCount}A`);
110-
}
111-
112-
for (const line of frame) {
113-
process.stdout.write(`\x1B[2K${chalk.white(line)}\n`);
114-
}
115-
123+
renderFrame(
124+
frames[frameIndex % frames.length],
125+
lineCount,
126+
frameIndex === 0,
127+
chalk.white
128+
);
116129
frameIndex++;
117-
118130
await new Promise((resolve) => setTimeout(resolve, 80));
119131
}
120132
} finally {
@@ -125,3 +137,47 @@ export async function handleArt(): Promise<void> {
125137

126138
console.log(chalk.white(' ✨ General Translation'));
127139
}
140+
141+
/**
142+
* Brief intro animation — plays a fixed number of rotations then stops.
143+
* Used by `gt init` to greet users in interactive mode.
144+
* Falls back to a static logo if not running in a TTY.
145+
*/
146+
export async function playIntroAnimation(
147+
rotations: number = 2
148+
): Promise<void> {
149+
if (!process.stdout.isTTY) {
150+
// Non-TTY: just print the static logo
151+
console.log();
152+
for (const line of LOGO_LINES) {
153+
console.log(chalk.white(line));
154+
}
155+
console.log();
156+
return;
157+
}
158+
159+
const frames = generateFrames();
160+
const totalFrames = FRAME_COUNT * rotations;
161+
const lineCount = LOGO_LINES.length;
162+
163+
process.stdout.write('\x1B[?25l');
164+
console.log();
165+
166+
for (let i = 0; i < totalFrames; i++) {
167+
renderFrame(
168+
frames[i % frames.length],
169+
lineCount,
170+
i === 0,
171+
chalk.white
172+
);
173+
await new Promise((resolve) => setTimeout(resolve, 80));
174+
}
175+
176+
// Clear the animation and show cursor
177+
process.stdout.write(`\x1B[${lineCount}A`);
178+
for (let i = 0; i < lineCount; i++) {
179+
process.stdout.write('\x1B[2K\n');
180+
}
181+
process.stdout.write(`\x1B[${lineCount}A`);
182+
process.stdout.write('\x1B[?25h');
183+
}

0 commit comments

Comments
 (0)