hex_patch/app/
text.rs

1use ratatui::text::{Line, Span, Text};
2
3use super::{log::NotificationLevel, settings::color_settings::ColorSettings, App};
4
5impl App {
6    pub(super) fn bytes_to_styled_text(
7        color_settings: &ColorSettings,
8        bytes: &'_ [u8],
9        block_size: usize,
10        blocks_per_row: usize,
11        selected_byte_offset: usize,
12    ) -> Text<'static> {
13        let mut ret = Text::default();
14        ret.lines
15            .reserve(bytes.len() / (block_size * blocks_per_row) + 1);
16        let mut current_line = Line::default();
17        let mut local_block = 0;
18        let mut local_byte = 0;
19        for (byte_index, b) in bytes.iter().enumerate() {
20            let style = if byte_index == selected_byte_offset {
21                color_settings.text_selected
22            } else {
23                Self::get_style_for_byte(color_settings, *b)
24            };
25            let mut next_line = false;
26            let char = Self::u8_to_char(*b);
27            let char_string = char.to_string();
28            let span = Span::styled(char_string, style);
29            current_line.spans.push(span);
30            let mut spacing_string = " ".to_string();
31            local_byte += 1;
32            if local_byte % block_size == 0 {
33                local_byte = 0;
34                spacing_string.push(' ');
35
36                local_block += 1;
37                if local_block % blocks_per_row == 0 {
38                    local_block = 0;
39                    next_line = true;
40                }
41            }
42
43            let span = Span::raw(spacing_string);
44            current_line.spans.push(span);
45
46            if next_line {
47                let new_line = std::mem::take(&mut current_line);
48                ret.lines.push(new_line);
49            }
50        }
51        if !current_line.spans.is_empty() {
52            ret.lines.push(current_line);
53        }
54
55        ret
56    }
57
58    pub(super) fn insert_text(&mut self, text: &str) {
59        self.patch_bytes(text.as_bytes(), false);
60    }
61
62    fn found_text_here(&self, starting_from: usize, text: &str) -> bool {
63        for (i, byte) in text.bytes().enumerate() {
64            if self.data.len() <= starting_from + i || self.data.bytes()[starting_from + i] != byte
65            {
66                return false;
67            }
68        }
69        true
70    }
71
72    pub(super) fn get_text_view(&self, start_row: usize, end_row: usize) -> Text<'static> {
73        let start_byte = start_row * self.blocks_per_row * self.block_size;
74        let end_byte = end_row * self.blocks_per_row * self.block_size;
75        let end_byte = std::cmp::min(end_byte, self.data.len());
76        let bytes = &self.data.bytes()[start_byte..end_byte];
77        let selected_byte_offset = self
78            .get_cursor_position()
79            .global_byte_index
80            .saturating_sub(start_byte);
81        Self::bytes_to_styled_text(
82            &self.settings.color,
83            bytes,
84            self.block_size,
85            self.blocks_per_row,
86            selected_byte_offset,
87        )
88    }
89
90    pub(super) fn find_text(&mut self, text: &str) {
91        if text.is_empty() || self.data.is_empty() {
92            return;
93        }
94        let already_searched = self.text_last_searched_string == text;
95        if !already_searched {
96            self.text_last_searched_string = text.to_string();
97        }
98        let mut search_here = self.get_cursor_position().global_byte_index;
99        // find the next occurrence of the text
100        if already_searched && Self::found_text_here(self, search_here, text) {
101            search_here += text.len();
102        } else {
103            search_here = 0;
104        }
105        let max_search_index = self.data.len() + search_here;
106        while search_here < max_search_index {
107            let actual_search_here = search_here % self.data.len();
108            if Self::found_text_here(self, actual_search_here, text) {
109                self.jump_to(actual_search_here, false);
110                return;
111            }
112            search_here += 1;
113        }
114        self.log(
115            NotificationLevel::Warning,
116            t!("app.messages.text_not_found"),
117        );
118    }
119
120    pub(super) fn u8_to_char(input: u8) -> char {
121        match input {
122            0x20..=0x7E => input as char,
123            0x0A => '⏎',
124            0x0C => '↡',
125            0x0D => '↵',
126            0x08 => '⇤',
127            0x09 => '⇥',
128            0x1B => '␛',
129            0x7F => '␡',
130            _ => '.',
131        }
132    }
133}
134
135#[cfg(test)]
136mod tests {
137    use super::*;
138
139    #[test]
140    fn test_bytes_to_styled_text() {
141        let color_settings = ColorSettings::get_default_dark_theme();
142        let bytes = b"CAFEBABE";
143        let block_size = 8;
144        let blocks_per_row = 2;
145        let selected_byte_offset = 0;
146        let text = App::bytes_to_styled_text(
147            &color_settings,
148            bytes,
149            block_size,
150            blocks_per_row,
151            selected_byte_offset,
152        );
153        assert_eq!(text.lines.len(), 1);
154        let mut char_index = 0;
155        for char in text.lines[0]
156            .spans
157            .iter()
158            .flat_map(|span| span.content.chars())
159        {
160            if char.is_alphanumeric() {
161                assert_eq!(char, bytes[char_index] as char);
162                char_index += 1;
163            }
164        }
165        assert_eq!(char_index, bytes.len());
166    }
167}