10 Commits

Author SHA1 Message Date
51c2c5a14a Fix formatting: reformat imports and thread closure
Apply rustfmt to fix formatting issues:
- Reformat nested use statement for std::sync imports
- Reformat thread::spawn closure to use inline loop syntax

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-09 07:11:26 +01:00
12793893c3 Fix: Propagate user exit signal to break animation loop
**CRITICAL BUG FIX**: When using -l (loop), the outer loop in main.rs
was restarting the animation even after user pressed exit keys.

Changes:
- render() now returns Result<bool> instead of Result<()>
- Returns true when user presses exit key (q/ESC/Ctrl+C)
- Returns false when animation completes naturally
- main.rs checks return value and breaks loop on user exit

This fixes the infinite loop issue where pressing q/ESC/Ctrl+C
had no effect when using the -l flag.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-09 07:08:24 +01:00
946289544d Add frequent exit checks throughout render loop
The issue was that sleep() was blocking for 16-33ms without checking
the exit flag. Now:
- Check should_exit at 5 points in the render loop
- Break sleep into 5ms chunks, checking between each chunk
- This gives <5ms response time to exit commands

Exit responds within 5ms: Press 'q', ESC, or Ctrl+C

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-09 07:03:57 +01:00
4e88ea5677 Use background thread for reliable keyboard event handling
Previous attempts to poll keyboard events in the render loop were
failing. Now using a dedicated background thread that continuously
monitors for exit keys and communicates with the render loop via
an atomic boolean flag.

This ensures keyboard events are never missed, even during heavy
rendering operations.

Exit: Press 'q', ESC, or Ctrl+C

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-09 07:00:04 +01:00
b2a9b6df36 Simplify keyboard handling and increase poll timeout
Removed complex signal handling that wasn't working reliably.
Now using simpler event polling with 10ms timeout instead of 0ms,
which allows keyboard events to be properly detected.

Exit methods:
- Press 'q' (most reliable)
- Press ESC
- Ctrl+C (with 10ms poll window)
- Ctrl+D (alternative)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-09 06:53:19 +01:00
5a707df725 Improve Ctrl+C handling with dual detection method
Implemented both keyboard event and signal detection for Ctrl+C:
1. Check for Ctrl+C as keyboard event (KeyModifiers::CONTROL + 'c')
2. Check for SIGINT signal via tokio::signal::unix

This ensures Ctrl+C works reliably in both raw terminal mode and
through signal handling.

Exit options: Ctrl+C, 'q', or ESC

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-09 06:41:07 +01:00
097fca6ed3 Fix Ctrl+C handling with proper tokio signal handler
Previous keyboard event polling didn't work for Ctrl+C in raw mode.
Now using tokio::signal::ctrl_c() with a background task that signals
the render loop through a channel when Ctrl+C is pressed.

Changes:
- Added "signal" and "sync" features to tokio dependency
- Spawn background task to listen for Ctrl+C signal
- Use mpsc channel to communicate signal to render loop
- Keep 'q' and ESC keyboard shortcuts for convenience

Ctrl+C now properly exits looping animations.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-09 06:38:15 +01:00
5a0ff3f5cc Add keyboard input handling to allow exiting animations
Fixed issue where Ctrl+C did not work to exit looping animations.
In raw terminal mode, signals are not automatically handled, so we
need to manually poll for keyboard events.

Changes:
- Added crossterm event polling in render loop
- Check for Ctrl+C, 'q', or ESC key to exit animation
- Polls with 0ms timeout to avoid blocking animation frames

Users can now exit with:
- Ctrl+C
- Press 'q'
- Press ESC

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-09 06:29:02 +01:00
4035f2dc23 Fix release workflow to upload platform-specific binary names
Previously all binaries were uploaded with the same name 'piglet',
causing them to overwrite each other. Now each binary is renamed
to include the target platform (e.g., piglet-x86_64-unknown-linux-gnu)
before uploading.

This ensures all 4 platform binaries are available in the release.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-09 04:46:37 +01:00
000823457d Fix release workflow permissions for creating releases
Added 'contents: write' permission to allow GITHUB_TOKEN to create
releases. This fixes the 403 error when the workflow tries to create
a GitHub release.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-09 04:42:34 +01:00
5 changed files with 84 additions and 11 deletions

View File

@@ -5,6 +5,9 @@ on:
tags:
- 'v*.*.*'
permissions:
contents: write
env:
CARGO_TERM_COLOR: always
RUST_BACKTRACE: 1
@@ -148,11 +151,14 @@ jobs:
if: matrix.os == 'macos-latest'
run: strip target/${{ matrix.target }}/release/piglet
- name: Rename binary
run: |
cp target/${{ matrix.target }}/release/piglet piglet-${{ matrix.target }}
- name: Upload release binary
uses: softprops/action-gh-release@v1
with:
files: target/${{ matrix.target }}/release/piglet
name: piglet-${{ matrix.target }}
files: piglet-${{ matrix.target }}
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

View File

@@ -21,8 +21,8 @@ palette = "0.7"
# Terminal manipulation
crossterm = "0.27"
# Async runtime (for timing)
tokio = { version = "1.35", features = ["time", "rt-multi-thread", "macros"] }
# Async runtime (for timing and signal handling)
tokio = { version = "1.35", features = ["time", "rt-multi-thread", "macros", "signal", "sync"] }
# Process execution
which = "5.0"

View File

@@ -43,7 +43,7 @@ impl AnimationEngine {
self
}
pub async fn run(&self, terminal: &mut TerminalManager) -> Result<()> {
pub async fn run(&self, terminal: &mut TerminalManager) -> Result<bool> {
let renderer = renderer::Renderer::new(
&self.ascii_art,
self.duration_ms,

View File

@@ -2,6 +2,12 @@ use crate::animation::{easing::EasingFunction, effects::Effect, timeline::Timeli
use crate::color::{apply, ColorEngine};
use crate::utils::{ansi, ascii::AsciiArt, terminal::TerminalManager};
use anyhow::Result;
use crossterm::event::{self, Event, KeyCode, KeyModifiers};
use std::sync::{
atomic::{AtomicBool, Ordering},
Arc,
};
use std::time::Duration;
use tokio::time::sleep;
pub struct Renderer<'a> {
@@ -30,17 +36,52 @@ impl<'a> Renderer<'a> {
}
}
pub async fn render(&self, terminal: &mut TerminalManager) -> Result<()> {
pub async fn render(&self, terminal: &mut TerminalManager) -> Result<bool> {
let mut timeline = Timeline::new(self.timeline.duration_ms(), self.timeline.fps());
timeline.start();
// Spawn background thread to listen for exit keys
let should_exit = Arc::new(AtomicBool::new(false));
let should_exit_clone = should_exit.clone();
std::thread::spawn(move || loop {
if let Ok(true) = event::poll(Duration::from_millis(100)) {
if let Ok(Event::Key(key)) = event::read() {
match key.code {
KeyCode::Char('q') | KeyCode::Esc => {
should_exit_clone.store(true, Ordering::Relaxed);
break;
}
KeyCode::Char('c') if key.modifiers.contains(KeyModifiers::CONTROL) => {
should_exit_clone.store(true, Ordering::Relaxed);
break;
}
_ => {}
}
}
}
if should_exit_clone.load(Ordering::Relaxed) {
break;
}
});
loop {
// Check for exit FIRST
if should_exit.load(Ordering::Relaxed) {
return Ok(true); // User requested exit
}
let frame_start = std::time::Instant::now();
// Calculate progress with easing
let linear_progress = timeline.progress();
let eased_progress = self.easing.ease(linear_progress);
// Check again before rendering
if should_exit.load(Ordering::Relaxed) {
return Ok(true); // User requested exit
}
// Apply effect
let effect_result = self.effect.apply(self.ascii_art, eased_progress);
@@ -51,6 +92,11 @@ impl<'a> Renderer<'a> {
effect_result.text.clone()
};
// Check before terminal operations
if should_exit.load(Ordering::Relaxed) {
return Ok(true); // User requested exit
}
// Render to terminal
terminal.clear()?;
terminal.refresh_size()?;
@@ -82,9 +128,14 @@ impl<'a> Renderer<'a> {
}
}
// Check if user wants to exit
if should_exit.load(Ordering::Relaxed) {
return Ok(true); // User requested exit
}
// Check if animation is complete before advancing
if timeline.is_complete() {
break;
return Ok(false); // Animation completed naturally
}
// Advance to next frame and wait
@@ -93,11 +144,21 @@ impl<'a> Renderer<'a> {
let elapsed = frame_start.elapsed();
if elapsed < frame_duration {
sleep(frame_duration - elapsed).await;
let sleep_duration = frame_duration - elapsed;
// Break sleep into small chunks to check should_exit frequently
let chunk_duration = Duration::from_millis(5);
let mut remaining = sleep_duration;
while remaining > Duration::ZERO {
if should_exit.load(Ordering::Relaxed) {
return Ok(true); // User requested exit during sleep
}
let sleep_time = remaining.min(chunk_duration);
sleep(sleep_time).await;
remaining = remaining.saturating_sub(sleep_time);
}
}
}
Ok(())
}
fn apply_colors(&self, text: &str, progress: f64) -> String {

View File

@@ -61,8 +61,14 @@ async fn run_piglet(args: PigletCli) -> Result<()> {
// Run animation
loop {
animation_engine.run(&mut terminal).await?;
let user_exited = animation_engine.run(&mut terminal).await?;
// If user pressed exit key, stop looping
if user_exited {
break;
}
// If not looping, stop after one animation
if !args.loop_animation {
break;
}