Skip to content

FEAT Animated ASCII banner with raccoon mascot for PyRIT CLI#1417

Merged
romanlutz merged 31 commits intoAzure:mainfrom
romanlutz:romanlutz/animated-ascii-banner
Mar 13, 2026
Merged

FEAT Animated ASCII banner with raccoon mascot for PyRIT CLI#1417
romanlutz merged 31 commits intoAzure:mainfrom
romanlutz:romanlutz/animated-ascii-banner

Conversation

@romanlutz
Copy link
Contributor

Summary

Adds an animated startup banner to pyrit_shell featuring a braille-art raccoon mascot, the PyRIT block-letter logo, and a striped curling tail —
inspired by the GitHub Copilot CLI's animated banner approach.

What it looks like

The banner plays a ~2.5 second animation on shell startup:

  1. Raccoon slides in from the right with sparkle stars (✦ ✧)
  2. PyRIT text reveals left-to-right in cyan block letters
  3. Sparkle celebration with stars at varied positions
  4. Commands section appears with a striped raccoon tail curling down from the divider

Architecture

  • pyrit/cli/banner.py (659 lines) — Frame-based animation engine with:
    - Per-segment coloring: Different ANSI colors within the same line (raccoon=magenta, PyRIT=cyan, tail=magenta, stars=yellow, border=cyan)
    - Semantic color roles mapped to 4-bit ANSI palette with dark/light theme support
    - Braille raccoon art — high-detail inverted Unicode braille face with bandit mask
    - Curling striped tail — 9-char wide braille tail with S-curve shape and alternating dense/sparse stripes
    - Graceful degradation: falls back to static banner for non-TTY, NO_COLOR, PYRIT_NO_ANIMATION, CI environments
    - Ctrl+C skips animation to static banner immediately
  • pyrit/cli/pyrit_shell.py — Integrates via cmdloop() override; waits for background init to complete before animation to prevent log messages
    corrupting cursor positioning. Adds --no-animation CLI flag.
  • tests/unit/cli/test_banner.py (24 tests) — Colors, themes, animation capability, frame generation, static banner content, segment rendering

How to test

Animated banner

python -m pyrit.cli.pyrit_shell

Static banner (skip animation)

python -m pyrit.cli.pyrit_shell --no-animation

Environment variable to disable

PYRIT_NO_ANIMATION=1 python -m pyrit.cli.pyrit_shell

Scope

  • Only affects pyrit_shell — pyrit_scan is unchanged
  • No new dependencies
  • All 64 CLI tests pass (24 banner + 40 shell)
roakey.banner.mp4

romanlutz and others added 16 commits February 26, 2026 13:49
- Create pyrit/cli/banner.py with frame-based animation engine
- Raccoon mascot walks in from right, PYRIT text reveals left-to-right
- Semantic color roles with light/dark terminal theme support
- Graceful degradation: static banner when not a TTY, NO_COLOR, CI, or --no-animation
- Ctrl+C during animation skips to static banner
- Add --no-animation flag to pyrit_shell CLI
- 24 unit tests covering color roles, themes, animation capability detection, frames, and fallback
- Update existing shell tests for new banner integration

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Move cursor up (frame_height - 1) lines instead of frame_height,
since the rendered frame has (N-1) newlines for N lines, leaving
the cursor on the last line rather than below it.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Redesign raccoon with bandit mask (=o.o=), w nose, wider head
  (/\___/\), and striped tail (~~) to look like a raccoon not a cat
- Reserve vertical space before animation to prevent scroll drift
  when cursor is near bottom of terminal

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Single raccoon with clear features: pointed ears, bandit mask
  (=o o=), w nose, bushy striped tail (~~~~~)
- Remove duplicate right-side raccoon from commands section
- Fix animation drift: wait for background init to complete before
  playing animation so log messages don't corrupt cursor positioning
- Build banner programmatically to guarantee correct line widths

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The raccoon is now rendered using Unicode braille characters with
inverted dots — the raccoon face is drawn as positive space on an
empty background, blending naturally with the banner's box-drawing
and block-letter aesthetic.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
User-edited raccoon trimmed to 12 lines. Cleared bottom-row braille
dots (bits 6-7) from chin characters to avoid visual artifacts below
the chin line.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Use RACCOON_BODY color for all header lines during animation Phase 2
instead of PYRIT_TEXT for lines with block letters. This ensures the
entire raccoon lights up uniformly. Also fix PYRIT_START_ROW (2) so
both subtitles fit within HEADER_ROWS (12), add lowercase y to block
letters, and remove stray braille dots from chin line.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Normalize all braille raccoon lines to exactly 30 chars (replace
  mixed regular spaces with braille empty U+2800) to fix alignment
- Use RACCOON_BODY color for all header rows including subtitle rows
  so the entire raccoon lights up consistently
- Add striped raccoon tail hanging from divider into commands section
  with alternating thick/thin stripes and bushy tip

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Remove SUBTITLE color overrides in Phase 2 and Phase 3 animation
  so all raccoon lines use consistent RACCOON_BODY color throughout
- Shift chin motif left by 2 positions to center under face
- Redesign tail as 6-char wide braille art with alternating dense/
  sparse stripes tapering from wide to narrow tip

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Tail now starts at w=5, bulges to w=6, then tapers through
5,4,3,2,1. Dark stripes use full braille fill, light stripes
use thin edge delimiters only (top dots at boundaries).

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Tail is now 9 chars wide (50% wider), curls rightward with
increasing offset per line, and uses vertical braille delimiters
(left=dots 1,2,3 right=dots 4,5,6) on light stripes instead of
horizontal ones.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Lines with tail content now always use COMMANDS color instead of
inheriting BORDER color from empty separator lines.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Tail offsets now follow 0,0,1,2,3,3,3,2,1,0 creating a proper
curl that curves right then sweeps back left at the tip.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Major animation upgrade inspired by GitHub Copilot CLI approach:

- Per-segment coloring: AnimationFrame now supports segment_colors
  for different colors within the same line (raccoon=magenta,
  PYRIT text=cyan, subtitles=white, tail=magenta, stars=yellow)
- Vibrant ANSI color theme: bright_cyan for PYRIT text,
  bright_magenta for raccoon/tail, bright_yellow for sparkles
- Sparkle stars (✦ ✧ · *) appear during raccoon entry and
  celebration phases at randomized positions
- 3-frame sparkle celebration instead of 2
- Phase 4 preserves segment colors from static banner

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Sparkle segments in Phase 3 overlapped with PYRIT_TEXT and SUBTITLE
segments, causing _render_line_with_segments to output characters
twice (e.g. 106 visible chars instead of 96). Rewrote the function
to use a per-character color map where later segments override
earlier ones, then group consecutive same-role characters for
rendering. This guarantees visible output width matches input.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Add per-segment coloring to quick start lines and non-tail command
lines so the border characters (║) use BORDER color (cyan) while
the text content uses COMMANDS color (white).

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot AI review requested due to automatic review settings February 27, 2026 23:46
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Adds an animated (with static fallback) startup banner to pyrit_shell, including a new banner module and accompanying unit tests, plus a --no-animation flag to disable animation.

Changes:

  • Introduces pyrit/cli/banner.py implementing frame-based terminal animation with theme-aware ANSI coloring and static fallback.
  • Updates pyrit_shell to play the banner before starting the REPL and adds --no-animation CLI flag.
  • Adds/updates unit tests for the banner behavior and adjusts shell tests for the new intro behavior.

Reviewed changes

Copilot reviewed 4 out of 4 changed files in this pull request and generated 7 comments.

File Description
pyrit/cli/banner.py New banner implementation (static + animation rendering, theme detection, animation gating).
pyrit/cli/pyrit_shell.py Integrates banner via cmdloop() override; adds --no-animation flag wiring.
tests/unit/cli/test_banner.py New unit tests for banner colors/themes/frames/static/animation gating.
tests/unit/cli/test_pyrit_shell.py Updates intro expectation now that intro is set via cmdloop()/banner.

romanlutz and others added 3 commits February 28, 2026 07:09
- Add Returns sections to all function docstrings (DOC201)
- Simplify can_animate() return logic (SIM103)
- Remove unused loop variable sparkle_idx (B007)
- Fix segs variable redefinition in _build_animation_frames (mypy)

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Apply ruff formatting fixes and auto-fixable lint issues.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot AI review requested due to automatic review settings March 1, 2026 13:02
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 4 out of 4 changed files in this pull request and generated 5 comments.

Copilot AI review requested due to automatic review settings March 3, 2026 00:42
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 4 out of 4 changed files in this pull request and generated 3 comments.

Copilot AI review requested due to automatic review settings March 3, 2026 05:01
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 4 out of 4 changed files in this pull request and generated 2 comments.

romanlutz and others added 3 commits March 2, 2026 21:21
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot AI review requested due to automatic review settings March 3, 2026 05:33
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 4 out of 4 changed files in this pull request and generated 3 comments.

Copy link
Contributor

@spencrr spencrr left a comment

Choose a reason for hiding this comment

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

LGTM! 🦝

Nits from my copilot review session:

Nit: No test coverage for Ctrl+C → static banner fallback

play_animation() has a KeyboardInterrupt handler (line ~700) that clears the screen, writes the static banner, and restores the cursor. This path isn't tested — a regression here could leave the terminal cursor hidden.

Suggested fix prompt:

Add a test to TestPlayAnimation in tests/unit/cli/test_banner.py that mocks can_animate to return True, mocks _detect_theme to return DARK_THEME, mocks sys.stdout as a TTY, and mocks time.sleep with a side_effect that counts calls and raises KeyboardInterrupt after 3 invocations (to let a few frames render before the interrupt). Assert that the cursor is restored (\033[?25h in the written output) and play_animation returns "". Follow the existing test patterns in the class.


Nit: No test verifying animation actually writes frames to stdout

TestPlayAnimation only tests the non-animated paths (no_animation=True and can_animate()=False). The happy path — where animation runs and writes colored frames to stdout — has no test coverage.

Suggested fix prompt:

Add a test to TestPlayAnimation in tests/unit/cli/test_banner.py that mocks can_animate to return True, mocks _detect_theme to return DARK_THEME, mocks time.sleep as a no-op, and mocks sys.stdout as a TTY. Assert that play_animation() returns "", that stdout.write was called, and that the written output contains ANSI escape codes (\033[), box-drawing characters (), and cursor hide/restore sequences (\033[?25l and \033[?25h). Follow the existing test patterns in the class.

romanlutz and others added 2 commits March 13, 2026 05:09
- Extract ASCII art assets (raccoon, PYRIT letters, tail) to banner_assets.py
- Remove unused RACCOON_MASK and RACCOON_EYES ColorRole members and theme entries
- Add StaticBannerData dataclass to replace opaque tuple return type
- Add descriptive comments for layout constants (BOX_W, RACCOON_COL, etc.)
- Fix PyRITShell.__init__ signature: add -> None and keyword-only args
- Guard sparkle color segments on actual character insertion
- Fix Ctrl+C handler to reposition cursor before clearing screen
- Strengthen test_all_frames_have_consistent_width: check end chars + width
- Add tests for --no-animation CLI flag in TestMain
- Add tests for cmdloop/play_animation integration wiring

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Test that animation writes ANSI frames with cursor hide/restore to stdout
- Test that Ctrl+C during animation restores cursor and shows static banner

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@romanlutz
Copy link
Contributor Author

LGTM! 🦝

Nits from my copilot review session:

Nit: No test coverage for Ctrl+C → static banner fallback

play_animation() has a KeyboardInterrupt handler (line ~700) that clears the screen, writes the static banner, and restores the cursor. This path isn't tested — a regression here could leave the terminal cursor hidden.

Suggested fix prompt:

Add a test to TestPlayAnimation in tests/unit/cli/test_banner.py that mocks can_animate to return True, mocks _detect_theme to return DARK_THEME, mocks sys.stdout as a TTY, and mocks time.sleep with a side_effect that counts calls and raises KeyboardInterrupt after 3 invocations (to let a few frames render before the interrupt). Assert that the cursor is restored (\033[?25h in the written output) and play_animation returns "". Follow the existing test patterns in the class.

Nit: No test verifying animation actually writes frames to stdout

TestPlayAnimation only tests the non-animated paths (no_animation=True and can_animate()=False). The happy path — where animation runs and writes colored frames to stdout — has no test coverage.

Suggested fix prompt:

Add a test to TestPlayAnimation in tests/unit/cli/test_banner.py that mocks can_animate to return True, mocks _detect_theme to return DARK_THEME, mocks time.sleep as a no-op, and mocks sys.stdout as a TTY. Assert that play_animation() returns "", that stdout.write was called, and that the written output contains ANSI escape codes (\033[), box-drawing characters (), and cursor hide/restore sequences (\033[?25l and \033[?25h). Follow the existing test patterns in the class.

Added tests. thanks for the suggestion!

@romanlutz romanlutz merged commit cea4f7c into Azure:main Mar 13, 2026
38 checks passed
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.

4 participants