use std::io::{self, Stdout};
use std::path::PathBuf;
use std::sync::mpsc::{self, Receiver, Sender};
use std::thread;
use std::time::Duration;
use anyhow::Result;
use crossterm::{
execute,
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
};
use ratatui::{backend::CrosstermBackend, Terminal};
use crate::event::{apply_action, handle_event, poll_event, ActionResult};
use crate::formats::{parse_file_with_options, FileFormat};
use crate::genetic_code::GeneticCodes;
use crate::model::{Alignment, AppState, LoadingState, Sequence, SequenceType};
use crate::ui::{calculate_visible_dimensions, render};
pub enum LoadMessage {
Complete(Alignment),
Error { message: String, path: PathBuf },
Progress { sequences_loaded: usize },
}
pub enum TranslateMessage {
Progress { sequences_done: usize, total: usize },
Complete(Alignment),
}
pub struct App {
terminal: Terminal<CrosstermBackend<Stdout>>,
state: AppState,
tick_rate: Duration,
load_receiver: Option<Receiver<LoadMessage>>,
translate_receiver: Option<Receiver<TranslateMessage>>,
}
impl App {
pub fn new(state: AppState) -> Result<Self> {
enable_raw_mode()?;
let mut stdout = io::stdout();
execute!(stdout, EnterAlternateScreen)?;
let backend = CrosstermBackend::new(stdout);
let terminal = Terminal::new(backend)?;
Ok(Self {
terminal,
state,
tick_rate: Duration::from_millis(50),
load_receiver: None,
translate_receiver: None,
})
}
pub fn new_with_background_load(
file_path: PathBuf,
forced_format: Option<FileFormat>,
preset_translation: Option<(u8, u8)>,
fancy_ui: bool,
) -> Result<Self> {
let file_name = file_path
.file_stem()
.and_then(|s| s.to_str())
.unwrap_or("alignment")
.to_string();
let mut state = AppState::new_loading(file_name, file_path.clone());
state.fancy_ui = fancy_ui;
if let Some((genetic_code, reading_frame)) = preset_translation {
let codes = GeneticCodes::new();
let code_index = codes.all().iter().position(|c| c.id == genetic_code).unwrap_or(0);
state.translation_settings.genetic_code_id = genetic_code;
state.translation_settings.selected_code_index = code_index;
let frame = (reading_frame.saturating_sub(1) as usize).min(2);
state.translation_settings.frame = frame;
state.translation_settings.selected_frame = frame;
state.translation_settings.has_translated = true; }
enable_raw_mode()?;
let mut stdout = io::stdout();
execute!(stdout, EnterAlternateScreen)?;
let backend = CrosstermBackend::new(stdout);
let terminal = Terminal::new(backend)?;
let (tx, rx): (Sender<LoadMessage>, Receiver<LoadMessage>) = mpsc::channel();
let path_for_error = file_path.clone();
thread::spawn(move || {
match parse_file_with_options(&file_path, forced_format) {
Ok(alignment) => {
let _ = tx.send(LoadMessage::Complete(alignment));
}
Err(e) => {
let _ = tx.send(LoadMessage::Error {
message: e.to_string(),
path: path_for_error,
});
}
}
});
Ok(Self {
terminal,
state,
tick_rate: Duration::from_millis(50),
load_receiver: Some(rx),
translate_receiver: None,
})
}
fn start_background_translation(&mut self) {
let genetic_code_id = self.state.translation_settings.genetic_code_id;
let frame = self.state.translation_settings.frame;
let codes = GeneticCodes::new();
let code = codes.get(genetic_code_id)
.unwrap_or_else(|| codes.default_code())
.clone();
let sequences: Vec<(String, Vec<u8>)> = self.state.alignment.sequences
.iter()
.map(|seq| (seq.id.clone(), seq.clone_bytes()))
.collect();
let total = sequences.len();
self.state.loading_state = LoadingState::Translating {
message: "Translating...".to_string(),
sequences_done: 0,
total,
};
let (tx, rx): (Sender<TranslateMessage>, Receiver<TranslateMessage>) = mpsc::channel();
self.translate_receiver = Some(rx);
thread::spawn(move || {
let mut translated_seqs = Vec::with_capacity(total);
let progress_interval = (total / 20).max(1);
for (i, (id, data)) in sequences.into_iter().enumerate() {
let aa_data = code.translate_sequence(&data, frame);
translated_seqs.push(Sequence::from_bytes(id, aa_data));
if (i + 1) % progress_interval == 0 || i + 1 == total {
let _ = tx.send(TranslateMessage::Progress {
sequences_done: i + 1,
total,
});
}
}
translated_seqs.shrink_to_fit();
let mut alignment = Alignment::new(translated_seqs);
alignment.sequence_type = SequenceType::AMINO_ACID;
let _ = tx.send(TranslateMessage::Complete(alignment));
});
}
pub fn run(&mut self) -> Result<()> {
self.update_viewport_size()?;
loop {
if let Some(ref rx) = self.load_receiver {
match rx.try_recv() {
Ok(LoadMessage::Complete(alignment)) => {
self.state.set_alignment(alignment);
self.load_receiver = None; }
Ok(LoadMessage::Error { message, path }) => {
self.state.set_loading_error(message, Some(path));
self.load_receiver = None;
}
Ok(LoadMessage::Progress { sequences_loaded }) => {
if let LoadingState::LoadingFile { sequences_loaded: ref mut sl, .. } = self.state.loading_state {
*sl = Some(sequences_loaded);
}
}
Err(mpsc::TryRecvError::Empty) => {
}
Err(mpsc::TryRecvError::Disconnected) => {
self.state.set_loading_error("Loading thread terminated unexpectedly".to_string(), None);
self.load_receiver = None;
}
}
}
if let Some(ref rx) = self.translate_receiver {
match rx.try_recv() {
Ok(TranslateMessage::Progress { sequences_done, total }) => {
if let LoadingState::Translating { sequences_done: ref mut sd, total: ref mut t, .. } = self.state.loading_state {
*sd = sequences_done;
*t = total;
}
}
Ok(TranslateMessage::Complete(alignment)) => {
self.state.set_translated_alignment(alignment);
self.translate_receiver = None;
}
Err(mpsc::TryRecvError::Empty) => {
}
Err(mpsc::TryRecvError::Disconnected) => {
self.state.loading_state = LoadingState::Ready;
self.state.status_message = Some("Translation failed unexpectedly".to_string());
self.translate_receiver = None;
}
}
}
if self.state.loading_state.is_loading() {
self.state.tick_spinner();
}
self.terminal.draw(|frame| {
render(frame, &self.state);
})?;
if let Some(event) = poll_event(self.tick_rate) {
let has_number_prefix = !self.state.number_buffer.is_empty();
let has_error_popup = self.state.error_popup.is_some();
let has_file_browser = self.state.file_browser.is_some();
let action = handle_event(
event,
&self.state.mode,
self.state.show_help,
self.state.pending_g,
self.state.pending_z,
has_number_prefix,
has_error_popup,
has_file_browser,
);
if let crate::event::Action::Resize(_, _) = action {
self.update_viewport_size()?;
}
let result = apply_action(&mut self.state, action);
match result {
ActionResult::StartTranslation => {
self.start_background_translation();
}
ActionResult::LoadFile(path) => {
self.start_background_load(path);
}
ActionResult::Continue => {}
}
if self.state.should_quit {
break;
}
}
}
Ok(())
}
fn start_background_load(&mut self, file_path: PathBuf) {
self.state.close_file_browser();
let file_name = file_path.file_name()
.and_then(|n| n.to_str())
.unwrap_or("Unknown")
.to_string();
self.state.loading_state = LoadingState::LoadingFile {
path: file_path.clone(),
message: format!("Loading {}...", file_name),
sequences_loaded: None,
};
self.state.status_message = None;
let (tx, rx): (Sender<LoadMessage>, Receiver<LoadMessage>) = mpsc::channel();
let path_for_error = file_path.clone();
thread::spawn(move || {
match parse_file_with_options(&file_path, None) {
Ok(alignment) => {
let _ = tx.send(LoadMessage::Complete(alignment));
}
Err(e) => {
let _ = tx.send(LoadMessage::Error {
message: e.to_string(),
path: path_for_error,
});
}
}
});
self.load_receiver = Some(rx);
}
fn update_viewport_size(&mut self) -> Result<()> {
let size = self.terminal.size()?;
let (visible_rows, visible_cols) = calculate_visible_dimensions(size.width, size.height);
self.state.update_viewport_size(visible_rows, visible_cols);
Ok(())
}
}
impl Drop for App {
fn drop(&mut self) {
let _ = disable_raw_mode();
let _ = execute!(self.terminal.backend_mut(), LeaveAlternateScreen);
let _ = self.terminal.show_cursor();
}
}
pub fn run_app(state: AppState) -> Result<()> {
let mut app = App::new(state)?;
app.run()
}
pub fn run_app_with_file_browser(fancy_ui: bool) -> Result<()> {
use crate::model::{Alignment, FileBrowserState};
let start_dir = std::env::current_dir().unwrap_or_else(|_| std::path::PathBuf::from("."));
let mut state = AppState::new(Alignment::new(vec![]), "No file".to_string());
state.fancy_ui = fancy_ui;
state.file_browser = Some(FileBrowserState::new(start_dir, "Select a sequence file".to_string()));
let mut app = App::new(state)?;
app.run()
}
pub fn run_app_with_file_browser_at(start_dir: PathBuf, fancy_ui: bool) -> Result<()> {
use crate::model::{Alignment, FileBrowserState};
let start_dir = if start_dir.is_dir() {
start_dir
} else {
std::env::current_dir().unwrap_or_else(|_| std::path::PathBuf::from("."))
};
let mut state = AppState::new(Alignment::new(vec![]), "No file".to_string());
state.fancy_ui = fancy_ui;
state.file_browser = Some(FileBrowserState::new(start_dir, "Select a sequence file".to_string()));
let mut app = App::new(state)?;
app.run()
}
pub fn run_app_with_loading(
file_path: PathBuf,
forced_format: Option<FileFormat>,
preset_translation: Option<(u8, u8)>,
fancy_ui: bool,
) -> Result<()> {
let mut app = App::new_with_background_load(file_path, forced_format, preset_translation, fancy_ui)?;
app.run()
}
#[cfg(test)]
mod tests {
use crate::model::{Alignment, Sequence};
use super::*;
#[test]
fn test_app_state_creation() {
let seqs = vec![
Sequence::new("seq1", "ACGT"),
Sequence::new("seq2", "TGCA"),
];
let alignment = Alignment::new(seqs);
let state = AppState::new(alignment, "test".to_string());
assert_eq!(state.alignment.sequence_count(), 2);
assert!(!state.should_quit);
}
}