Skip to main content

oxigaf_cli/
interactive.rs

1//! Interactive mode for live parameter adjustment during training.
2//!
3//! Provides keyboard controls to pause/resume training, adjust learning rates,
4//! toggle verbose logging, save checkpoints, and quit gracefully.
5
6use crossterm::{
7    event::{self, Event, KeyCode, KeyEvent},
8    terminal::{disable_raw_mode, enable_raw_mode},
9};
10use std::sync::{
11    atomic::{AtomicBool, AtomicU8, Ordering},
12    Arc,
13};
14use std::time::Duration;
15
16/// Controller for interactive training mode with keyboard controls.
17///
18/// Manages shared state between the keyboard listener thread and the main
19/// training loop using atomic operations for thread-safe communication.
20pub struct InteractiveController {
21    /// Pause/resume training
22    pub paused: Arc<AtomicBool>,
23    /// Learning rate adjustment multiplier (100 = 1.0x, range: 10-200)
24    pub lr_adjustment: Arc<AtomicU8>,
25    /// Toggle verbose logging
26    pub verbose_toggle: Arc<AtomicBool>,
27    /// Request checkpoint save
28    pub save_requested: Arc<AtomicBool>,
29    /// Request graceful quit
30    pub quit_requested: Arc<AtomicBool>,
31}
32
33impl InteractiveController {
34    /// Create a new interactive controller with default state.
35    #[must_use]
36    pub fn new() -> Self {
37        Self {
38            paused: Arc::new(AtomicBool::new(false)),
39            lr_adjustment: Arc::new(AtomicU8::new(100)), // 1.0x multiplier
40            verbose_toggle: Arc::new(AtomicBool::new(false)),
41            save_requested: Arc::new(AtomicBool::new(false)),
42            quit_requested: Arc::new(AtomicBool::new(false)),
43        }
44    }
45
46    /// Start the keyboard listener thread.
47    ///
48    /// Spawns a background thread that listens for keyboard events and updates
49    /// the controller's atomic state. The thread runs until quit is requested.
50    ///
51    /// # Note
52    ///
53    /// This enables terminal raw mode. Make sure to call
54    /// `crossterm::terminal::disable_raw_mode()` on cleanup.
55    pub fn start_keyboard_listener(&self) {
56        let paused = Arc::clone(&self.paused);
57        let lr_adj = Arc::clone(&self.lr_adjustment);
58        let verbose = Arc::clone(&self.verbose_toggle);
59        let save = Arc::clone(&self.save_requested);
60        let quit = Arc::clone(&self.quit_requested);
61
62        std::thread::spawn(move || {
63            // Try to enable raw mode
64            if enable_raw_mode().is_err() {
65                tracing::error!("Failed to enable terminal raw mode for interactive controls");
66                return;
67            }
68
69            loop {
70                // Poll for events with timeout
71                match event::poll(Duration::from_millis(100)) {
72                    Ok(true) => {
73                        // Event available, read it
74                        match event::read() {
75                            Ok(Event::Key(key_event)) => {
76                                handle_key_event(
77                                    key_event, &paused, &lr_adj, &verbose, &save, &quit,
78                                );
79                            }
80                            Ok(_) => {} // Ignore other events
81                            Err(e) => {
82                                tracing::error!("Failed to read keyboard event: {}", e);
83                            }
84                        }
85                    }
86                    Ok(false) => {} // No event available
87                    Err(e) => {
88                        tracing::error!("Failed to poll for keyboard events: {}", e);
89                    }
90                }
91
92                // Check quit flag
93                if quit.load(Ordering::Relaxed) {
94                    let _ = disable_raw_mode();
95                    break;
96                }
97            }
98        });
99    }
100
101    /// Print the interactive controls help message.
102    pub fn print_controls(&self) {
103        println!();
104        println!("Interactive Controls:");
105        println!("  [Space]    Pause/Resume training");
106        println!("  [Up/Down]  Increase/Decrease learning rate");
107        println!("  [v]        Toggle verbose logging");
108        println!("  [s]        Save checkpoint now");
109        println!("  [q]        Quit gracefully");
110        println!();
111    }
112}
113
114impl Default for InteractiveController {
115    fn default() -> Self {
116        Self::new()
117    }
118}
119
120/// Handle keyboard events and update controller state.
121fn handle_key_event(
122    key: KeyEvent,
123    paused: &Arc<AtomicBool>,
124    lr_adj: &Arc<AtomicU8>,
125    verbose: &Arc<AtomicBool>,
126    save: &Arc<AtomicBool>,
127    quit: &Arc<AtomicBool>,
128) {
129    match key.code {
130        KeyCode::Char(' ') => {
131            // Toggle pause state
132            let was_paused = paused.fetch_xor(true, Ordering::Relaxed);
133            let now_paused = !was_paused;
134            if now_paused {
135                println!("\nTraining paused");
136            } else {
137                println!("\nTraining resumed");
138            }
139        }
140        KeyCode::Up => {
141            // Increase learning rate (max 2.0x)
142            let current = lr_adj.load(Ordering::Relaxed);
143            let new = current.saturating_add(10).min(200);
144            lr_adj.store(new, Ordering::Relaxed);
145            println!("\nLearning rate: {:.1}x", f64::from(new) / 100.0);
146        }
147        KeyCode::Down => {
148            // Decrease learning rate (min 0.1x)
149            let current = lr_adj.load(Ordering::Relaxed);
150            let new = current.saturating_sub(10).max(10);
151            lr_adj.store(new, Ordering::Relaxed);
152            println!("\nLearning rate: {:.1}x", f64::from(new) / 100.0);
153        }
154        KeyCode::Char('v') | KeyCode::Char('V') => {
155            // Toggle verbose logging
156            let was_verbose = verbose.fetch_xor(true, Ordering::Relaxed);
157            let now_verbose = !was_verbose;
158            println!("\nVerbose: {}", if now_verbose { "ON" } else { "OFF" });
159        }
160        KeyCode::Char('s') | KeyCode::Char('S') => {
161            // Request checkpoint save
162            save.store(true, Ordering::Relaxed);
163            println!("\nCheckpoint save requested...");
164        }
165        KeyCode::Char('q') | KeyCode::Char('Q') => {
166            // Request graceful quit
167            quit.store(true, Ordering::Relaxed);
168            println!("\nGraceful shutdown requested...");
169        }
170        _ => {} // Ignore other keys
171    }
172}