diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..6e6de87 --- /dev/null +++ b/src/main.rs @@ -0,0 +1,35 @@ +mod alphabet; +mod app; +mod ciphertext; +mod cli; +mod decoder; +mod storage; +mod tui; + +use clap::Parser; +use cli::{Cli, Commands}; + +fn main() -> std::io::Result<()> { + let cli = Cli::parse(); + + match cli.command { + Commands::Generate { + path, + chars, + chars_file, + len, + mut_rate, + } => app::generate_cmd(path, chars, chars_file, len, mut_rate), + Commands::Cipher { + alphabet, + file, + output, + } => app::cipher_cmd(alphabet, file, output), + Commands::Decipher { + alphabet, + file, + output, + } => app::decipher_cmd(alphabet, file, output), + Commands::Tui { alphabet, file } => tui::run_decoder_tui(alphabet, file), + } +} diff --git a/src/storage.rs b/src/storage.rs new file mode 100644 index 0000000..f4dc608 --- /dev/null +++ b/src/storage.rs @@ -0,0 +1,32 @@ +use crate::alphabet::Alphabet; +use rand::{CryptoRng, Rng}; +use std::fs::File; +use std::io::{Error, ErrorKind, Read, Result, Write}; +use std::path::PathBuf; + +pub struct AlphabetFile { + file: File, + pub dict: Alphabet, +} + +impl AlphabetFile { + pub fn create(path: PathBuf, dict: Alphabet) -> Result { + let file = File::create(path)?; + Ok(Self { file, dict }) + } + + pub fn save(mut self) -> Result<()> { + self.file.write_all(self.dict.into_bytes().as_slice()) + } + + pub fn open(name: PathBuf, rng: R) -> Result { + let mut file = File::open(&name)?; + let mut bytes = Vec::new(); + file.read_to_end(&mut bytes)?; + + let dict = + Alphabet::from_bytes(rng, &bytes).map_err(|e| Error::new(ErrorKind::InvalidData, e))?; + + Ok(Self { file, dict }) + } +} diff --git a/src/tui.rs b/src/tui.rs new file mode 100644 index 0000000..cb6aeaf --- /dev/null +++ b/src/tui.rs @@ -0,0 +1,168 @@ +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); +}