Skip to main content

voirs_cli/commands/interactive/
shell.rs

1//! Interactive shell implementation for VoiRS CLI
2//!
3//! Provides a command-line interface with:
4//! - History support
5//! - Tab completion for commands and voices
6//! - Context-aware suggestions
7//! - Session state management
8
9use super::{
10    commands::CommandProcessor, session::SessionManager, synthesis::SynthesisEngine,
11    InteractiveOptions,
12};
13use crate::config::Config;
14use crate::error::{Result, VoirsCliError};
15use console::{style, Term};
16use dialoguer::{theme::ColorfulTheme, Input};
17use std::collections::HashMap;
18use std::io::Write;
19
20/// Interactive shell state
21pub struct InteractiveShell {
22    /// Synthesis engine for real-time TTS
23    synthesis_engine: SynthesisEngine,
24
25    /// Session manager for state persistence
26    session_manager: SessionManager,
27
28    /// Command processor for interactive commands
29    command_processor: CommandProcessor,
30
31    /// Command history
32    history: Vec<String>,
33
34    /// Current session state
35    current_voice: Option<String>,
36    current_speed: f32,
37    current_pitch: f32,
38    current_volume: f32,
39
40    /// Shell options
41    options: InteractiveOptions,
42
43    /// Terminal interface
44    term: Term,
45
46    /// Available voices cache
47    available_voices: Vec<String>,
48
49    /// Running state
50    running: bool,
51}
52
53impl InteractiveShell {
54    /// Create a new interactive shell
55    pub async fn new(options: InteractiveOptions) -> Result<Self> {
56        let term = Term::stdout();
57
58        // Initialize synthesis engine
59        let synthesis_engine = SynthesisEngine::new().await?;
60
61        // Initialize session manager
62        let mut session_manager = SessionManager::new(options.auto_save);
63
64        // Load session if specified
65        if let Some(session_path) = &options.load_session {
66            session_manager.load_session(session_path).await?;
67        }
68
69        // Get available voices
70        let available_voices = synthesis_engine.list_voices().await?;
71
72        // Initialize command processor
73        let command_processor = CommandProcessor::new(available_voices.clone());
74
75        // Set initial voice
76        let current_voice = options
77            .voice
78            .clone()
79            .or_else(|| session_manager.get_current_voice().cloned())
80            .or_else(|| available_voices.first().cloned());
81
82        let mut shell = Self {
83            synthesis_engine,
84            session_manager,
85            command_processor,
86            history: Vec::new(),
87            current_voice: current_voice.clone(),
88            current_speed: 1.0,
89            current_pitch: 0.0,
90            current_volume: 1.0,
91            options,
92            term,
93            available_voices,
94            running: true,
95        };
96
97        // Set initial voice in synthesis engine
98        if let Some(voice) = current_voice {
99            shell.synthesis_engine.set_voice(&voice).await?;
100        }
101
102        Ok(shell)
103    }
104
105    /// Run the main interactive loop
106    pub async fn run(&mut self) -> Result<()> {
107        self.print_welcome();
108        self.print_help_hint();
109
110        while self.running {
111            match self.read_command().await {
112                Ok(command) => {
113                    if let Err(e) = self.process_command(&command).await {
114                        self.print_error(&e);
115                    }
116                }
117                Err(e) => {
118                    if self.should_exit_on_error(&e) {
119                        break;
120                    }
121                    self.print_error(&e);
122                }
123            }
124        }
125
126        self.print_goodbye();
127        Ok(())
128    }
129
130    /// Read a command from user input
131    async fn read_command(&mut self) -> Result<String> {
132        let prompt = self.create_prompt();
133
134        // Check if we're in a non-terminal environment (like tests)
135        if !console::Term::stdout().is_term() {
136            // In non-terminal mode, read from stdin using standard input
137            use std::io::{self, BufRead};
138            let stdin = io::stdin();
139            let mut line = String::new();
140            match stdin.lock().read_line(&mut line) {
141                Ok(0) => {
142                    // EOF reached, exit gracefully
143                    self.running = false;
144                    return Ok("quit".to_string());
145                }
146                Ok(_) => {
147                    let input = line.trim().to_string();
148                    if input == "quit" || input == "exit" {
149                        self.running = false;
150                    }
151                    return Ok(input);
152                }
153                Err(e) => {
154                    return Err(VoirsCliError::IoError(format!("Input error: {}", e)));
155                }
156            }
157        }
158
159        let input: String = Input::with_theme(&ColorfulTheme::default())
160            .with_prompt(&prompt)
161            .interact_text()
162            .map_err(|e| VoirsCliError::IoError(format!("Input error: {}", e)))?;
163
164        // Add to history if not empty and not a duplicate
165        if !input.trim().is_empty() && self.history.last() != Some(&input) {
166            self.history.push(input.clone());
167
168            // Limit history size
169            if self.history.len() > 1000 {
170                self.history.remove(0);
171            }
172        }
173
174        Ok(input)
175    }
176
177    /// Process a user command
178    async fn process_command(&mut self, command: &str) -> Result<()> {
179        let command = command.trim();
180
181        if command.is_empty() {
182            return Ok(());
183        }
184
185        // Check for interactive commands (starting with :)
186        if command.starts_with(':') {
187            return self.handle_shell_command(command).await;
188        }
189
190        // Regular text for synthesis
191        self.synthesize_text(command).await?;
192
193        // Add to session history
194        self.session_manager
195            .add_synthesis(command, &self.current_voice);
196
197        Ok(())
198    }
199
200    /// Handle shell commands (starting with :)
201    async fn handle_shell_command(&mut self, command: &str) -> Result<()> {
202        // We need to extract the command processor to avoid borrowing self multiple ways
203        let available_voices = self.command_processor.available_voices().clone();
204        let temp_processor = CommandProcessor::new(available_voices);
205        temp_processor.process_command(self, command).await
206    }
207
208    /// Synthesize text and optionally play audio
209    async fn synthesize_text(&mut self, text: &str) -> Result<()> {
210        let start_time = std::time::Instant::now();
211
212        // Show synthesis indicator
213        self.print_status(&format!("Synthesizing: \"{}\"", text));
214
215        // Perform synthesis
216        let audio_data = self.synthesis_engine.synthesize(text).await?;
217
218        let synthesis_time = start_time.elapsed();
219
220        // Play audio if not disabled
221        if !self.options.no_audio {
222            self.synthesis_engine.play_audio(&audio_data).await?;
223        }
224
225        // Show completion status
226        self.print_status(&format!(
227            "✓ Synthesis completed in {:.2}s ({} samples)",
228            synthesis_time.as_secs_f64(),
229            audio_data.len()
230        ));
231
232        Ok(())
233    }
234
235    /// Create the command prompt
236    fn create_prompt(&self) -> String {
237        let voice_part = if let Some(ref voice) = self.current_voice {
238            format!("{}@{}", style("voirs").cyan(), style(voice).green())
239        } else {
240            style("voirs").cyan().to_string()
241        };
242
243        let params =
244            if self.current_speed != 1.0 || self.current_pitch != 0.0 || self.current_volume != 1.0
245            {
246                format!(
247                    " [s:{:.1} p:{:.1} v:{:.1}]",
248                    self.current_speed, self.current_pitch, self.current_volume
249                )
250            } else {
251                String::new()
252            };
253
254        format!("{}{}> ", voice_part, style(&params).dim())
255    }
256
257    /// Provide tab completion suggestions
258    fn complete_input(&self, input: &str) -> Vec<String> {
259        let mut suggestions = Vec::new();
260
261        if input.starts_with(':') {
262            // Command completion
263            let commands = [
264                ":help", ":voice", ":voices", ":speed", ":pitch", ":volume", ":save", ":load",
265                ":history", ":clear", ":status", ":quit", ":exit",
266            ];
267
268            for cmd in &commands {
269                if cmd.starts_with(input) {
270                    suggestions.push(cmd.to_string());
271                }
272            }
273        } else if input.contains(" ") && input.starts_with(":voice ") {
274            // Voice name completion
275            let voice_prefix = input.strip_prefix(":voice ").unwrap_or("");
276            for voice in &self.available_voices {
277                if voice.starts_with(voice_prefix) {
278                    suggestions.push(format!(":voice {}", voice));
279                }
280            }
281        }
282
283        suggestions
284    }
285
286    /// Print welcome message
287    fn print_welcome(&self) {
288        println!(
289            "{}",
290            style("Welcome to VoiRS Interactive Mode").bold().cyan()
291        );
292        println!("Type text to synthesize, or use :help for commands");
293
294        if let Some(ref voice) = self.current_voice {
295            println!("Current voice: {}", style(voice).green());
296        }
297
298        println!();
299    }
300
301    /// Print help hint
302    fn print_help_hint(&self) {
303        println!(
304            "{}",
305            style("Hint: Type ':help' for available commands").dim()
306        );
307        println!();
308    }
309
310    /// Print goodbye message
311    fn print_goodbye(&self) {
312        println!("\\n{}", style("Goodbye! 👋").cyan());
313    }
314
315    /// Print status message
316    fn print_status(&self, message: &str) {
317        println!("{} {}", style("ℹ").blue(), message);
318    }
319
320    /// Print error message
321    fn print_error(&self, error: &VoirsCliError) {
322        eprintln!("{} {}", style("✗").red(), style(error).red());
323    }
324
325    /// Check if we should exit on this error
326    fn should_exit_on_error(&self, _error: &VoirsCliError) -> bool {
327        // For now, don't exit on any errors - keep the shell running
328        false
329    }
330
331    /// Get current voice
332    pub fn current_voice(&self) -> Option<&str> {
333        self.current_voice.as_deref()
334    }
335
336    /// Set current voice
337    pub async fn set_voice(&mut self, voice: String) -> Result<()> {
338        self.synthesis_engine.set_voice(&voice).await?;
339        self.current_voice = Some(voice.clone());
340        self.session_manager.set_current_voice(voice);
341        Ok(())
342    }
343
344    /// Get available voices
345    pub fn available_voices(&self) -> &[String] {
346        &self.available_voices
347    }
348
349    /// Get current synthesis parameters
350    pub fn current_params(&self) -> (f32, f32, f32) {
351        (self.current_speed, self.current_pitch, self.current_volume)
352    }
353
354    /// Set synthesis parameters
355    pub async fn set_params(
356        &mut self,
357        speed: Option<f32>,
358        pitch: Option<f32>,
359        volume: Option<f32>,
360    ) -> Result<()> {
361        if let Some(s) = speed {
362            self.current_speed = s.clamp(0.1, 3.0);
363            self.synthesis_engine.set_speed(self.current_speed).await?;
364        }
365
366        if let Some(p) = pitch {
367            self.current_pitch = p.clamp(-12.0, 12.0);
368            self.synthesis_engine.set_pitch(self.current_pitch).await?;
369        }
370
371        if let Some(v) = volume {
372            self.current_volume = v.clamp(0.0, 2.0);
373            self.synthesis_engine
374                .set_volume(self.current_volume)
375                .await?;
376        }
377
378        Ok(())
379    }
380
381    /// Get session manager
382    pub fn session_manager(&mut self) -> &mut SessionManager {
383        &mut self.session_manager
384    }
385
386    /// Exit the shell
387    pub fn exit(&mut self) {
388        self.running = false;
389    }
390}