Skip to content

feat(cli): add gt art command with rotating GT logo animation#1079

Open
moss-bryophyta wants to merge 9 commits intogeneraltranslation:mainfrom
moss-bryophyta:moss/cli-art-command
Open

feat(cli): add gt art command with rotating GT logo animation#1079
moss-bryophyta wants to merge 9 commits intogeneraltranslation:mainfrom
moss-bryophyta:moss/cli-art-command

Conversation

@moss-bryophyta
Copy link
Contributor

@moss-bryophyta moss-bryophyta commented Mar 6, 2026

What

Adds a gt art command that displays a rotating ASCII art animation of the GT logo in the terminal.

Details

  • 7 frames of ASCII art simulating a 3D rotation of the GT monogram
  • Colors cycle through cyan → blue → magenta → red → yellow → green each full rotation
  • Press q, Escape, or Ctrl+C to exit
  • Registered in BaseCLI so it's available regardless of framework detection

Demo

$ gt art

  Press q or Escape to exit

    ╔══════════════════════╗
    ║   ████████████████   ║
    ║   ████████████████   ║
    ║   ████      ████     ║
    ║   ████      ████     ║
    ║   ████               ║
    ║   ████               ║
    ║   ████   ████████    ║
    ║   ████   ████████    ║
    ║   ████      ████     ║
    ║   ████      ████     ║
    ║   ████████████████   ║
    ║   ████████████████   ║
    ╚══════════════════════╝

Requested by @ErnestM1234

Greptile Summary

This PR adds a gt art easter-egg command that plays a looping ASCII animation of a rotating GT monogram in the terminal, with cycling colors. The integration into BaseCLI is clean and minimal. However, art.ts has two logic issues that can leave the user's terminal in a broken state (hidden cursor, stuck raw mode) and can cause an infinite hang in non-interactive environments.

Key changes:

  • packages/cli/src/cli/commands/art.ts — New file: 7-frame ASCII art, rotation sequence builder, color cycling, stdin raw-mode handling, and a handleArt() async function that drives the animation loop.
  • packages/cli/src/cli/base.ts — Adds import { handleArt } and calls this.setupArtCommand() in the constructor, exposing gt art in the CLI.

Issues found:

  • Non-TTY infinite loop (art.ts line ~144): When the TTY check fails and setRawMode is skipped, the while (running) loop still runs indefinitely because no keyboard data can ever arrive to set running = false. A TTY guard with an early return is needed.
  • No cleanup on unexpected exit (art.ts line ~167): External SIGTERM/SIGINT and uncaught exceptions inside the animation loop skip cleanup(), leaving the terminal with a permanently hidden cursor and stdin in raw mode. Signal handlers (process.once('SIGTERM'), process.once('SIGINT')) and a try/finally block are needed to guarantee cleanup.

Confidence Score: 3/5

  • Safe to merge after addressing the non-TTY infinite loop and the missing cleanup on external signals.
  • The registration in base.ts is trivial and correct. The art.ts logic is mostly sound (raw mode, frame rendering, color cycling), but two issues can leave the terminal in an unusable state or cause the process to hang indefinitely, which warrants fixing before merge.
  • packages/cli/src/cli/commands/art.ts — both logic issues are here

Important Files Changed

Filename Overview
packages/cli/src/cli/commands/art.ts New command that renders a rotating GT logo animation. Has two logic bugs: infinite loop in non-TTY environments (no TTY guard before setRawMode skip), and no cleanup on external signals/exceptions (terminal left with hidden cursor and raw mode on crashes or SIGTERM).
packages/cli/src/cli/base.ts Registers the new art command via setupArtCommand() called in the constructor. Minimal, clean integration — no issues found.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A([gt art]) --> B{stdin.isTTY?}
    B -- Yes --> C[setRawMode true\nresume + setEncoding]
    B -- No --> C2[resume + setEncoding\n⚠️ no exit path]
    C --> D[Hide cursor\nPrint instructions]
    C2 --> D
    D --> E{running?}
    E -- Yes --> F[Render frame\nwait 100ms]
    F --> G{key pressed?}
    G -- q/Q/Esc/Ctrl+C --> H[running = false]
    G -- other --> E
    H --> E
    E -- No --> I[cleanup\nShow cursor\nReset raw mode\nPause stdin]
    I --> J([Exit])

    K([SIGTERM / external SIGINT]) -.->|⚠️ cleanup never called| L([Broken terminal state])
    M([Exception in loop]) -.->|⚠️ no try/finally| L
Loading

Last reviewed commit: d984142

Greptile also left 2 inline comments on this PR.

@moss-bryophyta moss-bryophyta requested a review from a team as a code owner March 6, 2026 01:47
Comment on lines +144 to +149
// Enable raw mode to capture keystrokes
if (process.stdin.isTTY) {
process.stdin.setRawMode(true);
}
process.stdin.resume();
process.stdin.setEncoding('utf8');
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Infinite loop in non-TTY environments

When the command is run in a non-TTY context (piped output, CI, etc.), setRawMode is skipped so no keyboard input can ever arrive, yet process.stdin.resume() is still called and the while (running) loop runs forever with no way to break out. The function will never return.

Consider adding an early exit guard for non-TTY environments:

Suggested change
// Enable raw mode to capture keystrokes
if (process.stdin.isTTY) {
process.stdin.setRawMode(true);
}
process.stdin.resume();
process.stdin.setEncoding('utf8');
// Enable raw mode to capture keystrokes
if (!process.stdin.isTTY) {
console.log(chalk.yellow(' gt art requires an interactive terminal (TTY).'));
return;
}
process.stdin.setRawMode(true);
process.stdin.resume();
process.stdin.setEncoding('utf8');
Prompt To Fix With AI
This is a comment left during a code review.
Path: packages/cli/src/cli/commands/art.ts
Line: 144-149

Comment:
**Infinite loop in non-TTY environments**

When the command is run in a non-TTY context (piped output, CI, etc.), `setRawMode` is skipped so no keyboard input can ever arrive, yet `process.stdin.resume()` is still called and the `while (running)` loop runs forever with no way to break out. The function will never return.

Consider adding an early exit guard for non-TTY environments:

```suggestion
  // Enable raw mode to capture keystrokes
  if (!process.stdin.isTTY) {
    console.log(chalk.yellow('  gt art requires an interactive terminal (TTY).'));
    return;
  }
  process.stdin.setRawMode(true);
  process.stdin.resume();
  process.stdin.setEncoding('utf8');
```

How can I resolve this? If you propose a fix, please make it concise.

Comment on lines +167 to +210
const cleanup = () => {
// Show cursor
process.stdout.write('\x1B[?25h');
if (process.stdin.isTTY) {
process.stdin.setRawMode(false);
}
process.stdin.removeListener('data', onKey);
process.stdin.pause();
console.log();
};

const FRAME_HEIGHT = 16; // lines per frame (including blank lines)

while (running) {
const color = COLORS[colorIndex % COLORS.length];
const frame = frames[frameIndex % frames.length];

// Move cursor up to overwrite previous frame (except first frame)
if (frameIndex > 0) {
process.stdout.write(`\x1B[${FRAME_HEIGHT}A`);
}

// Draw frame
const lines = frame.split('\n');
for (const line of lines) {
process.stdout.write(`\x1B[2K${color(line)}\n`);
}
// Pad remaining lines
for (let i = lines.length; i < FRAME_HEIGHT; i++) {
process.stdout.write(`\x1B[2K\n`);
}

frameIndex++;
if (frameIndex % frames.length === 0) {
colorIndex++;
}

// ~100ms per frame = ~10 FPS
await new Promise((resolve) => setTimeout(resolve, 100));
}

cleanup();
console.log(chalk.green(' ✨ General Translation'));
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Terminal left in broken state on unexpected exit

If the process is terminated externally (SIGTERM, or SIGINT sent from another process via kill -2) while the animation is running, cleanup() is never called. This leaves the terminal cursor permanently hidden (\x1B[?25l was written but \x1B[?25h is never sent) and stdin stuck in raw mode — a very disruptive experience for users.

Additionally, if an exception is thrown inside the while loop (e.g. broken pipe), the same problem occurs since there is no try/finally.

Consider registering signal handlers and using try/finally:

const sigHandler = () => { cleanup(); process.exit(); };
process.once('SIGTERM', sigHandler);
process.once('SIGINT', sigHandler);

try {
  while (running) {
    // ... animation loop ...
  }
} finally {
  cleanup();
  // Remove signal handlers so they don't fire again
  process.off('SIGTERM', sigHandler);
  process.off('SIGINT', sigHandler);
}
Prompt To Fix With AI
This is a comment left during a code review.
Path: packages/cli/src/cli/commands/art.ts
Line: 167-210

Comment:
**Terminal left in broken state on unexpected exit**

If the process is terminated externally (SIGTERM, or SIGINT sent from another process via `kill -2`) while the animation is running, `cleanup()` is never called. This leaves the terminal cursor permanently hidden (`\x1B[?25l` was written but `\x1B[?25h` is never sent) and stdin stuck in raw mode — a very disruptive experience for users.

Additionally, if an exception is thrown inside the `while` loop (e.g. broken pipe), the same problem occurs since there is no `try/finally`.

Consider registering signal handlers and using `try/finally`:

```typescript
const sigHandler = () => { cleanup(); process.exit(); };
process.once('SIGTERM', sigHandler);
process.once('SIGINT', sigHandler);

try {
  while (running) {
    // ... animation loop ...
  }
} finally {
  cleanup();
  // Remove signal handlers so they don't fire again
  process.off('SIGTERM', sigHandler);
  process.off('SIGINT', sigHandler);
}
```

How can I resolve this? If you propose a fix, please make it concise.

Displays a rotating ASCII art animation of the GT logo in the terminal.
Colors cycle through cyan, blue, magenta, red, yellow, and green.
Press q or Escape to exit.
- Replace hand-drawn ASCII frames with figlet slant font output
- Generate rotation frames programmatically via cos(angle) horizontal scaling
- Add non-TTY early exit guard (prevents infinite loop in CI/pipes)
- Add SIGTERM/SIGINT handlers + try/finally for terminal cleanup
- Addresses Greptile review feedback
@moss-bryophyta
Copy link
Contributor Author

Addressed both Greptile review items in 5b7ab97:

  1. Non-TTY guard — Early exit with message if !process.stdin.isTTY, prevents infinite loop in CI/pipes
  2. Signal cleanupSIGTERM/SIGINT handlers + try/finally ensure terminal cursor and raw mode are always restored

Also rewrote the whole command: replaced hand-drawn ASCII frames with figlet slant font output, and generate 48 smooth rotation frames programmatically via cos(angle) horizontal scaling. Much cleaner — 134 lines down from 273.

Replace figlet text with a block-character GT monogram that matches
the General Translation brand mark. Three concentric strokes on the G,
shared top bar, and parallel T stems — just like the real logo.
@moss-bryophyta
Copy link
Contributor Author

Updated the logo to use the GT monogram (matching the brand mark) instead of the figlet text. Three concentric strokes on the G, shared top bar, and parallel T stems — just like the real logo.

The rotation/color-cycling animation works the same way, just with the new art. ASCII-safe characters (#) used for maximum terminal compatibility.

Generated from assets/no-bg-gt-logo-light.png using:
  jp2a gt-logo.jpg --width=50 --chars='@#%+=- '

Much more accurate representation of the GT monogram
than the hand-drawn version.
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.
- console/logo.ts: logo data + frame generation utilities
- console/animation.ts: interactive and intro animation playback
- cli/commands/art.ts: thin command handler

Follows existing convention of putting display/UI code in console/.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant