6060#include < thread>
6161#include < chrono>
6262#include < atomic>
63+ #include < csignal>
6364#include < map>
6465#include < set>
6566#include < memory>
@@ -1527,6 +1528,9 @@ namespace pythonic
15271528
15281529 // ==================== Animation Support ====================
15291530
1531+ // Global flag for signal handling in animations
1532+ inline std::atomic<bool > g_animation_interrupted{false };
1533+
15301534 /* *
15311535 * @brief Configuration for plot animations
15321536 *
@@ -1542,7 +1546,7 @@ namespace pythonic
15421546 * cfg.height = 40;
15431547 *
15441548 * // Or using the fluent builder pattern:
1545- * auto cfg = AnimateConfig().x_range(-PI, PI).size(120, 40).fps(60);
1549+ * auto cfg = AnimateConfig().x_range(-PI, PI).size(120, 40).fps(60).loop(false) ;
15461550 */
15471551 struct AnimateConfig
15481552 {
@@ -1553,6 +1557,7 @@ namespace pythonic
15531557 int width = 80 ; // /< Width in terminal characters
15541558 int height = 24 ; // /< Height in terminal characters
15551559 bool use_pixels = false ; // /< If true, width/height are pixels (converted to chars)
1560+ bool loop_animation = true ; // /< If true, loop animation; if false, stop after duration
15561561 std::string label_color = " cyan" ; // /< Color for X/Y axis labels
15571562 std::string range_color = " magenta" ; // /< Color for min/max range values
15581563 std::string title = " " ; // /< Optional title
@@ -1603,6 +1608,11 @@ namespace pythonic
16031608 title = t;
16041609 return *this ;
16051610 }
1611+ AnimateConfig &loop (bool l)
1612+ {
1613+ loop_animation = l;
1614+ return *this ;
1615+ }
16061616
16071617 // Get actual character dimensions (converts pixels if needed)
16081618 int char_width () const { return use_pixels ? (width + 1 ) / 2 : width; }
@@ -1612,6 +1622,53 @@ namespace pythonic
16121622 // Internal implementation for multi-plot animation
16131623 namespace detail
16141624 {
1625+ // Signal handler for clean Ctrl+C exit
1626+ inline void animation_signal_handler (int /* sig*/ )
1627+ {
1628+ g_animation_interrupted.store (true );
1629+ }
1630+
1631+ // RAII helper to manage terminal state and signal handlers
1632+ class TerminalStateGuard
1633+ {
1634+ public:
1635+ TerminalStateGuard ()
1636+ {
1637+ // Reset interrupt flag
1638+ g_animation_interrupted.store (false );
1639+
1640+ // Save old signal handler and install ours
1641+ _old_handler = std::signal (SIGINT, animation_signal_handler);
1642+
1643+ // Hide cursor
1644+ std::cout << " \033 [?25l" << std::flush;
1645+ }
1646+
1647+ ~TerminalStateGuard ()
1648+ {
1649+ // Restore cursor
1650+ std::cout << " \033 [?25h" << std::flush;
1651+
1652+ // Clear screen back to normal position and add newline
1653+ std::cout << " \n "
1654+ << std::flush;
1655+
1656+ // Restore old signal handler
1657+ if (_old_handler != SIG_ERR)
1658+ {
1659+ std::signal (SIGINT, _old_handler);
1660+ }
1661+ }
1662+
1663+ bool interrupted () const
1664+ {
1665+ return g_animation_interrupted.load ();
1666+ }
1667+
1668+ private:
1669+ void (*_old_handler)(int ) = SIG_DFL;
1670+ };
1671+
16151672 template <typename ... PlotEntries>
16161673 inline void animate_impl (const AnimateConfig &cfg, PlotEntries &&...plots)
16171674 {
@@ -1648,57 +1705,71 @@ namespace pythonic
16481705 (sample_range (plots), ...);
16491706 fig.ylim (y_min - 0.1 * (y_max - y_min), y_max + 0.1 * (y_max - y_min));
16501707
1651- // Hide cursor
1652- std::cout << " \033 [?25l " << std::flush ;
1708+ // Use RAII guard for terminal state and signal handling
1709+ TerminalStateGuard terminal_guard ;
16531710
16541711 auto frame_time = std::chrono::microseconds (static_cast <int >(1000000.0 / cfg.fps ));
16551712 auto start_time = std::chrono::steady_clock::now ();
16561713
1657- try
1714+ while (!terminal_guard. interrupted ())
16581715 {
1659- while (true )
1660- {
1661- auto now = std::chrono::steady_clock::now ();
1662- double t = std::chrono::duration<double >(now - start_time).count ();
1716+ auto now = std::chrono::steady_clock::now ();
1717+ double t = std::chrono::duration<double >(now - start_time).count ();
16631718
1664- if (t > cfg.duration )
1719+ // Handle looping vs stopping
1720+ if (t > cfg.duration )
1721+ {
1722+ if (cfg.loop_animation )
16651723 {
16661724 t = std::fmod (t, cfg.duration );
1725+ start_time = now - std::chrono::duration_cast<std::chrono::steady_clock::duration>(
1726+ std::chrono::duration<double >(t));
16671727 }
1728+ else
1729+ {
1730+ // Non-looping: stop after duration
1731+ break ;
1732+ }
1733+ }
16681734
1669- fig.clear ();
1670- fig.set_time (t);
1735+ fig.clear ();
1736+ fig.set_time (t);
1737+
1738+ auto plot_one = [&](auto &&plot_tuple)
1739+ {
1740+ auto &f = std::get<0 >(plot_tuple);
1741+ const auto &color = std::get<1 >(plot_tuple);
16711742
1672- auto plot_one = [&]( auto && plot_tuple)
1743+ if constexpr (std::tuple_size_v<std:: decay_t < decltype ( plot_tuple)>> >= 3 )
16731744 {
1674- auto &f = std::get<0 >(plot_tuple);
1675- const auto &color = std::get<1 >(plot_tuple);
1745+ const auto &label = std::get<2 >(plot_tuple);
1746+ fig.plot_animated (f, cfg.x_min , cfg.x_max , color, label);
1747+ }
1748+ else
1749+ {
1750+ fig.plot_animated (f, cfg.x_min , cfg.x_max , color);
1751+ }
1752+ };
16761753
1677- if constexpr (std::tuple_size_v<std::decay_t <decltype (plot_tuple)>> >= 3 )
1678- {
1679- const auto &label = std::get<2 >(plot_tuple);
1680- fig.plot_animated (f, cfg.x_min , cfg.x_max , color, label);
1681- }
1682- else
1683- {
1684- fig.plot_animated (f, cfg.x_min , cfg.x_max , color);
1685- }
1686- };
1754+ (plot_one (plots), ...);
16871755
1688- ( plot_one (plots), ... );
1756+ std::cout << " \033 [H " << fig. render_to_string ( );
16891757
1690- std::cout << " \033 [H" << fig.render_to_string ();
1758+ // Show different message depending on loop setting
1759+ if (cfg.loop_animation )
1760+ {
16911761 std::cout << " \n t = " << std::fixed << std::setprecision (2 ) << t
16921762 << " s (Press Ctrl+C to stop)" << std::flush;
1693-
1694- std::this_thread::sleep_for (frame_time);
16951763 }
1696- }
1697- catch (...)
1698- {
1699- }
1764+ else
1765+ {
1766+ std::cout << " \n t = " << std::fixed << std::setprecision (2 ) << t
1767+ << " s / " << cfg.duration << " s" << std::flush;
1768+ }
17001769
1701- std::cout << " \033 [?25h" << std::flush;
1770+ std::this_thread::sleep_for (frame_time);
1771+ }
1772+ // TerminalStateGuard destructor will restore terminal state
17021773 }
17031774 } // namespace detail
17041775
0 commit comments