From 1b7074c8d052ea1abd7c7ad0c46130c03ade89be Mon Sep 17 00:00:00 2001 From: Scott Munro Date: Wed, 27 Jan 2021 00:32:03 -0800 Subject: [PATCH] Add a cursor and word selection to the hex dump TUI (#7) * lots of refactoring to cleanup game.rs (more incoming) * added new implementation (and tests) for moving the cursor around the hex dump panes and fitting to words --- Cargo.lock | 7 + Cargo.toml | 1 + src/game.rs | 817 +++++++++++++++++++++++++++++++++++++++++++++------- src/main.rs | 2 + 4 files changed, 727 insertions(+), 100 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index d0e7d6b..6dcdebc 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -25,6 +25,7 @@ dependencies = [ "pancurses", "rand", "snm_simple_file", + "static_assertions", "text_io", ] @@ -156,6 +157,12 @@ name = "snm_simple_file" version = "0.1.0" source = "git+https://github.com/scottnm/snm_simple_file?branch=main#b9730f7cc37309cb41e089c29005adb57bb80151" +[[package]] +name = "static_assertions" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" + [[package]] name = "text_io" version = "0.1.8" diff --git a/Cargo.toml b/Cargo.toml index b9e056d..f1a5be1 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -11,3 +11,4 @@ snm_simple_file = { git = "https://github.com/scottnm/snm_simple_file", branch = text_io = "0.1.8" pancurses = "0.16.1" rand = { version = "0.7.0", features = ["small_rng"] } +static_assertions = "1.1.0" diff --git a/src/game.rs b/src/game.rs index da3fb22..c75007a 100644 --- a/src/game.rs +++ b/src/game.rs @@ -1,10 +1,5 @@ // Work breakdown -// - constrain the number of words generated to only as much as would fit in two panes // - setup a better word selection algorithm which results in more common letters -// - place the words throughout the pane w/ filler text that goes in between words -// - add support for selecting between the words in the TUI and highlighting the current selection -// - mouse support? -// - keyboard support? // - add support for using that selection instead of text input to power the gameloop // - add an output pane which tells you the results of your current selection // - refactor out tui utils into its own module @@ -16,7 +11,6 @@ use crate::dict; use crate::randwrapper::{select_rand, RangeRng, ThreadRangeRng}; use crate::utils::Rect; -use std::str::FromStr; const TITLE: &str = "FONV: Terminal Cracker"; @@ -29,6 +23,50 @@ pub enum Difficulty { VeryHard, } +impl std::str::FromStr for Difficulty { + type Err = &'static str; + + fn from_str(s: &str) -> Result { + if s.eq_ignore_ascii_case("VeryEasy") || s.eq_ignore_ascii_case("VE") { + return Ok(Difficulty::VeryEasy); + } + + if s.eq_ignore_ascii_case("Easy") || s.eq_ignore_ascii_case("E") { + return Ok(Difficulty::Easy); + } + + if s.eq_ignore_ascii_case("Average") || s.eq_ignore_ascii_case("A") { + return Ok(Difficulty::Average); + } + + if s.eq_ignore_ascii_case("Hard") || s.eq_ignore_ascii_case("H") { + return Ok(Difficulty::Hard); + } + + if s.eq_ignore_ascii_case("VeryHard") || s.eq_ignore_ascii_case("VH") { + return Ok(Difficulty::VeryHard); + } + + Err("Invalid difficulty string") + } +} + +enum Movement { + Left, + Right, + Up, + Down, +} + +//#[derive(PartialEq, Eq)] +enum InputCmd { + Move(Movement), + _Select, + Quit, +} + +// TODO: should this be split out into two structs? +// one for dump dimensions and another for formatting? (i.e. the padding param) struct HexDumpPane { dump_width: i32, dump_height: i32, @@ -62,32 +100,13 @@ impl HexDumpPane { } } -impl FromStr for Difficulty { - type Err = &'static str; - - fn from_str(s: &str) -> Result { - if s.eq_ignore_ascii_case("VeryEasy") || s.eq_ignore_ascii_case("VE") { - return Ok(Difficulty::VeryEasy); - } - - if s.eq_ignore_ascii_case("Easy") || s.eq_ignore_ascii_case("E") { - return Ok(Difficulty::Easy); - } - - if s.eq_ignore_ascii_case("Average") || s.eq_ignore_ascii_case("A") { - return Ok(Difficulty::Average); - } - - if s.eq_ignore_ascii_case("Hard") || s.eq_ignore_ascii_case("H") { - return Ok(Difficulty::Hard); - } - - if s.eq_ignore_ascii_case("VeryHard") || s.eq_ignore_ascii_case("VH") { - return Ok(Difficulty::VeryHard); - } - - Err("Invalid difficulty string") - } +// TODO: this chunk selection logic is pretty ugly. Can it be refactored for readability? +#[derive(Debug, PartialEq, Eq)] +struct SelectedChunk { + pane_num: usize, + row_num: usize, + col_start: usize, + len: usize, } fn generate_words(difficulty: Difficulty, rng: &mut dyn RangeRng) -> Vec { @@ -107,34 +126,145 @@ fn generate_words(difficulty: Difficulty, rng: &mut dyn RangeRng) -> Vec< .collect() } +fn move_selection( + selection: SelectedChunk, + movement: Movement, + hex_dump_pane_dimensions: &HexDumpPane, + num_panes: usize, +) -> SelectedChunk { + let (col_move, row_move): (i32, i32) = match movement { + Movement::Down => (0, 1), + Movement::Up => (0, -1), + Movement::Left => (-1, 0), + // We might be at the beginning of a full word, in which case move past the end of the word + Movement::Right => (selection.len as i32, 0), + }; + + // Naively update the row and col with our movement + let mut next_col = col_move + (selection.col_start as i32); + let mut next_row = row_move + (selection.row_num as i32); + let mut next_pane = selection.pane_num as i32; + + // Check if we've moved from one pane to another by moving laterally across the columns in a row. + if next_col >= hex_dump_pane_dimensions.width() { + next_col = 0; + next_pane += 1; + } else if next_col < 0 { + next_col = hex_dump_pane_dimensions.width() - 1; + next_pane -= 1; + } + + // Check if we've moved to an invalid row outside of our pane. + // In which case just wrap around to the next valid row in the same pane. + if next_row >= hex_dump_pane_dimensions.height() { + next_row = 0; + } else if next_row < 0 { + next_row = hex_dump_pane_dimensions.height() - 1; + } + + // Check if we've moved to an invalid pane outside of our hex dump. + // In which case just wrap around to the next valid pane. + if next_pane >= num_panes as i32 { + next_pane = 0; + } else if next_pane < 0 { + next_pane = 1; + } + + SelectedChunk { + pane_num: next_pane as usize, + row_num: next_row as usize, + col_start: next_col as usize, + len: 1, + } +} + +fn refit_selection>( + selection: SelectedChunk, + words: &[S], + word_offsets: &[usize], + hex_dump_pane_dimensions: &HexDumpPane, +) -> SelectedChunk { + let cursor_index = selection.pane_num * hex_dump_pane_dimensions.max_bytes_in_pane() + + selection.row_num * hex_dump_pane_dimensions.width() as usize + + selection.col_start; + + // turn our list of words and word_offsets into a list of ranges where those words live + // in the contiguous hex dump memory span + let word_ranges = words + .iter() + .zip(word_offsets.iter()) + .map(|(word, word_offset)| (*word_offset, word_offset + word.as_ref().len())); + + // TODO: clean up this implementation. It's a bit ugly + let mut result_selection = selection; + + for word_range in word_ranges { + if cursor_index >= word_range.0 && cursor_index < word_range.1 { + // if our cursor is on or in the middle of a full word, update the cursor selection + // to highlight the whole word + let cursor_offset = cursor_index - word_range.0; + if cursor_offset <= result_selection.col_start { + result_selection.col_start -= cursor_offset; + } else { + // account for the word starting on the previous row + // account for the previous row, being in the previous pane + if result_selection.row_num > 0 { + result_selection.row_num -= 1; + } else { + result_selection.row_num = (hex_dump_pane_dimensions.height() - 1) as usize; + result_selection.pane_num -= 1; + } + result_selection.col_start += + hex_dump_pane_dimensions.width() as usize - cursor_offset; + } + result_selection.len = word_range.1 - word_range.0; + break; + } + } + + result_selection +} + +// TODO: this chunk render function is pretty nasty. can it be refactored better readability +// one idea is to split out the rendering of memory addresses from rendering out the actual hex dumps +// this could help greatly clean up some offset calculations... fn render_hexdump_pane( window: &pancurses::Window, hex_dump_dimensions: &HexDumpPane, render_rect: &Rect, - mem_start: usize, + hex_dump_first_byte: usize, bytes: &str, + pane_offset: usize, + (highlighted_byte_start, highlighted_byte_end): (usize, usize), ) { for row in 0..hex_dump_dimensions.height() { - let byte_offset = (row * hex_dump_dimensions.width()) as usize; + let row_first_byte = pane_offset + (row * hex_dump_dimensions.width()) as usize; let mem_addr = format!( "0x{:0width$X}", - mem_start + byte_offset, + hex_dump_first_byte + row_first_byte, width = hex_dump_dimensions.addr_width() - 2, ); - let row_bytes = &bytes[byte_offset..][..hex_dump_dimensions.width() as usize]; - let y = row + render_rect.top; // render the memaddr window.mvaddstr(y, render_rect.left, &mem_addr); - // render the dump - window.mvaddstr( - y, - render_rect.left + mem_addr.len() as i32 + hex_dump_dimensions.padding(), - row_bytes, - ); + let begin_dump_offset = + render_rect.left + mem_addr.len() as i32 + hex_dump_dimensions.padding(); + let byte_at_cols = bytes[row_first_byte..] + .chars() + .zip(0..hex_dump_dimensions.width()); + for (byte, col_index) in byte_at_cols { + let byte_offset = row_first_byte + col_index as usize; + if byte_offset >= highlighted_byte_start && byte_offset < highlighted_byte_end { + window.attron(pancurses::A_BLINK); + } else { + window.attroff(pancurses::A_BLINK); + } + window.mvaddch(y, begin_dump_offset + col_index, byte); + } + window.attroff(pancurses::A_BLINK); } } @@ -187,37 +317,63 @@ fn obfuscate_words( (string_builder, offsets) } +fn render_game_window( + window: &pancurses::Window, + cursor_selection: &SelectedChunk, + hex_dump_start_addr: usize, + hex_dump: &str, + hex_dump_dimensions: &HexDumpPane, + hex_dump_rects: &[Rect], +) { + // Render the hex dump header + window.mvaddstr(0, 0, "ROBCO INDUSTRIES (TM) TERMALINK PROTOCOL"); + window.mvaddstr(1, 0, "ENTER PASSWORD NOW"); + const BLOCK_CHAR: char = '#'; + window.mvaddstr( + 3, + 0, + format!( + "# ATTEMPT(S) LEFT: {} {} {} {}", + BLOCK_CHAR, BLOCK_CHAR, BLOCK_CHAR, BLOCK_CHAR + ), + ); + + let highlighted_byte_range = { + let start = cursor_selection.pane_num * hex_dump_dimensions.max_bytes_in_pane() + + cursor_selection.row_num * hex_dump_dimensions.width() as usize + + cursor_selection.col_start; + let end = start + cursor_selection.len; + (start, end) + }; + + // render each hex dump pane (assume ordered left to right) + for hex_dump_pane_index in 0..hex_dump_rects.len() { + let hex_dump_rect = &hex_dump_rects[hex_dump_pane_index]; + let pane_byte_offset = hex_dump_pane_index * hex_dump_dimensions.max_bytes_in_pane(); + render_hexdump_pane( + &window, + hex_dump_dimensions, + &hex_dump_rect, + hex_dump_start_addr + pane_byte_offset, + &hex_dump, + pane_byte_offset, + highlighted_byte_range, + ); + } +} + pub fn run_game(difficulty: Difficulty) { const HEX_DUMP_PANE: HexDumpPane = HexDumpPane { - dump_width: 12, // 12 characters per row of the hexdump - dump_height: 16, // 16 rows of hex dump per dump pane + dump_width: 12, // 12 characters per row of the hexdump + dump_height: 16, // 16 rows of hex dump per dump pane + // TODO: update this to not be characters but bytes or bits or something addr_width: "0x1234".len() as i32, // 2 byte memaddr addr_to_dump_padding: 4, // horizontal padding between panes in the memdump window }; const HEXDUMP_PANE_VERT_OFFSET: i32 = 5; - // Generate a random set of words based on the difficulty - let mut rng = ThreadRangeRng::new(); - let rand_words = generate_words(difficulty, &mut rng); - let (obfuscated_words, _word_offsets) = - obfuscate_words(&rand_words, HEX_DUMP_PANE.max_bytes_in_pane() * 2, &mut rng); - - // For the sake of keeping the windowing around let's dump those words in a window - { - // setup the window - let window = pancurses::initscr(); - pancurses::noecho(); // prevent key inputs rendering to the screen - pancurses::cbreak(); - pancurses::curs_set(0); - pancurses::set_title(TITLE); - window.nodelay(true); // don't block waiting for key inputs (we'll poll) - window.keypad(true); // let special keys be captured by the program (i.e. esc/backspace/del/arrow keys) - - // TODO just open a stub window for now. We'll write the game soon. - window.clear(); - - // are all of these constants only used by this struct? + let hex_dump_rects = { let left_hex_dump_rect = Rect { left: 0, top: HEXDUMP_PANE_VERT_OFFSET, @@ -232,49 +388,96 @@ pub fn run_game(difficulty: Difficulty) { height: HEX_DUMP_PANE.height(), }; - window.mvaddstr(0, 0, "ROBCO INDUSTRIES (TM) TERMALINK PROTOCOL"); - window.mvaddstr(1, 0, "ENTER PASSWORD NOW"); - const BLOCK_CHAR: char = '#'; - window.mvaddstr( - 3, - 0, - format!( - "# ATTEMPT(S) LEFT: {} {} {} {}", - BLOCK_CHAR, BLOCK_CHAR, BLOCK_CHAR, BLOCK_CHAR - ), - ); + [left_hex_dump_rect, right_hex_dump_rect] + }; - let hexdump_first_addr = rng.gen_range(0xCC00, 0xFFFF); - render_hexdump_pane( - &window, - &HEX_DUMP_PANE, - &left_hex_dump_rect, - hexdump_first_addr, - &obfuscated_words, - ); + // Generate a random set of words based on the provided difficulty setting + let mut rng = ThreadRangeRng::new(); + let words = generate_words(difficulty, &mut rng); + + // Generate a mock hexdump from the randomly generated words + const MAX_BYTES_IN_DUMP: usize = HEX_DUMP_PANE.max_bytes_in_pane() * 2; // 2 dump panes + let (hex_dump, word_offsets) = obfuscate_words(&words, MAX_BYTES_IN_DUMP, &mut rng); + + // For visual flair, randomize the mem address of the hex dump + const MIN_MEMADDR: usize = 0xCC00; + const MAX_MEMADDR: usize = 0xFFFF - MAX_BYTES_IN_DUMP; + const_assert!(MIN_MEMADDR < MAX_MEMADDR); + let hex_dump_start_addr = rng.gen_range(MIN_MEMADDR, MAX_MEMADDR); + + // initially select the first character in the row pane + let mut selected_chunk = SelectedChunk { + pane_num: 0, + row_num: 0, + col_start: 0, + len: 1, + }; - render_hexdump_pane( - &window, - &HEX_DUMP_PANE, - &right_hex_dump_rect, - hexdump_first_addr + HEX_DUMP_PANE.max_bytes_in_pane(), - &obfuscated_words[HEX_DUMP_PANE.max_bytes_in_pane()..], - ); + // Immediately refit the selection in case the first character is part of a larger word + selected_chunk = refit_selection(selected_chunk, &words, &word_offsets, &HEX_DUMP_PANE); + + // setup the window + let window = pancurses::initscr(); + pancurses::noecho(); // prevent key inputs rendering to the screen + pancurses::cbreak(); + pancurses::curs_set(0); + pancurses::set_title(TITLE); + window.nodelay(true); // don't block waiting for key inputs (we'll poll) + window.keypad(true); // let special keys be captured by the program (i.e. esc/backspace/del/arrow keys) + + // TODO: refactor this loop for readability and testing + loop { + // Poll for input + let polled_input_cmd = match window.getch() { + Some(pancurses::Input::Character('w')) => Some(InputCmd::Move(Movement::Up)), + Some(pancurses::Input::Character('s')) => Some(InputCmd::Move(Movement::Down)), + Some(pancurses::Input::Character('a')) => Some(InputCmd::Move(Movement::Left)), + Some(pancurses::Input::Character('d')) => Some(InputCmd::Move(Movement::Right)), + Some(pancurses::Input::Character('q')) => Some(InputCmd::Quit), + // TODO: handle entering in guesses... Some(pancurses::Input::Character('ENTER')) => (), + _ => None, + }; - /* TODO: render the words in the memdump - window.mvaddstr(0, 0, format!("{:?}", difficulty)); - for (i, rand_word) in rand_words.iter().enumerate() { - window.mvaddstr(i as i32 + 1, 0, rand_word); + // Handle the input + if let Some(input_cmd) = polled_input_cmd { + match input_cmd { + // Handle moving the cursor around the hex dump pane + InputCmd::Move(movement) => { + // Move the cursor based on our input + selected_chunk = move_selection(selected_chunk, movement, &HEX_DUMP_PANE, 2); + // If the cursor is now selecting a word, refit the selection highlight for the whole word + selected_chunk = + refit_selection(selected_chunk, &words, &word_offsets, &HEX_DUMP_PANE); + } + + // Handle selecting a word + InputCmd::_Select => unimplemented!(), + + // Handle quitting the game early + InputCmd::Quit => break, + } } - */ + // Render the next frame + window.clear(); + render_game_window( + &window, + &selected_chunk, + hex_dump_start_addr, + &hex_dump, + &HEX_DUMP_PANE, + &hex_dump_rects, + ); window.refresh(); - std::thread::sleep(std::time::Duration::from_millis(5000)); - pancurses::endwin(); + + // No need to waste cycles doing nothing but rendering over and over. + // Yield the processor until the next frame. + std::thread::sleep(std::time::Duration::from_millis(33)); } + pancurses::endwin(); // now let's run a mock game_loop - // run_game_from_line_console(&rand_words, &mut rng); + // run_game_from_line_console(&words, &mut rng); } fn _run_game_from_line_console(words: &[String], rng: &mut dyn RangeRng) { @@ -382,4 +585,418 @@ mod tests { assert_eq!(word, word_in_blob); } } + + fn move_and_refit( + mut selection: SelectedChunk, + movement: Movement, + words: &[&str], + word_offsets: &[usize], + hex_dump_pane_dimensions: &HexDumpPane, + num_panes: usize, + ) -> SelectedChunk { + selection = move_selection(selection, movement, &hex_dump_pane_dimensions, num_panes); + refit_selection(selection, &words, &word_offsets, &hex_dump_pane_dimensions) + } + + #[test] + fn test_single_char_move_next() { + // .... .... + // .abc .xyz + // .... .... + // ^^ + let start_selection = SelectedChunk { + pane_num: 0, + row_num: 2, + col_start: 1, + len: 1, + }; + let expected_end_selection = SelectedChunk { + pane_num: 0, + row_num: 2, + col_start: 2, + len: 1, + }; + let movement = Movement::Right; + let words = ["abc", "xyz"]; + let word_offsets = [5, 17]; + let hex_dump_pane_dimensions = HexDumpPane { + dump_width: 4, + dump_height: 3, + addr_width: 0, // unused + addr_to_dump_padding: 0, // unused + }; + + let end_selection = move_and_refit( + start_selection, + movement, + &words, + &word_offsets, + &hex_dump_pane_dimensions, + 2, + ); + + assert_eq!(end_selection, expected_end_selection); + } + + #[test] + fn test_single_char_move_across_panes_right() { + // .... .... + // .abc .xyz + // .... .... + // ^ ^ + let start_selection = SelectedChunk { + pane_num: 0, + row_num: 2, + col_start: 3, + len: 1, + }; + let expected_end_selection = SelectedChunk { + pane_num: 1, + row_num: 2, + col_start: 0, + len: 1, + }; + let movement = Movement::Right; + let words = ["abc", "xyz"]; + let word_offsets = [5, 17]; + let hex_dump_pane_dimensions = HexDumpPane { + dump_width: 4, + dump_height: 3, + addr_width: 0, // unused + addr_to_dump_padding: 0, // unused + }; + + let end_selection = move_and_refit( + start_selection, + movement, + &words, + &word_offsets, + &hex_dump_pane_dimensions, + 2, + ); + + assert_eq!(end_selection, expected_end_selection); + } + + #[test] + fn test_single_char_move_across_panes_left() { + // .... .... + // .abc .xyz + // .... .... + // ^ ^ + let start_selection = SelectedChunk { + pane_num: 0, + row_num: 2, + col_start: 0, + len: 1, + }; + let expected_end_selection = SelectedChunk { + pane_num: 1, + row_num: 2, + col_start: 3, + len: 1, + }; + let movement = Movement::Left; + let words = ["abc", "xyz"]; + let word_offsets = [5, 17]; + let hex_dump_pane_dimensions = HexDumpPane { + dump_width: 4, + dump_height: 3, + addr_width: 0, // unused + addr_to_dump_padding: 0, // unused + }; + + let end_selection = move_and_refit( + start_selection, + movement, + &words, + &word_offsets, + &hex_dump_pane_dimensions, + 2, + ); + + assert_eq!(end_selection, expected_end_selection); + } + + #[test] + fn test_word_move_wrap_vertical() { + // v-start + // .... .... + // .abc .xyz + // .... .... + // ^-end + let start_selection = SelectedChunk { + pane_num: 1, + row_num: 0, + col_start: 2, + len: 1, + }; + let expected_end_selection = SelectedChunk { + pane_num: 1, + row_num: 2, + col_start: 2, + len: 1, + }; + let movement = Movement::Up; + let words = ["abc", "xyz"]; + let word_offsets = [5, 17]; + let hex_dump_pane_dimensions = HexDumpPane { + dump_width: 4, + dump_height: 3, + addr_width: 0, // unused + addr_to_dump_padding: 0, // unused + }; + + let end_selection = move_and_refit( + start_selection, + movement, + &words, + &word_offsets, + &hex_dump_pane_dimensions, + 2, + ); + + assert_eq!(end_selection, expected_end_selection); + } + + #[test] + fn test_word_move_right() { + // v v + // abc. .... + // .... .xyz + // .... .... + let start_selection = SelectedChunk { + pane_num: 0, + row_num: 0, + col_start: 0, + len: 3, + }; + let expected_end_selection = SelectedChunk { + pane_num: 0, + row_num: 0, + col_start: 3, + len: 1, + }; + let movement = Movement::Right; + let words = ["abc", "xyz"]; + let word_offsets = [0, 17]; + let hex_dump_pane_dimensions = HexDumpPane { + dump_width: 4, + dump_height: 3, + addr_width: 0, // unused + addr_to_dump_padding: 0, // unused + }; + + let end_selection = move_and_refit( + start_selection, + movement, + &words, + &word_offsets, + &hex_dump_pane_dimensions, + 2, + ); + + assert_eq!(end_selection, expected_end_selection); + } + + #[test] + fn test_word_move_left() { + // abc. .... + // .... .xyz + // .... ^^.. + let start_selection = SelectedChunk { + pane_num: 1, + row_num: 1, + col_start: 1, + len: 3, + }; + let expected_end_selection = SelectedChunk { + pane_num: 1, + row_num: 1, + col_start: 0, + len: 1, + }; + let movement = Movement::Left; + let words = ["abc", "xyz"]; + let word_offsets = [0, 17]; + let hex_dump_pane_dimensions = HexDumpPane { + dump_width: 4, + dump_height: 3, + addr_width: 0, // unused + addr_to_dump_padding: 0, // unused + }; + + let end_selection = move_and_refit( + start_selection, + movement, + &words, + &word_offsets, + &hex_dump_pane_dimensions, + 2, + ); + + assert_eq!(end_selection, expected_end_selection); + } + + #[test] + fn test_move_word_wrapped() { + // v v + // ..ab .... + // c... .xyz + // .... .... + let start_selection = SelectedChunk { + pane_num: 0, + row_num: 0, + col_start: 2, + len: 3, + }; + let expected_end_selection = SelectedChunk { + pane_num: 1, + row_num: 0, + col_start: 0, + len: 1, + }; + let movement = Movement::Right; + let words = ["abc", "xyz"]; + let word_offsets = [2, 17]; + let hex_dump_pane_dimensions = HexDumpPane { + dump_width: 4, + dump_height: 3, + addr_width: 0, // unused + addr_to_dump_padding: 0, // unused + }; + + let end_selection = move_and_refit( + start_selection, + movement, + &words, + &word_offsets, + &hex_dump_pane_dimensions, + 2, + ); + + assert_eq!(end_selection, expected_end_selection); + } + + #[test] + fn test_move_up_into_word_selection_vertical() { + // v-start + // .... .... + // .abc .... + // .... .xyz + // ^-end + let start_selection = SelectedChunk { + pane_num: 1, + row_num: 0, + col_start: 2, + len: 1, + }; + let expected_end_selection = SelectedChunk { + pane_num: 1, + row_num: 2, + col_start: 1, + len: 3, + }; + let movement = Movement::Up; + let words = ["abc", "xyz"]; + let word_offsets = [5, 21]; + let hex_dump_pane_dimensions = HexDumpPane { + dump_width: 4, + dump_height: 3, + addr_width: 0, // unused + addr_to_dump_padding: 0, // unused + }; + + let end_selection = move_and_refit( + start_selection, + movement, + &words, + &word_offsets, + &hex_dump_pane_dimensions, + 2, + ); + + assert_eq!(end_selection, expected_end_selection); + } + + #[test] + fn test_move_down_into_word_selection_vertical() { + // .... .... + // .abc ..v-start + // .... .xyz + // ^-end + let start_selection = SelectedChunk { + pane_num: 1, + row_num: 1, + col_start: 2, + len: 1, + }; + let expected_end_selection = SelectedChunk { + pane_num: 1, + row_num: 2, + col_start: 1, + len: 3, + }; + let movement = Movement::Down; + let words = ["abc", "xyz"]; + let word_offsets = [5, 21]; + let hex_dump_pane_dimensions = HexDumpPane { + dump_width: 4, + dump_height: 3, + addr_width: 0, // unused + addr_to_dump_padding: 0, // unused + }; + + let end_selection = move_and_refit( + start_selection, + movement, + &words, + &word_offsets, + &hex_dump_pane_dimensions, + 2, + ); + + assert_eq!(end_selection, expected_end_selection); + } + + #[test] + fn test_move_left_into_cross_pane_word_selection() { + // v-start + // .... z... + // .abc .... + // ..xy .... + // ^-end + let start_selection = SelectedChunk { + pane_num: 1, + row_num: 0, + col_start: 1, + len: 1, + }; + let expected_end_selection = SelectedChunk { + pane_num: 0, + row_num: 2, + col_start: 2, + len: 3, + }; + let movement = Movement::Left; + let words = ["abc", "xyz"]; + let word_offsets = [5, 10]; + let hex_dump_pane_dimensions = HexDumpPane { + dump_width: 4, + dump_height: 3, + addr_width: 0, // unused + addr_to_dump_padding: 0, // unused + }; + + let end_selection = move_and_refit( + start_selection, + movement, + &words, + &word_offsets, + &hex_dump_pane_dimensions, + 2, + ); + + assert_eq!(end_selection, expected_end_selection); + } } diff --git a/src/main.rs b/src/main.rs index 2d5be02..eb89a69 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,6 +1,8 @@ extern crate pancurses; extern crate rand; extern crate snm_simple_file; +#[macro_use] +extern crate static_assertions; mod dict; mod game;