168 lines
5.1 KiB
Rust
168 lines
5.1 KiB
Rust
|
|
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<CrosstermBackend<std::io::Stdout>>,
|
||
|
|
) -> io::Result<usize> {
|
||
|
|
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::<Vec<_>>(),
|
||
|
|
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);
|
||
|
|
}
|