From 93a50ddb42574f9b9afcfc31ac0c5d78ddec0991 Mon Sep 17 00:00:00 2001 From: "K. Hodges" Date: Wed, 20 May 2026 03:55:43 -0700 Subject: [PATCH] added hotdog animations I am a professional software engineer --- nightshift/cli.py | 23 +++++- nightshift/terminal.py | 163 +++++++++++++++++++++++++++++++++++++++++ nightshift/version.py | 6 +- tests/test_terminal.py | 21 +++++- tests/test_version.py | 4 +- 5 files changed, 208 insertions(+), 9 deletions(-) diff --git a/nightshift/cli.py b/nightshift/cli.py index 510bbd9..599794b 100644 --- a/nightshift/cli.py +++ b/nightshift/cli.py @@ -14,7 +14,7 @@ from .integ_setup import format_setup_result, setup_python_project from .pipeline import PipelineRunner from .runlog import RunLogger from .status import build_status, format_status -from .terminal import format_banner, style_text +from .terminal import HOTDOG_ANIMATIONS, TerminalAnimation, format_banner, style_text from .tasks import ( ensure_dependencies_satisfied, parse_task_file, @@ -49,6 +49,13 @@ def build_parser() -> argparse.ArgumentParser: run_parser.add_argument("--config", default="nightshift.yaml", help="Config file to use.") run_parser.add_argument("--task", help="Specific task id to run.") run_parser.add_argument("--all", action="store_true", help="Run all runnable incomplete tasks.") + run_parser.add_argument( + "--animation", + default="agent_thinking", + choices=tuple(sorted(HOTDOG_ANIMATIONS)), + help="Terminal animation to show while the run is active.", + ) + run_parser.add_argument("--no-animation", action="store_true", help="Disable terminal animation.") status_parser = subparsers.add_parser("status", help="Inspect NightShift project status.") status_parser.add_argument("--config", default="nightshift.yaml", help="Config file to inspect.") @@ -140,7 +147,12 @@ def main(argv: list[str] | None = None) -> int: runner = PipelineRunner(config, logger=RunLogger(console=print)) if args.all: selected = [task for task in tasks if not task.completed] - result = runner.run_tasks(selected) + with TerminalAnimation( + args.animation, + message="NightShift running all tasks", + enabled=not args.no_animation, + ): + result = runner.run_tasks(selected) print(f"Status: {result.status}") print(f"Tasks run: {len(result.task_results)}") print(f"Completed: {result.completed_count}") @@ -150,7 +162,12 @@ def main(argv: list[str] | None = None) -> int: task = select_task_by_id(tasks, args.task) if args.task else select_next_runnable_task(tasks) ensure_dependencies_satisfied(tasks, task) - result = runner.run_task(task) + with TerminalAnimation( + args.animation, + message=f"NightShift running {task.id}", + enabled=not args.no_animation, + ): + result = runner.run_task(task) print(f"Task: {result.task_id}") print(style_text(f"Status: {result.status}", color=_status_color(result.status), bold=True)) print(f"Retries: {result.retry_count}") diff --git a/nightshift/terminal.py b/nightshift/terminal.py index 7b6e202..662347c 100644 --- a/nightshift/terminal.py +++ b/nightshift/terminal.py @@ -6,6 +6,8 @@ import os import sys from typing import TextIO import random +import threading +import time from .version import display_version @@ -41,6 +43,167 @@ BANNER_MESSAGES = [ ] quote = random.choice(BANNER_MESSAGES) +HOTDOG_ANIMATIONS = { + "classic_dance": [ + "🌭", + "ヽ(🌭)ノ", + "(🌭)", + "(🌭)", + "(🌭)", + ], + "shuffle_mode": [ + " (🌭) ", + " (🌭) ", + "<(🌭<)", + "(>🌭)>", + "~(🌭)~", + ], + "gremlin_energy": [ + "(ノ🌭)ノ", + "ᕕ(🌭)ᕗ", + "^(🌭)^", + "(🌭)b", + "(🌭)", + ], + "roller_grill": [ + "🌭", + "🌭", + "🌭", + "🌭", + "🌭", + ], + "ascending_glizzy": [ + "🌭", + " 🌭", + " 🌭", + " ", + "🌭", + ], + "agent_thinking": [ + "🌭 .", + "🌭 ..", + "🌭 ...", + "🌭 ....", + "🌭 ???", + ], + "tubular_offering": [ + " つ 🌭_🌭 つ", + " つ🌭 _🌭 つ", + " つ 🌭🌭 つ", + " つ🌭🌭_ つ", + " つ 🌭_🌭 つ", + ], + "tubular_offering_wobble": [ + " つ 🌭_🌭 つ", + " つ 🌭~🌭 つ", + " つ ~🌭~ つ", + " つ 🌭~🌭 つ", + " つ 🌭_🌭 つ", + ], + "chaotic_summoning": [ + " つ 🌭_🌭 つ", + " つ 🌭 つ", + " つ 🌭🔥 つ", + " つ 🌭 つ", + " つ 🌭_🌭 つ", + ], + "hotdog_ritual_dance": [ + "( ಠ_ಠ)🌭(ಠ_ಠ )", + "( ಠ_ಠ)🌭(ಠ_ಠ )", + "( ಠ_ಠ) 🌭 (ಠ_ಠ )", + "( ಠ_ಠ) 🌭 (ಠ_ಠ )", + "( ಠ_ಠ)🌭(ಠ_ಠ )", + ], + "ritual_side_to_side": [ + "( ಠ_ಠ)🌭(ಠ_ಠ )", + "( ಠ_ಠ) 🌭(ಠ_ಠ )", + "( ಠ_ಠ) 🌭(ಠ_ಠ )", + "( ಠ_ಠ) (ಠ_ಠ )", + "( ಠ_ಠ)🌭(ಠ_ಠ )", + ], + "full_rave_mode": [ + "( ಠ_ಠ)🌭(ಠ_ಠ )", + "(ಠ_ಠ )🌭( ಠ_ಠ)", + "( ಠ_ಠ)🌭(ಠ_ಠ )", + "(ಠ_ಠ )🔥🌭🔥( ಠ_ಠ)", + "( ಠ_ಠ)🌭(ಠ_ಠ )", + ], + "terminal_cult_initiation": [ + "( ಠ_ಠ) (ಠ_ಠ )", + "( ಠ_ಠ)🌭 (ಠ_ಠ )", + "( ಠ_ಠ) 🌭 (ಠ_ಠ )", + "( ಠ_ಠ) 🌭 (ಠ_ಠ )", + "( ಠ_ಠ) 🌭 (ಠ_ಠ )", + ], +} + + +class TerminalAnimation: + """Transient terminal status animation.""" + + def __init__( + self, + name: str = "agent_thinking", + *, + message: str = "NightShift running", + stream: TextIO | None = None, + interval_seconds: float = 0.18, + enabled: bool = True, + ) -> None: + self.frames = animation_frames(name) + self.message = message + self.stream = stream or sys.stderr + self.interval_seconds = interval_seconds + self.enabled = enabled and should_style(self.stream) + self._stop = threading.Event() + self._thread: threading.Thread | None = None + self._width = 0 + + def __enter__(self) -> "TerminalAnimation": + self.start() + return self + + def __exit__(self, *_exc: object) -> None: + self.stop() + + def start(self) -> None: + if not self.enabled or self._thread is not None: + return + self._thread = threading.Thread(target=self._run, daemon=True) + self._thread.start() + + def stop(self) -> None: + if self._thread is None: + return + self._stop.set() + self._thread.join(timeout=1) + self._clear() + self._thread = None + + def _run(self) -> None: + index = 0 + while not self._stop.is_set(): + frame = self.frames[index % len(self.frames)] + text = f"{frame} {self.message}" + self._width = max(self._width, len(text)) + self.stream.write("\r" + text.ljust(self._width)) + self.stream.flush() + index += 1 + self._stop.wait(self.interval_seconds) + + def _clear(self) -> None: + if not self.enabled: + return + self.stream.write("\r" + (" " * self._width) + "\r") + self.stream.flush() + + +def animation_frames(name: str) -> tuple[str, ...]: + frames = HOTDOG_ANIMATIONS.get(name) + if not frames: + frames = HOTDOG_ANIMATIONS["agent_thinking"] + return tuple(frames) + def should_style(stream: TextIO | None = None) -> bool: stream = stream or sys.stdout if os.environ.get("NO_COLOR"): diff --git a/nightshift/version.py b/nightshift/version.py index 034b45b..39aaa2e 100644 --- a/nightshift/version.py +++ b/nightshift/version.py @@ -3,10 +3,10 @@ from __future__ import annotations -PACKAGE_VERSION = "0.2.2" +PACKAGE_VERSION = "0.2.3" RELEASE_CHANNEL = "alpha" -hotdog_version = "footlong" -topping_version = "mustard" +hotdog_version = "new-york" +topping_version = "sport-peppers" HOTDOG_VERSIONS = ( "bratwurst", diff --git a/tests/test_terminal.py b/tests/test_terminal.py index 11fcf0f..f86f2f3 100644 --- a/tests/test_terminal.py +++ b/tests/test_terminal.py @@ -6,7 +6,13 @@ from unittest.mock import patch from nightshift.artifacts import ArtifactStore from nightshift.runlog import RunLogger -from nightshift.terminal import format_banner, format_console_event_line +from nightshift.terminal import ( + HOTDOG_ANIMATIONS, + TerminalAnimation, + animation_frames, + format_banner, + format_console_event_line, +) class FakeTTY(StringIO): @@ -25,6 +31,19 @@ class TerminalStylingTests(unittest.TestCase): self.assertIn("NightShift", banner) self.assertIn("\x1b[", banner) + def test_animation_frames_fall_back_to_agent_thinking(self) -> None: + self.assertEqual(animation_frames("missing"), tuple(HOTDOG_ANIMATIONS["agent_thinking"])) + self.assertEqual(animation_frames("classic_dance"), tuple(HOTDOG_ANIMATIONS["classic_dance"])) + + def test_terminal_animation_is_disabled_for_non_tty(self) -> None: + stream = StringIO() + animation = TerminalAnimation(stream=stream) + + with animation: + pass + + self.assertEqual(stream.getvalue(), "") + def test_console_event_line_colors_success_and_failure(self) -> None: success = format_console_event_line( "2026-05-17T00:00:00Z", diff --git a/tests/test_version.py b/tests/test_version.py index 7c59a18..6c7f65a 100644 --- a/tests/test_version.py +++ b/tests/test_version.py @@ -15,8 +15,8 @@ from nightshift.version import ( class VersionTests(unittest.TestCase): def test_display_version_includes_channel_hotdog_and_topping(self) -> None: - self.assertEqual(display_version(), "0.2.2-alpha-footlong-mustard") - self.assertEqual(PACKAGE_VERSION, "0.2.2") + self.assertEqual(display_version(), "0.2.3-alpha-new-york-sport-peppers") + self.assertEqual(PACKAGE_VERSION, "0.2.3") self.assertIn(hotdog_version, HOTDOG_VERSIONS) self.assertIn(topping_version, TOPPING_VERSIONS)