use crate::ciphertext::Ciphertext; use crate::decoder::DecoderState; use crate::storage::AlphabetFile; use crossterm::{ event::{self, Event, KeyCode, KeyEventKind}, execute, terminal::{EnterAlternateScreen, LeaveAlternateScreen, disable_raw_mode, enable_raw_mode}, }; use rand_chacha::ChaCha20Rng; use ratatui::{ prelude::*, widgets::{Block, Borders, List, ListItem, Paragraph}, }; use std::io; use std::path::PathBuf; use std::time::Duration; struct TuiGuard; impl Drop for TuiGuard { fn drop(&mut self) { let _ = disable_raw_mode(); let _ = execute!(io::stdout(), LeaveAlternateScreen); } } fn get_visible_symbols( terminal: &Terminal>, ) -> io::Result { let size = terminal.size()?; let inner_width = size.width.saturating_sub(2) as usize; Ok(inner_width.max(1)) } pub fn run() -> io::Result<()> { todo!("call run_decoder_tui(alphabet_path, ciphertext_path) from CLI later") } pub fn run_decoder_tui(alphabet_path: PathBuf, ciphertext_path: PathBuf) -> io::Result<()> { let alphabet_bytes = std::fs::read(&alphabet_path)?; let bytes = std::fs::read(ciphertext_path)?; let ciphertext = Ciphertext::from_bytes(&bytes).map_err(io::Error::other)?; let rng_seed = [7u8; 32]; let mut state = DecoderState::new(alphabet_bytes, rng_seed, ciphertext.bits)?; enable_raw_mode()?; execute!(io::stdout(), EnterAlternateScreen)?; let _guard = TuiGuard; let mut terminal = Terminal::new(CrosstermBackend::new(io::stdout()))?; loop { terminal.draw(|f| draw_ui(f, &state))?; if event::poll(Duration::from_millis(100))? { if let Event::Key(key) = event::read()? { if key.kind != KeyEventKind::Press { continue; } let visible = get_visible_symbols(&terminal)?; match key.code { KeyCode::Char('q') => break, KeyCode::Left | KeyCode::Char('h') => state.move_left(visible), KeyCode::Right | KeyCode::Char('l') => state.move_right(visible), KeyCode::Char('u') => { let _ = state.undo_current(); } KeyCode::Char('1') => { let _ = state.apply_candidate(0); } KeyCode::Char('2') => { let _ = state.apply_candidate(1); } KeyCode::Char('3') => { let _ = state.apply_candidate(2); } KeyCode::Char('4') => { let _ = state.apply_candidate(3); } KeyCode::Char('5') => { let _ = state.apply_candidate(4); } KeyCode::Char('m') => { state.set_ranking_mode(crate::decoder::RankingMode::LocalFirst); } KeyCode::Char('M') => { state.set_ranking_mode(crate::decoder::RankingMode::FutureFirst); } _ => {} } } } } Ok(()) } fn draw_ui(f: &mut Frame, state: &DecoderState) { let layout = Layout::vertical([ Constraint::Length(5), Constraint::Min(10), Constraint::Length(3), ]) .split(f.area()); draw_text_panel(f, layout[0], state); draw_candidates_panel(f, layout[1], state); draw_help_panel(f, layout[2]); } fn draw_text_panel(f: &mut Frame, area: Rect, state: &DecoderState) { let cursor = state.cursor; let rendered: String = state .rendered_text .chars() .enumerate() .flat_map(|(i, ch)| { if i == cursor { vec!['[', ch, ']'] } else { vec![ch] } }) .collect(); let p = Paragraph::new(rendered) .block(Block::default().title("Decoded text").borders(Borders::ALL)); f.render_widget(p, area); } fn draw_candidates_panel(f: &mut Frame, area: Rect, state: &DecoderState) { let items = match state.current_candidates() { Ok(cands) => cands .into_iter() .enumerate() .map(|(i, c)| { ListItem::new(format!( "{}. {:?} local={} future={}", i + 1, c.symbol, c.distance, c.future_distance )) }) .collect::>(), Err(err) => vec![ListItem::new(format!("error: {}", err))], }; let list = List::new(items).block( Block::default() .title("Top-5 candidates") .borders(Borders::ALL), ); f.render_widget(list, area); } fn draw_help_panel(f: &mut Frame, area: Rect) { let p = Paragraph::new("h/← left l/→ right 1..5 choose u undo q quit") .block(Block::default().title("Keys").borders(Borders::ALL)); f.render_widget(p, area); }