use super::{
commands::CommandProcessor, session::SessionManager, synthesis::SynthesisEngine,
InteractiveOptions,
};
use crate::config::Config;
use crate::error::{Result, VoirsCliError};
use console::{style, Term};
use dialoguer::{theme::ColorfulTheme, Input};
use std::collections::HashMap;
use std::io::Write;
pub struct InteractiveShell {
synthesis_engine: SynthesisEngine,
session_manager: SessionManager,
command_processor: CommandProcessor,
history: Vec<String>,
current_voice: Option<String>,
current_speed: f32,
current_pitch: f32,
current_volume: f32,
options: InteractiveOptions,
term: Term,
available_voices: Vec<String>,
running: bool,
}
impl InteractiveShell {
pub async fn new(options: InteractiveOptions) -> Result<Self> {
let term = Term::stdout();
let synthesis_engine = SynthesisEngine::new().await?;
let mut session_manager = SessionManager::new(options.auto_save);
if let Some(session_path) = &options.load_session {
session_manager.load_session(session_path).await?;
}
let available_voices = synthesis_engine.list_voices().await?;
let command_processor = CommandProcessor::new(available_voices.clone());
let current_voice = options
.voice
.clone()
.or_else(|| session_manager.get_current_voice().cloned())
.or_else(|| available_voices.first().cloned());
let mut shell = Self {
synthesis_engine,
session_manager,
command_processor,
history: Vec::new(),
current_voice: current_voice.clone(),
current_speed: 1.0,
current_pitch: 0.0,
current_volume: 1.0,
options,
term,
available_voices,
running: true,
};
if let Some(voice) = current_voice {
shell.synthesis_engine.set_voice(&voice).await?;
}
Ok(shell)
}
pub async fn run(&mut self) -> Result<()> {
self.print_welcome();
self.print_help_hint();
while self.running {
match self.read_command().await {
Ok(command) => {
if let Err(e) = self.process_command(&command).await {
self.print_error(&e);
}
}
Err(e) => {
if self.should_exit_on_error(&e) {
break;
}
self.print_error(&e);
}
}
}
self.print_goodbye();
Ok(())
}
async fn read_command(&mut self) -> Result<String> {
let prompt = self.create_prompt();
if !console::Term::stdout().is_term() {
use std::io::{self, BufRead};
let stdin = io::stdin();
let mut line = String::new();
match stdin.lock().read_line(&mut line) {
Ok(0) => {
self.running = false;
return Ok("quit".to_string());
}
Ok(_) => {
let input = line.trim().to_string();
if input == "quit" || input == "exit" {
self.running = false;
}
return Ok(input);
}
Err(e) => {
return Err(VoirsCliError::IoError(format!("Input error: {}", e)));
}
}
}
let input: String = Input::with_theme(&ColorfulTheme::default())
.with_prompt(&prompt)
.interact_text()
.map_err(|e| VoirsCliError::IoError(format!("Input error: {}", e)))?;
if !input.trim().is_empty() && self.history.last() != Some(&input) {
self.history.push(input.clone());
if self.history.len() > 1000 {
self.history.remove(0);
}
}
Ok(input)
}
async fn process_command(&mut self, command: &str) -> Result<()> {
let command = command.trim();
if command.is_empty() {
return Ok(());
}
if command.starts_with(':') {
return self.handle_shell_command(command).await;
}
self.synthesize_text(command).await?;
self.session_manager
.add_synthesis(command, &self.current_voice);
Ok(())
}
async fn handle_shell_command(&mut self, command: &str) -> Result<()> {
let available_voices = self.command_processor.available_voices().clone();
let temp_processor = CommandProcessor::new(available_voices);
temp_processor.process_command(self, command).await
}
async fn synthesize_text(&mut self, text: &str) -> Result<()> {
let start_time = std::time::Instant::now();
self.print_status(&format!("Synthesizing: \"{}\"", text));
let audio_data = self.synthesis_engine.synthesize(text).await?;
let synthesis_time = start_time.elapsed();
if !self.options.no_audio {
self.synthesis_engine.play_audio(&audio_data).await?;
}
self.print_status(&format!(
"✓ Synthesis completed in {:.2}s ({} samples)",
synthesis_time.as_secs_f64(),
audio_data.len()
));
Ok(())
}
fn create_prompt(&self) -> String {
let voice_part = if let Some(ref voice) = self.current_voice {
format!("{}@{}", style("voirs").cyan(), style(voice).green())
} else {
style("voirs").cyan().to_string()
};
let params =
if self.current_speed != 1.0 || self.current_pitch != 0.0 || self.current_volume != 1.0
{
format!(
" [s:{:.1} p:{:.1} v:{:.1}]",
self.current_speed, self.current_pitch, self.current_volume
)
} else {
String::new()
};
format!("{}{}> ", voice_part, style(¶ms).dim())
}
fn complete_input(&self, input: &str) -> Vec<String> {
let mut suggestions = Vec::new();
if input.starts_with(':') {
let commands = [
":help", ":voice", ":voices", ":speed", ":pitch", ":volume", ":save", ":load",
":history", ":clear", ":status", ":quit", ":exit",
];
for cmd in &commands {
if cmd.starts_with(input) {
suggestions.push(cmd.to_string());
}
}
} else if input.contains(" ") && input.starts_with(":voice ") {
let voice_prefix = input.strip_prefix(":voice ").unwrap_or("");
for voice in &self.available_voices {
if voice.starts_with(voice_prefix) {
suggestions.push(format!(":voice {}", voice));
}
}
}
suggestions
}
fn print_welcome(&self) {
println!(
"{}",
style("Welcome to VoiRS Interactive Mode").bold().cyan()
);
println!("Type text to synthesize, or use :help for commands");
if let Some(ref voice) = self.current_voice {
println!("Current voice: {}", style(voice).green());
}
println!();
}
fn print_help_hint(&self) {
println!(
"{}",
style("Hint: Type ':help' for available commands").dim()
);
println!();
}
fn print_goodbye(&self) {
println!("\\n{}", style("Goodbye! 👋").cyan());
}
fn print_status(&self, message: &str) {
println!("{} {}", style("ℹ").blue(), message);
}
fn print_error(&self, error: &VoirsCliError) {
eprintln!("{} {}", style("✗").red(), style(error).red());
}
fn should_exit_on_error(&self, _error: &VoirsCliError) -> bool {
false
}
pub fn current_voice(&self) -> Option<&str> {
self.current_voice.as_deref()
}
pub async fn set_voice(&mut self, voice: String) -> Result<()> {
self.synthesis_engine.set_voice(&voice).await?;
self.current_voice = Some(voice.clone());
self.session_manager.set_current_voice(voice);
Ok(())
}
pub fn available_voices(&self) -> &[String] {
&self.available_voices
}
pub fn current_params(&self) -> (f32, f32, f32) {
(self.current_speed, self.current_pitch, self.current_volume)
}
pub async fn set_params(
&mut self,
speed: Option<f32>,
pitch: Option<f32>,
volume: Option<f32>,
) -> Result<()> {
if let Some(s) = speed {
self.current_speed = s.clamp(0.1, 3.0);
self.synthesis_engine.set_speed(self.current_speed).await?;
}
if let Some(p) = pitch {
self.current_pitch = p.clamp(-12.0, 12.0);
self.synthesis_engine.set_pitch(self.current_pitch).await?;
}
if let Some(v) = volume {
self.current_volume = v.clamp(0.0, 2.0);
self.synthesis_engine
.set_volume(self.current_volume)
.await?;
}
Ok(())
}
pub fn session_manager(&mut self) -> &mut SessionManager {
&mut self.session_manager
}
pub fn exit(&mut self) {
self.running = false;
}
}