diff --git a/src/hex/draw.rs b/src/hex/draw.rs index 006174a..e4a3d86 100644 --- a/src/hex/draw.rs +++ b/src/hex/draw.rs @@ -2,7 +2,8 @@ use ratatui::{ Frame, layout::{Constraint, Rect}, style::{Color, Style}, - widgets::{Cell, Clear, Row, Table}, + text::{Line, Span}, + widgets::{Cell, Clear, Row, Table, Paragraph}, }; use crate::{app::App, editor::UIState}; @@ -162,114 +163,148 @@ pub fn draw_hex_contents(app: &mut App, frame: &mut Frame, area: Rect) { /// /// OBS.: Table é criada a partir de Row, que são conjuntos de Cell pub fn draw_hex_ascii(app: &mut App, frame: &mut Frame, area: Rect) { - // Uma linha é um conjunto de células, cada uma contendo um caractere - let mut row: Vec = Vec::with_capacity(app.config.hex_mode_bytes_per_line); + let mut lines: Vec = Vec::new(); + let char_style = app.config.theme.main; - // A tabela precisa receber um conjunto de linhas - let mut rows: Vec = Vec::new(); - - // O estilo que será usado no caractere individual - let mut char_style = app.config.theme.main; - - // Se estiver editando e apertou tab para editar via ASCII, usa o highlight - // correto a partir do tema let cell_hl_style = if app.state == UIState::HexEditing && !app.hex_view.editing_hex { app.config.theme.editing } else { app.config.theme.highlight }; + let bytes_per_line = app.config.hex_mode_bytes_per_line; + let page_start = app.reader.page_start; + let page_current_size = app.reader.page_current_size; + let file_size = app.file_info.size; let buffer = app.file_info.get_buffer(); - for (i, byte) in buffer - .iter() - .skip(app.reader.page_start) - .take(app.reader.page_current_size) - .enumerate() - { - // Antes de criar a Cell a partir do byte, preciso tratar - // os bytes inválidos em ASCII - let c = if (*byte).is_ascii_graphic() { - *byte as char - } else { - app.config.hex_mode_non_graphic_char - }; + let page_bytes_len = file_size.saturating_sub(page_start).min(page_current_size); - // O conteúdo e o estilo da célula agora vai depender se - // o offset atual está no hashmap de bytes alterados - let offset = i + app.reader.page_start; - let cell = if app.hex_view.changed_bytes.contains_key(&offset) { - // Set regular highlight style if selection is happening - char_style = if app.hex_view.selection.contains(offset) { - app.config.theme.highlight - } else { - app.config.theme.changed_bytes - }; - // Recupera o byte alterado (como hex string) - let s = &app.hex_view.changed_bytes[&offset]; - - // Converte para um u8 numérico. Se não rolar, é porque deu uma - // merda muito grande pois só deveria ter hex strings no hashmap. - let num = u8::from_str_radix(s, 16).unwrap(); - // If changed byte is not printable use the non graphic char instead - let c = if (num as char).is_ascii_graphic() { - num as char - } else { - app.config.hex_mode_non_graphic_char - }; - // Agora cria uma string a partir do char `c` - // Parece doido, mas isso faz "41" -> 0x41 -> "A" - let s = String::from(c); - // Por fim, retorna a célula - Cell::new(s).style(char_style) - } else if app.state == UIState::HexSelection && app.hex_view.selection.contains(offset) { - char_style = app.config.theme.highlight; - let s = String::from(c); - Cell::new(s).style(char_style) + if page_bytes_len == 0 { + let paragraph = Paragraph::new(lines); + frame.render_widget(Clear, area); + frame.render_widget(paragraph, area); + return; + } + + // Step 1: Build the entire page bytes (applying changed_bytes) + let mut page_bytes = vec![0u8; page_bytes_len]; + for (i, b) in page_bytes.iter_mut().enumerate() { + let offset = page_start + i; + *b = if let Some(s) = app.hex_view.changed_bytes.get(&offset) { + u8::from_str_radix(s, 16).unwrap_or(buffer[offset]) } else { - // Se não for um byte alterado, usa o estilo padrão do tema - char_style = app.config.theme.main; - // Cria a string a partir do char `c` e retorna a célula - let s = String::from(c); - Cell::new(s).style(char_style) + buffer[offset] }; + } - // Agora a célula tá pronta para ser colocado na linha, - // mas antes aplico o estilo nela - row.push(cell.style(char_style)); + // Step 2: Decode the entire page to find character boundaries + let mut char_cells = vec![(app.config.hex_mode_non_graphic_char.to_string(), 1usize); page_bytes_len]; - // Se chegamos no fim da linha - if (i + 1) % app.config.hex_mode_bytes_per_line == 0 { - // Cria uma linha a partir do vetor de células - // e a adiciona no vetor de linhas - rows.push(Row::new(row.clone())); - // Limpa a linha para ser reutilizada - row.clear(); + let mut idx = 0; + while idx < page_bytes_len { + if char_cells[idx].1 == 0 { + idx += 1; + continue; + } + + let mut found = false; + for len in 1..=4 { + if idx + len > page_bytes_len { + continue; + } + let slice = &page_bytes[idx..idx + len]; + let (decoded_str, _, had_errors) = app.text_view.table.decode(slice); + + if !had_errors && decoded_str.chars().count() == 1 { + let c = decoded_str.chars().next().unwrap(); + if c != '\u{FFFD}' && !c.is_control() { + let cell_char = if c.is_ascii() { + if c.is_ascii_graphic() { + c + } else { + app.config.hex_mode_non_graphic_char + } + } else if !c.is_whitespace() { + c + } else { + app.config.hex_mode_non_graphic_char + }; + + char_cells[idx] = (cell_char.to_string(), len); + for j in 1..len { + if idx + j < page_bytes_len { + char_cells[idx + j] = (String::new(), 0); + } + } + found = true; + break; + } + } } - } // for - // Se a última linha não estiver vazia, significa que ela não foi - // incluída no vetor de linhas ainda. É o caso onde a última linha - // tem menos de `app.config.hex_mode_bytes_per_line` bytes. - if !row.is_empty() { - rows.push(Row::new(row)); + if !found { + char_cells[idx] = (app.config.hex_mode_non_graphic_char.to_string(), 1); + } + + idx += 1; } - // Atualiza o estado da tabela com a seleção do cursor - app.hex_view.ascii_state.select(Some(app.hex_view.cursor.y)); - app.hex_view - .ascii_state - .select_column(Some(app.hex_view.cursor.x)); + // Step 3: Split into rows and build styled lines + let mut row_start = 0; + while row_start < page_bytes_len { + let row_end = (row_start + bytes_per_line).min(page_bytes_len); - // O Constraint contém as dimensões da tabela - let constraint = vec![Constraint::Length(1); app.config.hex_mode_bytes_per_line]; + let mut spans: Vec = Vec::new(); + let mut col_idx = row_start; + while col_idx < row_end { + let (cell_str, byte_len) = &char_cells[col_idx]; - // Cria a tabela - let table = Table::new(rows, constraint) - .column_spacing(0) - .style(char_style) - .cell_highlight_style(cell_hl_style); + if *byte_len == 0 { + col_idx += 1; + continue; + } + + let offset = page_start + col_idx; + let local_col = col_idx - row_start; + + let is_cursor_on_char = app.hex_view.cursor.y == (row_start / bytes_per_line) + && app.hex_view.cursor.x >= local_col + && app.hex_view.cursor.x < local_col + byte_len; + + let mut span_style = char_style; + + if is_cursor_on_char { + span_style = cell_hl_style; + } else if app.state == UIState::HexSelection && app.hex_view.selection.contains(offset) { + span_style = app.config.theme.highlight; + } else if app.hex_view.changed_bytes.contains_key(&offset) { + if !app.hex_view.selection.contains(offset) { + span_style = app.config.theme.changed_bytes; + } + } + + // If this multi-byte char spans across the row boundary, show non-graphic for + // the bytes within this row and let the next row handle the rest + if col_idx + byte_len > row_end { + for _ in col_idx..row_end { + spans.push(Span::styled( + app.config.hex_mode_non_graphic_char.to_string(), + span_style, + )); + } + break; + } + + spans.push(Span::styled(cell_str.clone(), span_style)); + col_idx += byte_len; + } + lines.push(Line::from(spans)); + row_start += bytes_per_line; + } + + let paragraph = Paragraph::new(lines); - // Desenha a tabela frame.render_widget(Clear, area); - frame.render_stateful_widget(table, area, &mut app.hex_view.ascii_state); + frame.render_widget(paragraph, area); } + diff --git a/src/hex/edit.rs b/src/hex/edit.rs index a58ea28..3f30fef 100644 --- a/src/hex/edit.rs +++ b/src/hex/edit.rs @@ -131,7 +131,19 @@ pub fn edit_events(app: &mut App, key: KeyEvent) -> Result { } } } else { - fill_with(app, c as u8, true); + let encoded_bytes = crate::util::encode_char(c, app.text_view.table); + let mut ofs = app.hex_view.offset; + for &b in encoded_bytes.iter() { + if ofs < app.file_info.size { + let s = format!("{:02X}", b); + app.hex_view.changed_bytes.insert(ofs, s); + app.hex_view.changed_history.push(ofs); + ofs += 1; + } else { + break; + } + } + app.goto(ofs); } } _ => {} diff --git a/src/hex/events.rs b/src/hex/events.rs index 04adba8..0e8dcc3 100644 --- a/src/hex/events.rs +++ b/src/hex/events.rs @@ -354,6 +354,11 @@ pub fn hex_mode_events(app: &mut App, key: KeyEvent) -> Result { } } } + // change encoding + KeyCode::Char('e') => { + app.state = UIState::DialogEncoding; + app.dialog_renderer = Some(crate::text::dialog_encoding::dialog_encoding_draw); + } _ => {} } Ok(false) diff --git a/src/hex/names.rs b/src/hex/names.rs index fb93979..47cab83 100644 --- a/src/hex/names.rs +++ b/src/hex/names.rs @@ -8,27 +8,44 @@ use ratatui::{ use ratatui::crossterm::event::{Event, KeyCode}; use std::io::Result; +use tui_input::backend::crossterm::EventHandler; -use crate::{app::App, commands::Commands, editor::UIState, util::center_widget}; +use crate::{app::App, editor::UIState, util::center_widget}; pub fn dialog_names_draw(app: &mut App, frame: &mut Frame) { - let n = app.hex_view.comment_name_list.len(); - let mut items = Vec::with_capacity(n); + let re = if !app.hex_view.names_regex.is_empty() { + regex::RegexBuilder::new(&app.hex_view.names_regex) + .case_insensitive(true) + .build() + .ok() + } else { + None + }; + + let mut items = Vec::new(); + let mut count = 0; for cmt in &app.hex_view.comment_name_list { - items.push(ListItem::from(format!( - "{:08X} {}", - cmt.offset, cmt.comment - ))); - } + let is_match = if let Some(ref r) = re { + r.is_match(&cmt.comment) + } else { + true + }; - let names_count = app.hex_view.comment_name_list.len(); + if is_match { + items.push(ListItem::from(format!( + "{:08X} {}", + cmt.offset, cmt.comment + ))); + count += 1; + } + } let list = List::new(items) .style(app.config.theme.dialog) .block( Block::bordered() - .title(format!(" Names ({}) ", names_count)) + .title(format!(" Names ({}) ", count)) .title_alignment(Alignment::Center) .padding(Padding::horizontal(1)), ) @@ -82,16 +99,30 @@ pub fn dialog_names_events(app: &mut App, event: &Event) -> Result { } KeyCode::Enter => { if let Some(choice) = app.hex_view.names_list_state.selected() { - if choice > app.hex_view.comment_name_list.len() { - App::log( - app, - "wtf {choice} is greater than `app.hex_mode.comments.len()`, dunno how" - .to_string(), - ); - return Ok(true); + let re = if !app.hex_view.names_regex.is_empty() { + regex::RegexBuilder::new(&app.hex_view.names_regex) + .case_insensitive(true) + .build() + .ok() + } else { + None + }; + + let mut matched_comments = Vec::new(); + for cmt in &app.hex_view.comment_name_list { + let is_match = if let Some(ref r) = re { + r.is_match(&cmt.comment) + } else { + true + }; + if is_match { + matched_comments.push(cmt.clone()); + } + } + + if choice < matched_comments.len() { + app.goto(matched_comments[choice].offset); } - // Vec - app.goto(app.hex_view.comment_name_list[choice].offset); } app.state = UIState::Normal; app.dialog_renderer = None; @@ -147,9 +178,11 @@ pub fn dialog_names_regex_events(app: &mut App, event: &Event) -> Result { app.hex_view.names_regex = String::from(app.hex_view.names_regex_input.value()); app.dialog_2nd_renderer = None; app.state = UIState::DialogNames; - Commands::load_strings(app, true); + app.hex_view.names_list_state.select(Some(0)); + } + _ => { + app.hex_view.names_regex_input.handle_event(event); } - _ => (), } } Ok(false) diff --git a/src/text/dialog_encoding.rs b/src/text/dialog_encoding.rs index 2512545..f920ff2 100644 --- a/src/text/dialog_encoding.rs +++ b/src/text/dialog_encoding.rs @@ -7,19 +7,20 @@ use crate::{app::App, editor::UIState, widgets::ListChoice}; use std::io::Result; +const ENCODING_LIST: [&str; 7] = [ + "UTF-8", + "CP949 (EUC-KR)", + "CP936 (GBK)", + "ISO-8859-1", + "ISO-8859-2", + "UTF-16-LE", + "UTF-16-BE", +]; + pub fn dialog_encoding_draw(app: &mut App, frame: &mut Frame) { let mut dialog = ListChoice::new(); dialog.set_title(" Select encoding ".to_string()); - dialog.choices = [ - "UTF-8", - "ISO-8859-1", - "ISO-8859-2", - "UTF-16-LE", - "UTF-16-BE", - ] - .iter() - .map(|s| s.to_string()) - .collect(); + dialog.choices = ENCODING_LIST.iter().map(|s| s.to_string()).collect(); dialog.render(app, frame); } @@ -36,10 +37,12 @@ pub fn dialog_encoding_events(app: &mut App, key: KeyEvent) -> Result { let sel = app.list_state.selected().unwrap_or(0); app.text_view.table = match sel { 0 => encoding_rs::UTF_8, - 1 => encoding_rs::WINDOWS_1252, - 2 => encoding_rs::ISO_8859_2, - 3 => encoding_rs::UTF_16LE, - 4 => encoding_rs::UTF_16BE, + 1 => encoding_rs::EUC_KR, + 2 => encoding_rs::GBK, + 3 => encoding_rs::WINDOWS_1252, + 4 => encoding_rs::ISO_8859_2, + 5 => encoding_rs::UTF_16LE, + 6 => encoding_rs::UTF_16BE, _ => encoding_rs::UTF_8, }; @@ -47,7 +50,7 @@ pub fn dialog_encoding_events(app: &mut App, key: KeyEvent) -> Result { app.dialog_renderer = None; } KeyCode::Down | KeyCode::Char('j') => { - if app.list_state.selected() == Some(4) { + if app.list_state.selected() == Some(ENCODING_LIST.len() - 1) { app.list_state.select_first(); } else { app.list_state.select_next(); @@ -70,3 +73,5 @@ pub fn dialog_encoding_events(app: &mut App, key: KeyEvent) -> Result { } Ok(false) } + + diff --git a/src/util.rs b/src/util.rs index 55a8b1b..ff3bbfc 100644 --- a/src/util.rs +++ b/src/util.rs @@ -21,6 +21,38 @@ pub fn center_widget(width: u16, height: u16, area: Rect) -> Rect { } } +pub fn encode_char(c: char, enc: &'static encoding_rs::Encoding) -> Vec { + match enc.name() { + "UTF-16LE" => { + let mut buf = [0u16; 2]; + let u16_slice = c.encode_utf16(&mut buf); + let mut bytes = Vec::new(); + for &val in u16_slice.iter() { + bytes.extend_from_slice(&val.to_le_bytes()); + } + bytes + } + "UTF-16BE" => { + let mut buf = [0u16; 2]; + let u16_slice = c.encode_utf16(&mut buf); + let mut bytes = Vec::new(); + for &val in u16_slice.iter() { + bytes.extend_from_slice(&val.to_be_bytes()); + } + bytes + } + _ => { + let c_str = c.to_string(); + let (encoded_bytes, _, has_unmappable) = enc.encode(&c_str); + if has_unmappable { + vec![b'?'] + } else { + encoded_bytes.into_owned() + } + } + } +} + #[cfg(test)] mod tests { use super::*; diff --git a/src/widgets.rs b/src/widgets.rs index da0720a..42b48c1 100644 --- a/src/widgets.rs +++ b/src/widgets.rs @@ -58,8 +58,18 @@ impl ListChoice { pub fn render(&mut self, app: &mut App, frame: &mut Frame) { let area = frame.area(); - let dialog_area = center_widget(area.width / 3, area.height / 4, area); - + + // Calculate height dynamically: choices count + 2 (borders) + let dialog_height = (self.choices.len() as u16 + 2).min(area.height); + + // Calculate width dynamically: longest choice + padding + let max_choice_width = self.choices.iter().map(|s| s.len()).max().unwrap_or(0) as u16; + let dialog_width = (max_choice_width + 12).max(area.width / 3).min(area.width); + + // Calculate centered position, then shift it upwards dynamically based on height (roughly 2 lines for standard heights) + let mut dialog_area = center_widget(dialog_width, dialog_height, area); + dialog_area.y = dialog_area.y.saturating_sub((area.height / 12).max(1)); + let block = Block::new() .title(Line::raw(self.title.clone()).centered()) .borders(Borders::ALL)