├── .gitignore ├── CHANGELOG.md ├── Cargo.toml ├── LICENSE ├── README.md ├── rustfmt.toml ├── src ├── colors.rs ├── gui │ ├── hex │ │ ├── ascii_view.rs │ │ ├── goto.rs │ │ ├── hex_grid.rs │ │ ├── info_line.rs │ │ ├── lines.rs │ │ ├── mod.rs │ │ └── search.rs │ └── mod.rs ├── main.rs └── utils.rs └── ss.png /.gitignore: -------------------------------------------------------------------------------- 1 | target 2 | Cargo.lock 3 | .gdb_history 4 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # WIP 2 | 3 | - Fix a bug that caused terminating rhex in in search mode when typed 'q'. 4 | 5 | # 2017/07/14: 0.1.0 6 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "rhex" 3 | version = "0.1.0" 4 | authors = ["Ömer Sinan Ağacan "] 5 | description = "A rudimentary hex editor" 6 | license = "MIT" 7 | repository = "https://github.com/osa1/rhex" 8 | 9 | [dependencies] 10 | libc = "0.2" 11 | nix = "0.9" 12 | term_input = "0.1.4" 13 | termbox_simple = "0.2.2" 14 | 15 | [profile.release] 16 | lto = true 17 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2017 Ömer Sinan Ağacan 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of 4 | this software and associated documentation files (the "Software"), to deal in 5 | the Software without restriction, including without limitation the rights to 6 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies 7 | of the Software, and to permit persons to whom the Software is furnished to do 8 | so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | SOFTWARE. 20 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## rhex - a rudimentary, curses-based hex editor 2 | 3 | ![rhex in action](ss.png) 4 | -------------------------------------------------------------------------------- /rustfmt.toml: -------------------------------------------------------------------------------- 1 | error_on_line_overflow_comments = false 2 | match_arm_forces_newline = true 3 | wrap_match_arms = false 4 | -------------------------------------------------------------------------------- /src/colors.rs: -------------------------------------------------------------------------------- 1 | use termbox_simple::*; 2 | 3 | pub struct Style { 4 | pub fg: u16, 5 | pub bg: u16, 6 | } 7 | 8 | pub const DEFAULT: Style = Style { 9 | fg: TB_DEFAULT, 10 | bg: TB_DEFAULT, 11 | }; 12 | 13 | pub const CURSOR_NO_FOCUS: Style = Style { 14 | fg: TB_WHITE, 15 | bg: TB_YELLOW, 16 | }; 17 | 18 | pub const CURSOR_FOCUS: Style = Style { 19 | fg: TB_WHITE, 20 | bg: TB_GREEN, 21 | }; 22 | 23 | pub const STATUS_BAR: Style = Style { 24 | fg: TB_WHITE, 25 | bg: TB_GREEN, 26 | }; 27 | 28 | pub const HIGHLIGHT: Style = Style { 29 | fg: TB_BLACK, 30 | bg: TB_BLUE, 31 | }; 32 | -------------------------------------------------------------------------------- /src/gui/hex/ascii_view.rs: -------------------------------------------------------------------------------- 1 | use std::cmp; 2 | 3 | use colors; 4 | 5 | use termbox_simple::*; 6 | 7 | pub struct AsciiView<'view> { 8 | pos_x: i32, 9 | pos_y: i32, 10 | width: i32, 11 | height: i32, 12 | 13 | data: &'view [u8], 14 | 15 | cursor_x: i32, 16 | cursor_y: i32, 17 | scroll: i32, 18 | 19 | has_focus: bool, 20 | } 21 | 22 | impl<'view> AsciiView<'view> { 23 | pub fn new( 24 | width: i32, 25 | height: i32, 26 | pos_x: i32, 27 | pos_y: i32, 28 | data: &'view [u8], 29 | ) -> AsciiView<'view> { 30 | AsciiView { 31 | width: width, 32 | height: height, 33 | pos_x: pos_x, 34 | pos_y: pos_y, 35 | data: data, 36 | cursor_x: 0, 37 | cursor_y: 0, 38 | scroll: 0, 39 | has_focus: false, 40 | } 41 | } 42 | 43 | pub fn set_scroll(&mut self, scroll: i32) { 44 | self.scroll = scroll; 45 | } 46 | 47 | pub fn draw(&self, tb: &mut Termbox, hl: &[usize], hl_len: usize) { 48 | let rows = self.height; 49 | let cols = self.width; 50 | 51 | let mut hl_idx = 0; 52 | 53 | 'outer: for row in self.scroll..self.scroll + rows { 54 | for col in 0..cols { 55 | let byte_idx = (row * cols + col) as usize; 56 | if let Some(&byte) = self.data.get(byte_idx) { 57 | let ch = if byte >= 32 && byte <= 126 { 58 | byte 59 | } else { 60 | b'.' 61 | }; 62 | 63 | while hl_idx < hl.len() && hl[hl_idx] + hl_len < byte_idx { 64 | hl_idx += 1; 65 | } 66 | 67 | let style = if self.cursor_x == col && self.cursor_y == row { 68 | if self.has_focus { 69 | colors::CURSOR_FOCUS 70 | } else { 71 | colors::CURSOR_NO_FOCUS 72 | } 73 | } else if let Some(&hl_offset) = hl.get(hl_idx) { 74 | if byte_idx >= hl_offset && byte_idx < hl_offset + hl_len { 75 | colors::HIGHLIGHT 76 | } else { 77 | colors::DEFAULT 78 | } 79 | } else { 80 | colors::DEFAULT 81 | }; 82 | 83 | tb.change_cell( 84 | self.pos_x + col, 85 | self.pos_y + row - self.scroll, 86 | ch as char, 87 | style.fg, 88 | style.bg, 89 | ); 90 | } else { 91 | break 'outer; 92 | } 93 | } 94 | } 95 | } 96 | 97 | pub fn move_cursor_offset(&mut self, byte_idx: i32) { 98 | let cursor_y = byte_idx / self.width; 99 | let cursor_x = byte_idx % self.width; 100 | 101 | if cursor_y > self.scroll + self.height - 3 { 102 | self.scroll = cursor_y - (self.height - 3); 103 | } else if cursor_y < self.scroll + 2 { 104 | self.scroll = cmp::max(cursor_y - 2, 0); 105 | } 106 | 107 | self.cursor_y = cursor_y; 108 | self.cursor_x = cursor_x; 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /src/gui/hex/goto.rs: -------------------------------------------------------------------------------- 1 | use std::char; 2 | use std::cmp; 3 | 4 | use colors; 5 | use utils::*; 6 | 7 | use term_input::Key; 8 | use termbox_simple::*; 9 | 10 | /// Return value of the overlay. Returned by `keypressed()` method. 11 | pub enum OverlayRet { 12 | /// User submitted the form. 13 | Ret(i32), 14 | 15 | /// For vi-like "go to beginning" (gg) 16 | GotoBeginning, 17 | 18 | /// User cancelled. 19 | Abort, 20 | 21 | /// Overlay still has focus. 22 | Continue, 23 | } 24 | 25 | pub struct GotoOverlay { 26 | pos_x: i32, 27 | pos_y: i32, 28 | width: i32, 29 | height: i32, 30 | input: String, 31 | } 32 | 33 | impl GotoOverlay { 34 | pub fn new(width: i32, height: i32, pos_x: i32, pos_y: i32) -> GotoOverlay { 35 | let width_ = cmp::min(width, 50); 36 | let height_ = cmp::min(height, 10); 37 | 38 | let pos_x = pos_x + (width - width_) / 2; 39 | let pos_y = pos_y + (height - height_) / 2; 40 | 41 | GotoOverlay { 42 | pos_x, 43 | pos_y, 44 | width: width_, 45 | height: height_, 46 | input: String::new(), 47 | } 48 | } 49 | 50 | pub fn draw(&self, tb: &mut Termbox) { 51 | draw_box(tb, self.pos_x, self.pos_y, self.width, self.height); 52 | print( 53 | tb, 54 | self.pos_x + 5, 55 | self.pos_y + 3, 56 | colors::DEFAULT, 57 | "Goto byte offset:", 58 | ); 59 | print(tb, self.pos_x + 5, self.pos_y + 5, colors::DEFAULT, ">"); 60 | print( 61 | tb, 62 | self.pos_x + 7, 63 | self.pos_y + 5, 64 | colors::DEFAULT, 65 | &self.input, 66 | ); 67 | 68 | tb.change_cell( 69 | self.pos_x + 7 + self.input.len() as i32, 70 | self.pos_y + 5, 71 | ' ', 72 | colors::CURSOR_FOCUS.fg, 73 | colors::CURSOR_FOCUS.bg, 74 | ); 75 | } 76 | 77 | pub fn keypressed(&mut self, key: Key) -> OverlayRet { 78 | match key { 79 | Key::Char(ch) if (ch >= '0' && ch <= '9') => { 80 | self.input.push(char::from_u32(ch as u32).unwrap()); 81 | OverlayRet::Continue 82 | } 83 | Key::Char('g') => 84 | OverlayRet::GotoBeginning, 85 | Key::Esc => 86 | OverlayRet::Abort, 87 | Key::Backspace => { 88 | self.input.pop(); 89 | OverlayRet::Continue 90 | } 91 | Key::Char('\r') => 92 | if self.input.is_empty() { 93 | OverlayRet::Abort 94 | } else { 95 | OverlayRet::Ret(self.input.parse().unwrap()) 96 | }, 97 | _ => 98 | OverlayRet::Continue, 99 | } 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /src/gui/hex/hex_grid.rs: -------------------------------------------------------------------------------- 1 | use std::cmp; 2 | use std::ptr; 3 | 4 | use gui::hex::HexGui; 5 | 6 | use colors; 7 | use utils::*; 8 | 9 | use term_input::{Arrow, Key}; 10 | use termbox_simple::*; 11 | 12 | pub struct HexGrid<'grid> { 13 | pos_x: i32, 14 | pos_y: i32, 15 | width: i32, 16 | height: i32, 17 | 18 | data: &'grid [u8], 19 | path: &'grid str, 20 | 21 | cursor_x: i32, 22 | cursor_y: i32, 23 | scroll: i32, 24 | 25 | gui: *mut HexGui<'grid>, 26 | } 27 | 28 | impl<'grid> HexGrid<'grid> { 29 | pub fn new( 30 | width: i32, 31 | height: i32, 32 | pos_x: i32, 33 | pos_y: i32, 34 | data: &'grid [u8], 35 | path: &'grid str, 36 | ) -> HexGrid<'grid> { 37 | HexGrid { 38 | pos_x: pos_x, 39 | pos_y: pos_y, 40 | height: height, 41 | width: width, 42 | data: data, 43 | path: path, 44 | 45 | // Cursor positions are relative to the grid. 46 | // (i.e. they stay the same when grid is moved) 47 | cursor_x: 0, 48 | cursor_y: 0, 49 | scroll: 0, 50 | 51 | gui: ptr::null_mut(), 52 | } 53 | } 54 | 55 | pub fn width(&self) -> i32 { 56 | self.width 57 | } 58 | 59 | pub fn set_gui(&mut self, gui: *mut HexGui<'grid>) { 60 | self.gui = gui; 61 | } 62 | 63 | /// How many bytes we can show in a line? 64 | pub fn bytes_per_line(&self) -> i32 { 65 | let bytes = self.width / 3; 66 | 67 | // Can we fit one more column? 68 | if self.width % 3 == 2 { 69 | bytes + 1 70 | } else { 71 | bytes 72 | } 73 | } 74 | 75 | /// Effective width of a line (e.g. ignores extra trailing space that we 76 | /// can't utilize) 77 | fn cols_per_line(&self) -> i32 { 78 | self.bytes_per_line() * 3 - 1 79 | } 80 | 81 | /// How many lines needed to draw the entire file? 82 | fn total_lines_needed(&self) -> i32 { 83 | let len = self.data.len() as i32; 84 | let bpl = self.bytes_per_line(); 85 | // round up 86 | (len + bpl - 1) / bpl 87 | } 88 | 89 | /// How many bytes do we render in last line? (this is usually different 90 | /// than self.width) 91 | fn last_line_bytes(&self) -> i32 { 92 | (self.data.len() % self.bytes_per_line() as usize) as i32 93 | } 94 | 95 | /// Unconditionally increment the Y position. Updates X position if there's 96 | /// not enough columns in the next line. 97 | /// 98 | /// This doesn't update anything other than X and Y positions. 99 | /// Only post-condition: post(self.pos_y) = self.pos_y + 1. 100 | fn move_next_line(&mut self) { 101 | let max_y = self.total_lines_needed() - 1; 102 | debug_assert!(self.cursor_y + 1 <= max_y); 103 | if self.cursor_y + 1 == max_y { 104 | let last_line_bytes = self.last_line_bytes(); 105 | let last_line_cols = (last_line_bytes - 1) * 3 + 2; 106 | if self.cursor_x >= last_line_cols { 107 | self.cursor_x = last_line_cols - 1; 108 | } 109 | } 110 | self.cursor_y += 1; 111 | } 112 | 113 | pub fn get_byte_idx(&self) -> i32 { 114 | self.cursor_y * self.bytes_per_line() + self.cursor_x / 3 115 | } 116 | 117 | pub fn get_column(&self) -> i32 { 118 | self.cursor_x 119 | } 120 | 121 | pub fn get_row(&self) -> i32 { 122 | self.cursor_y 123 | } 124 | 125 | pub fn get_scroll(&self) -> i32 { 126 | self.scroll 127 | } 128 | 129 | pub fn try_center_scroll(&mut self) { 130 | if self.cursor_y - self.height / 2 >= 0 { 131 | self.scroll = self.cursor_y - self.height / 2; 132 | } 133 | } 134 | 135 | pub fn keypressed(&mut self, key: Key) -> bool { 136 | match key { 137 | Key::Arrow(Arrow::Up) | Key::Char('k') => { 138 | if self.cursor_y > self.scroll + 2 && self.cursor_y > 0 { 139 | self.cursor_y -= 1; 140 | } else if self.scroll > 0 { 141 | self.scroll -= 1; 142 | self.cursor_y -= 1; 143 | } else if self.cursor_y - 1 >= 0 { 144 | self.cursor_y -= 1 145 | } 146 | 147 | self.update_ascii_view(); 148 | self.update_lines(); 149 | self.update_info_line(); 150 | true 151 | } 152 | Key::Arrow(Arrow::Down) | Key::Char('j') => { 153 | // TODO: This assumes there's at least one line 154 | let max_y = self.total_lines_needed() - 1; 155 | 156 | if self.cursor_y < self.scroll + self.height - 3 && self.cursor_y < max_y { 157 | self.move_next_line(); 158 | } else if self.cursor_y < max_y { 159 | // We want to scroll, but is there a text to show? Otherwise we 160 | // just move cursor down. 161 | if self.scroll + self.height <= max_y { 162 | // We can scroll 163 | self.scroll += 1; 164 | // We move the cursor too, because it's not relative to the 165 | // current scroll 166 | self.cursor_y += 1; 167 | } else { 168 | // We can't scroll but there's a line that we can move to 169 | self.move_next_line(); 170 | } 171 | } 172 | 173 | self.update_ascii_view(); 174 | self.update_lines(); 175 | self.update_info_line(); 176 | true 177 | } 178 | Key::Arrow(Arrow::Left) | Key::Char('h') => { 179 | if self.cursor_x > 0 { 180 | self.cursor_x -= 1; 181 | if (self.cursor_x + 1) % 3 == 0 { 182 | self.cursor_x -= 1; 183 | } 184 | } 185 | 186 | self.update_ascii_view(); 187 | self.update_lines(); 188 | self.update_info_line(); 189 | true 190 | } 191 | Key::Arrow(Arrow::Right) | Key::Char('l') => { 192 | let next_on_blank = 193 | // add 1 to move to next column 194 | // add 1 to make the index 1-based 195 | (self.cursor_x + 1 + 1) % 3 == 0; 196 | 197 | let total_lines = self.total_lines_needed(); 198 | let last_col_in_line = 199 | // FIXME: This won't work on empty files 200 | if self.cursor_y + 1 == total_lines { 201 | // We're on the last line 202 | (self.last_line_bytes() - 1) * 3 + 2 203 | } else { 204 | self.cols_per_line() 205 | }; 206 | 207 | let potential_next_col = if next_on_blank { 208 | self.cursor_x + 2 209 | } else { 210 | self.cursor_x + 1 211 | }; 212 | 213 | if potential_next_col <= last_col_in_line { 214 | self.cursor_x = potential_next_col; 215 | } 216 | 217 | self.update_ascii_view(); 218 | self.update_lines(); 219 | self.update_info_line(); 220 | true 221 | } 222 | Key::Char('G') => { 223 | self.move_cursor_offset(self.data.len() as i32 - 1); 224 | true 225 | } 226 | Key::Ctrl('d') => { 227 | let current_cursor = self.get_byte_idx(); 228 | let bytes_per_line = self.bytes_per_line(); 229 | 230 | let new_cursor = current_cursor + 10 * bytes_per_line; 231 | let new_cursor = if new_cursor > (self.data.len() as i32) - 1 { 232 | (self.data.len() as i32) - 1 233 | } else { 234 | new_cursor 235 | }; 236 | 237 | self.move_cursor_offset(new_cursor); 238 | true 239 | } 240 | Key::Ctrl('u') => { 241 | let current_cursor = self.get_byte_idx(); 242 | let bytes_per_line = self.bytes_per_line(); 243 | 244 | let new_cursor = current_cursor - 10 * bytes_per_line; 245 | let new_cursor = if new_cursor < 0 { 0 } else { new_cursor }; 246 | 247 | self.move_cursor_offset(new_cursor); 248 | true 249 | } 250 | _ => 251 | false, 252 | } 253 | } 254 | 255 | pub fn update_ascii_view(&self) { 256 | let gui: &mut HexGui = unsafe { &mut *self.gui }; 257 | gui.get_ascii_view().move_cursor_offset(self.get_byte_idx()); 258 | gui.get_info_line().set_text(format!( 259 | "{} - {}: {} (scroll: {})", 260 | self.path, 261 | self.get_row(), 262 | self.get_column(), 263 | self.get_scroll() 264 | )); 265 | } 266 | 267 | pub fn update_lines(&self) { 268 | let gui: &mut HexGui = unsafe { &mut *self.gui }; 269 | gui.get_lines().move_cursor_offset(self.get_byte_idx()); 270 | } 271 | 272 | pub fn update_info_line(&self) { 273 | let gui: &mut HexGui = unsafe { &mut *self.gui }; 274 | gui.get_info_line().set_text(format!( 275 | "{} - {}: {} (scroll: {})", 276 | self.path, 277 | self.get_row(), 278 | self.get_column(), 279 | self.get_scroll() 280 | )); 281 | } 282 | 283 | pub fn draw(&self, tb: &mut Termbox, hl: &[usize], hl_len: usize) { 284 | let cols = self.bytes_per_line(); 285 | let rows = self.height; 286 | 287 | let mut hl_idx = 0; 288 | 289 | 'outer: for row in self.scroll..self.scroll + rows { 290 | for col in 0..cols { 291 | let byte_idx = (row * cols + col) as usize; 292 | if let Some(&byte) = self.data.get(byte_idx) { 293 | let char1: u8 = hex_char(byte >> 4); 294 | let char2: u8 = hex_char(byte & 0b0000_1111); 295 | 296 | let attr_1 = col * 3 == self.cursor_x && row == self.cursor_y; 297 | let attr_2 = col * 3 + 1 == self.cursor_x && row == self.cursor_y; 298 | 299 | let mut highlight = false; 300 | let style = if let Some(&hl_offset) = hl.get(hl_idx) { 301 | if byte_idx >= hl_offset && byte_idx < hl_offset + hl_len { 302 | highlight = true; 303 | colors::HIGHLIGHT 304 | } else { 305 | colors::DEFAULT 306 | } 307 | } else { 308 | colors::DEFAULT 309 | }; 310 | 311 | while hl_idx < hl.len() && hl[hl_idx] + hl_len < byte_idx { 312 | hl_idx += 1; 313 | } 314 | 315 | tb.change_cell( 316 | self.pos_x + col * 3, 317 | self.pos_y + row - self.scroll, 318 | char1 as char, 319 | if attr_1 { 320 | colors::CURSOR_NO_FOCUS.fg 321 | } else { 322 | style.fg 323 | }, 324 | if attr_1 { 325 | colors::CURSOR_NO_FOCUS.bg 326 | } else { 327 | style.bg 328 | }, 329 | ); 330 | 331 | tb.change_cell( 332 | self.pos_x + col * 3 + 1, 333 | self.pos_y + row - self.scroll, 334 | char2 as char, 335 | if attr_2 { 336 | colors::CURSOR_NO_FOCUS.fg 337 | } else { 338 | style.fg 339 | }, 340 | if attr_2 { 341 | colors::CURSOR_NO_FOCUS.bg 342 | } else { 343 | style.bg 344 | }, 345 | ); 346 | 347 | // When highlighting a word, paint the space between bytes too 348 | let highlight = highlight && byte_idx + 1 < hl[hl_idx] + hl_len; 349 | 350 | let space_col = self.pos_x + col * 3 + 2; 351 | if highlight && space_col < self.width - 1 { 352 | tb.change_cell( 353 | space_col, 354 | self.pos_y + row - self.scroll, 355 | ' ', 356 | colors::HIGHLIGHT.fg, 357 | colors::HIGHLIGHT.bg, 358 | ); 359 | } 360 | } else { 361 | // Nothing to draw here, also we can break the loop 362 | break 'outer; 363 | } 364 | } 365 | } 366 | } 367 | 368 | pub fn move_cursor_offset(&mut self, byte_idx: i32) { 369 | let byte_idx = cmp::min((self.data.len() - 1) as i32, byte_idx); 370 | 371 | let bpl = self.bytes_per_line(); 372 | self.cursor_y = byte_idx / bpl; 373 | self.cursor_x = (byte_idx % bpl) * 3; 374 | 375 | let min_scroll = cmp::max(0, self.cursor_y - self.height + 3); 376 | let max_scroll = cmp::max(0, self.cursor_y - 3); 377 | 378 | if self.scroll > max_scroll { 379 | self.scroll = max_scroll; 380 | } else if self.scroll < min_scroll { 381 | self.scroll = min_scroll; 382 | } 383 | 384 | self.update_ascii_view(); 385 | self.update_lines(); 386 | self.update_info_line(); 387 | } 388 | } 389 | -------------------------------------------------------------------------------- /src/gui/hex/info_line.rs: -------------------------------------------------------------------------------- 1 | use utils::*; 2 | use colors; 3 | 4 | use termbox_simple::*; 5 | 6 | pub struct InfoLine { 7 | pos_x: i32, 8 | pos_y: i32, 9 | width: i32, 10 | text: String, 11 | } 12 | 13 | impl InfoLine { 14 | pub fn new(width: i32, pos_x: i32, pos_y: i32, text: String) -> InfoLine { 15 | InfoLine { 16 | pos_x: pos_x, 17 | pos_y: pos_y, 18 | width: width, 19 | text: text, 20 | } 21 | } 22 | 23 | pub fn set_text(&mut self, text: String) { 24 | self.text = text; 25 | } 26 | 27 | pub fn draw(&self, tb: &mut Termbox) { 28 | let fg = colors::STATUS_BAR.fg; 29 | let bg = colors::STATUS_BAR.bg; 30 | 31 | for x in self.pos_x..=self.pos_x + self.width { 32 | tb.change_cell(x, self.pos_y, ' ', fg, bg); 33 | } 34 | 35 | print(tb, self.pos_x, self.pos_y, colors::STATUS_BAR, &self.text); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/gui/hex/lines.rs: -------------------------------------------------------------------------------- 1 | use std::cmp; 2 | 3 | use colors; 4 | use utils::*; 5 | 6 | use termbox_simple::*; 7 | 8 | pub struct Lines { 9 | bytes_per_line: i32, 10 | length: i32, 11 | 12 | width: i32, 13 | height: i32, 14 | 15 | /// Byte offset (aka. address) 16 | cursor: i32, 17 | 18 | scroll: i32, 19 | } 20 | 21 | impl Lines { 22 | pub fn new(bytes_per_line: i32, length: i32, width: i32, height: i32) -> Lines { 23 | Lines { 24 | bytes_per_line: bytes_per_line, 25 | length: length, 26 | width: width, 27 | height: height, 28 | cursor: 0, 29 | scroll: 0, 30 | } 31 | } 32 | 33 | pub fn width(&self) -> i32 { 34 | self.width 35 | } 36 | 37 | pub fn set_scroll(&mut self, scroll: i32) { 38 | self.scroll = scroll; 39 | } 40 | 41 | pub fn draw(&self, tb: &mut Termbox) { 42 | let mut addr_str = String::with_capacity(self.width as usize); 43 | 44 | let start_addr = self.scroll * self.bytes_per_line; 45 | 46 | for line in 0..self.height { 47 | let addr = start_addr + self.bytes_per_line * line; 48 | if addr >= self.length { 49 | break; 50 | } 51 | 52 | self.mk_hex_string(addr, &mut addr_str); 53 | 54 | let highlight = self.cursor >= addr && self.cursor < addr + self.bytes_per_line; 55 | let style = if highlight { 56 | colors::CURSOR_NO_FOCUS 57 | } else { 58 | colors::DEFAULT 59 | }; 60 | 61 | print(tb, 0, line, style, &addr_str); 62 | } 63 | } 64 | 65 | pub fn move_cursor_offset(&mut self, byte_offset: i32) { 66 | self.cursor = byte_offset; 67 | 68 | let mut line = byte_offset / self.bytes_per_line; 69 | if byte_offset % self.bytes_per_line != 0 { 70 | line += 1; 71 | } 72 | 73 | let min_scroll = cmp::max(0, line - self.height + 3); 74 | let max_scroll = cmp::max(0, line - 3); 75 | 76 | if self.scroll > max_scroll { 77 | self.scroll = max_scroll; 78 | } else if self.scroll < min_scroll { 79 | self.scroll = min_scroll; 80 | } 81 | } 82 | 83 | fn mk_hex_string(&self, addr: i32, ret: &mut String) { 84 | ret.clear(); 85 | 86 | // for debugging purposes: 87 | // ret.push_str(format!("{}", addr).borrow()); 88 | 89 | ret.push('0'); 90 | ret.push('x'); 91 | 92 | for i in 0..self.width - 2 + 1 { 93 | let nibble = ((addr >> (4 * (self.width - 2 - i))) & 0b0000_1111) as u8; 94 | ret.push(hex_char(nibble) as char); 95 | } 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /src/gui/hex/mod.rs: -------------------------------------------------------------------------------- 1 | mod ascii_view; 2 | mod goto; 3 | mod hex_grid; 4 | mod info_line; 5 | mod lines; 6 | mod search; 7 | 8 | use colors; 9 | use self::ascii_view::AsciiView; 10 | use self::goto::{GotoOverlay, OverlayRet}; 11 | use self::hex_grid::HexGrid; 12 | use self::info_line::InfoLine; 13 | use self::lines::Lines; 14 | use self::search::{SearchOverlay, SearchRet}; 15 | 16 | use libc; 17 | use nix::poll::{poll, PollFd, POLLIN}; 18 | use term_input::{Event, Input, Key}; 19 | use termbox_simple::*; 20 | 21 | /// GUI is the main thing that owns every widget. It's also responsible for 22 | /// ncurses initialization and finalization. 23 | pub struct HexGui<'gui> { 24 | tb: Termbox, 25 | width: i32, 26 | height: i32, 27 | 28 | hex_grid: HexGrid<'gui>, 29 | lines: Lines, 30 | ascii_view: AsciiView<'gui>, 31 | info_line: InfoLine, 32 | overlay: Overlay<'gui>, 33 | contents: &'gui [u8], 34 | 35 | highlight: Vec, 36 | highlight_len: usize, 37 | 38 | z_pressed: bool, 39 | } 40 | 41 | pub enum Overlay<'overlay> { 42 | NoOverlay, 43 | SearchOverlay(SearchOverlay<'overlay>), 44 | GotoOverlay(GotoOverlay), 45 | } 46 | 47 | struct Layout { 48 | lines_width: i32, 49 | hex_grid_x: i32, 50 | hex_grid_width: i32, 51 | ascii_view_x: i32, 52 | ascii_view_width: i32, 53 | } 54 | 55 | fn layout(w: i32, content_size: usize) -> Layout { 56 | // Calculate cols needed for showing the addresses 57 | let hex_digits_needed = (content_size as f32).log(16.0f32) as i32; 58 | let lines_width_pre = hex_digits_needed + 2; // take 0x prefix into account 59 | let lines_width = if lines_width_pre as f32 > w as f32 / 40.0 * 100.0 { 60 | 0 61 | } else { 62 | lines_width_pre 63 | }; 64 | 65 | // -1 for the vertical line between hex and ascii views 66 | // Another -1 for a vertical line between lines and hex view if we draw lines 67 | let grid_width = w - lines_width - 1 - if lines_width == 0 { 1 } else { 0 }; 68 | 69 | // Every byte takes 3 characters in hex view and 1 character in ascii view. 70 | // So we have this 3/1 ratio. 71 | let unit_column = grid_width / 4; 72 | let hex_grid_width = unit_column * 3; 73 | Layout { 74 | lines_width, 75 | hex_grid_x: lines_width + 1, 76 | hex_grid_width, 77 | ascii_view_x: lines_width + if lines_width == 0 { 0 } else { 1 } + hex_grid_width, 78 | ascii_view_width: unit_column, 79 | } 80 | } 81 | 82 | // WARNING: Moving this after init() will cause a segfault. Not calling init() 83 | // will cause a segfault. 84 | 85 | impl<'gui> HexGui<'gui> { 86 | pub fn new( 87 | tb: Termbox, 88 | contents: &'gui [u8], 89 | path: &'gui str, 90 | width: i32, 91 | height: i32, 92 | ) -> HexGui<'gui> { 93 | let layout = layout(width, contents.len()); 94 | let hex_grid = HexGrid::new( 95 | layout.hex_grid_width, 96 | height - 1, 97 | layout.hex_grid_x, 98 | 0, 99 | contents, 100 | path, 101 | ); 102 | let lines = Lines::new( 103 | hex_grid.bytes_per_line(), 104 | contents.len() as i32, 105 | layout.lines_width, 106 | height, 107 | ); 108 | let ascii_view = AsciiView::new( 109 | layout.ascii_view_width, 110 | height - 1, 111 | layout.ascii_view_x, 112 | 0, 113 | contents, 114 | ); 115 | let info_line = InfoLine::new(width, 0, height - 1, format!("{} - 0: 0", path)); 116 | HexGui { 117 | tb: tb, 118 | width: width, 119 | height: height, 120 | 121 | hex_grid: hex_grid, 122 | lines: lines, 123 | ascii_view: ascii_view, 124 | info_line: info_line, 125 | overlay: Overlay::NoOverlay, 126 | contents: contents, 127 | 128 | highlight: Vec::new(), 129 | highlight_len: 0, 130 | 131 | z_pressed: false, 132 | } 133 | } 134 | 135 | pub fn init(&mut self) { 136 | let self_ptr = self as *mut HexGui; 137 | self.hex_grid.set_gui(self_ptr); 138 | } 139 | 140 | pub fn get_lines(&mut self) -> &mut Lines { 141 | &mut self.lines 142 | } 143 | 144 | pub fn get_ascii_view(&mut self) -> &mut AsciiView<'gui> { 145 | &mut self.ascii_view 146 | } 147 | 148 | pub fn get_info_line(&mut self) -> &mut InfoLine { 149 | &mut self.info_line 150 | } 151 | 152 | pub fn draw(&mut self) { 153 | self.tb.clear(); 154 | 155 | self.lines.draw(&mut self.tb); 156 | 157 | let vsplit_x = self.lines.width(); 158 | for y in 0..self.height - 1 { 159 | self.tb 160 | .change_cell(vsplit_x, y, '│', colors::DEFAULT.fg, colors::DEFAULT.bg); 161 | } 162 | 163 | self.hex_grid 164 | .draw(&mut self.tb, &self.highlight, self.highlight_len); 165 | 166 | let vsplit_x = vsplit_x + self.hex_grid.width(); 167 | for y in 0..self.height - 1 { 168 | self.tb 169 | .change_cell(vsplit_x, y, '│', colors::DEFAULT.fg, colors::DEFAULT.bg); 170 | } 171 | 172 | self.ascii_view 173 | .draw(&mut self.tb, &self.highlight, self.highlight_len); 174 | 175 | self.info_line.draw(&mut self.tb); 176 | 177 | match self.overlay { 178 | Overlay::NoOverlay => 179 | {} 180 | Overlay::SearchOverlay(ref o) => 181 | o.draw(&mut self.tb), 182 | Overlay::GotoOverlay(ref o) => 183 | o.draw(&mut self.tb), 184 | } 185 | 186 | self.tb.present(); 187 | } 188 | 189 | pub fn mainloop(&mut self) { 190 | let mut input = Input::new(); 191 | let mut evs = Vec::with_capacity(10); 192 | self.draw(); 193 | 194 | loop { 195 | let mut fds = [PollFd::new(libc::STDIN_FILENO, POLLIN)]; 196 | let _ = poll(&mut fds, -1); 197 | 198 | input.read_input_events(&mut evs); 199 | 200 | let mut brk = false; 201 | for ev in evs.drain(..) { 202 | brk |= self.handle_event(ev); 203 | } 204 | if brk { 205 | break; 206 | } 207 | self.draw(); 208 | } 209 | } 210 | 211 | fn handle_event(&mut self, ev: Event) -> bool { 212 | match ev { 213 | Event::Key(key) => 214 | self.keypressed(key), 215 | Event::String(_) | 216 | Event::Resize | 217 | Event::FocusGained | 218 | Event::FocusLost | 219 | Event::Unknown(_) => 220 | false, 221 | } 222 | } 223 | 224 | fn keypressed(&mut self, key: Key) -> bool { 225 | let mut reset_overlay = false; 226 | match self.overlay { 227 | Overlay::NoOverlay => { 228 | if key == Key::Char('q') { 229 | return true; 230 | } 231 | self.keypressed_no_overlay(key) 232 | } 233 | 234 | Overlay::GotoOverlay(ref mut o) => 235 | match o.keypressed(key) { 236 | OverlayRet::Ret(offset) => { 237 | self.hex_grid.move_cursor_offset(offset); 238 | reset_overlay = true; 239 | } 240 | OverlayRet::GotoBeginning => { 241 | self.hex_grid.move_cursor_offset(0); 242 | reset_overlay = true; 243 | } 244 | OverlayRet::Continue => 245 | {} 246 | OverlayRet::Abort => { 247 | reset_overlay = true; 248 | } 249 | }, 250 | 251 | Overlay::SearchOverlay(ref mut o) => { 252 | match o.keypressed(key) { 253 | SearchRet::Highlight { 254 | all_bytes: bs, 255 | len: l, 256 | .. 257 | } => { 258 | self.highlight = bs; 259 | self.highlight_len = l; 260 | reset_overlay = true; 261 | } 262 | SearchRet::Abort => { 263 | reset_overlay = true; 264 | } 265 | SearchRet::Continue => 266 | { /* nothing to do */ } 267 | } 268 | } 269 | }; 270 | 271 | if reset_overlay { 272 | self.overlay = Overlay::NoOverlay; 273 | } 274 | 275 | false 276 | } 277 | 278 | fn keypressed_no_overlay(&mut self, key: Key) { 279 | match key { 280 | Key::Char('g') => { 281 | self.z_pressed = false; 282 | self.mk_goto_overlay(); 283 | } 284 | Key::Char('/') => { 285 | self.z_pressed = false; 286 | self.mk_search_overlay(); 287 | } 288 | Key::Char('z') => 289 | if self.z_pressed { 290 | self.hex_grid.try_center_scroll(); 291 | self.lines.set_scroll(self.hex_grid.get_scroll()); 292 | self.ascii_view.set_scroll(self.hex_grid.get_scroll()); 293 | self.z_pressed = false; 294 | } else { 295 | self.z_pressed = true; 296 | }, 297 | Key::Char('n') => { 298 | self.z_pressed = false; 299 | let hls = &self.highlight; 300 | let byte_idx = self.hex_grid.get_byte_idx() as usize; 301 | for &hl_offset in hls { 302 | if hl_offset > byte_idx { 303 | self.hex_grid.move_cursor_offset(hl_offset as i32); 304 | return; 305 | } 306 | } 307 | // We couldn't jump to a match, start from the beginning 308 | if let Some(&hl_offset) = hls.get(0) { 309 | self.hex_grid.move_cursor_offset(hl_offset as i32); 310 | } 311 | } 312 | Key::Char('N') => { 313 | self.z_pressed = false; 314 | let hls = &self.highlight; 315 | let byte_idx = self.hex_grid.get_byte_idx() as usize; 316 | for &hl_offset in hls.iter().rev() { 317 | if hl_offset < byte_idx { 318 | self.hex_grid.move_cursor_offset(hl_offset as i32); 319 | return; 320 | } 321 | } 322 | // We couldn't jump to a match, start from the beginning 323 | if let Some(&hl_offset) = hls.get(hls.len() - 1) { 324 | self.hex_grid.move_cursor_offset(hl_offset as i32); 325 | } 326 | } 327 | _ => { 328 | self.z_pressed = false; 329 | self.hex_grid.keypressed(key); 330 | } 331 | } 332 | } 333 | 334 | fn mk_goto_overlay(&mut self) { 335 | self.overlay = Overlay::GotoOverlay(GotoOverlay::new( 336 | self.width / 2, 337 | self.height / 2, 338 | self.width / 4, 339 | self.height / 4, 340 | )); 341 | } 342 | 343 | fn mk_search_overlay(&mut self) { 344 | self.overlay = Overlay::SearchOverlay(SearchOverlay::new( 345 | self.width / 2, 346 | self.height / 2, 347 | self.width / 4, 348 | self.height / 4, 349 | self.contents, 350 | )); 351 | } 352 | } 353 | -------------------------------------------------------------------------------- /src/gui/hex/search.rs: -------------------------------------------------------------------------------- 1 | use std::cmp; 2 | 3 | use colors; 4 | use utils::*; 5 | 6 | use term_input::Key; 7 | use termbox_simple::*; 8 | 9 | pub enum SearchRet { 10 | /// Highlight these bytes. 11 | Highlight { 12 | /// Byte in focus. 13 | focus: usize, 14 | 15 | /// All matching byte offsets. 16 | all_bytes: Vec, 17 | 18 | /// Length of searched bytes. 19 | len: usize, 20 | }, 21 | 22 | /// User cancelled. 23 | Abort, 24 | 25 | /// Carry on. 26 | Continue, 27 | } 28 | 29 | enum SearchMode { 30 | Ascii, 31 | Hex, 32 | } 33 | 34 | enum NibbleCursor { 35 | /// More significant part 36 | MS, 37 | /// Less significant part 38 | LS, 39 | } 40 | 41 | pub struct SearchOverlay<'overlay> { 42 | pos_x: i32, 43 | pos_y: i32, 44 | width: i32, 45 | height: i32, 46 | 47 | // TODO: rename this to maybe something like 'focus' 48 | mode: SearchMode, 49 | buffer: Vec, 50 | 51 | /// Byte offset in buffer. 52 | byte_cursor: usize, 53 | nibble_cursor: NibbleCursor, 54 | 55 | contents: &'overlay [u8], 56 | } 57 | 58 | impl<'overlay> SearchOverlay<'overlay> { 59 | pub fn new( 60 | width: i32, 61 | height: i32, 62 | pos_x: i32, 63 | pos_y: i32, 64 | contents: &'overlay [u8], 65 | ) -> SearchOverlay<'overlay> { 66 | let width_ = cmp::min(width, 50); 67 | let height_ = cmp::min(height, 10); 68 | 69 | let pos_x = pos_x + (width - width_) / 2; 70 | let pos_y = pos_y + (height - height_) / 2; 71 | 72 | SearchOverlay { 73 | pos_x, 74 | pos_y, 75 | width: width_, 76 | height: height_, 77 | 78 | mode: SearchMode::Ascii, 79 | buffer: Vec::new(), 80 | byte_cursor: 0, 81 | nibble_cursor: NibbleCursor::MS, 82 | 83 | contents: contents, 84 | } 85 | } 86 | 87 | pub fn draw(&self, tb: &mut Termbox) { 88 | draw_box(tb, self.pos_x, self.pos_y, self.width, self.height); 89 | tb.change_cell( 90 | self.pos_x + self.width / 2, 91 | self.pos_y, 92 | '┬', 93 | colors::DEFAULT.fg, 94 | colors::DEFAULT.bg, 95 | ); 96 | for y in 1..self.height - 1 { 97 | tb.change_cell( 98 | self.pos_x + self.width / 2, 99 | self.pos_y + y, 100 | '│', 101 | colors::DEFAULT.fg, 102 | colors::DEFAULT.bg, 103 | ); 104 | } 105 | tb.change_cell( 106 | self.pos_x + self.width / 2, 107 | self.pos_y + self.height - 1, 108 | '┴', 109 | colors::DEFAULT.fg, 110 | colors::DEFAULT.bg, 111 | ); 112 | 113 | self.draw_hex(tb); 114 | self.draw_ascii(tb); 115 | } 116 | 117 | 118 | fn draw_ascii(&self, tb: &mut Termbox) { 119 | // Not the most efficient way to draw, but be fine at this scale 120 | // (e.g. for a couple of characters at most) 121 | let width = ((self.width - 1) / 2) as usize; 122 | for (byte_offset, byte) in self.buffer.iter().enumerate() { 123 | let pos_x = ((byte_offset % width) + 1) as i32; 124 | let pos_y = ((byte_offset / width) + 1) as i32; 125 | 126 | 127 | tb.change_cell( 128 | self.pos_x + pos_x, 129 | self.pos_y + pos_y, 130 | *byte as char, 131 | colors::DEFAULT.fg, 132 | colors::DEFAULT.bg, 133 | ); 134 | } 135 | 136 | // Draw cursor 137 | let cursor_x = (self.byte_cursor % width) + 1; 138 | let cursor_y = self.byte_cursor / width; 139 | 140 | let byte = if self.byte_cursor >= self.buffer.len() { 141 | b' ' 142 | } else { 143 | self.buffer[self.byte_cursor] 144 | }; 145 | 146 | let cursor_style = match self.mode { 147 | SearchMode::Ascii => 148 | colors::CURSOR_FOCUS, 149 | SearchMode::Hex => 150 | colors::CURSOR_NO_FOCUS, 151 | }; 152 | 153 | tb.change_cell( 154 | self.pos_x + cursor_x as i32, 155 | self.pos_y + cursor_y as i32 + 1, 156 | byte as char, 157 | cursor_style.fg, 158 | cursor_style.bg, 159 | ); 160 | } 161 | 162 | fn draw_hex(&self, tb: &mut Termbox) { 163 | // Ideally we could reuse some of the code from HexGrid, but the code 164 | // here should be very simple as we don't have to deal with scrolling, 165 | // jumping around etc. 166 | let start_column = self.width / 2; 167 | let width = self.width / 2 - 1; 168 | 169 | // We skip first row and column as it's occupied by the window border 170 | let mut col = 1; 171 | let mut row = 1; 172 | 173 | for byte in &self.buffer { 174 | if col + 1 >= width { 175 | col = 1; 176 | row += 1; 177 | } 178 | 179 | let nibble1 = hex_char(*byte >> 4); 180 | let nibble2 = hex_char(*byte & 0b0000_1111); 181 | 182 | tb.change_cell( 183 | self.pos_x + start_column + col, 184 | self.pos_y + row, 185 | nibble1 as char, 186 | colors::DEFAULT.fg, 187 | colors::DEFAULT.bg, 188 | ); 189 | tb.change_cell( 190 | self.pos_x + start_column + col + 1, 191 | self.pos_y + row, 192 | nibble2 as char, 193 | colors::DEFAULT.fg, 194 | colors::DEFAULT.bg, 195 | ); 196 | 197 | col += 3; 198 | } 199 | 200 | // Draw cursor 201 | let bytes_per_line = width / 3; 202 | 203 | let cursor_x_byte = self.byte_cursor as i32 % bytes_per_line; 204 | let cursor_x = cursor_x_byte * 3 + 1; 205 | let cursor_x = match self.nibble_cursor { 206 | NibbleCursor::MS => 207 | cursor_x, 208 | NibbleCursor::LS => 209 | cursor_x + 1, 210 | }; 211 | let cursor_y = self.byte_cursor as i32 / bytes_per_line; 212 | 213 | let byte = if self.byte_cursor >= self.buffer.len() { 214 | b' ' 215 | } else { 216 | match self.nibble_cursor { 217 | NibbleCursor::MS => 218 | hex_char(self.buffer[self.byte_cursor] >> 4), 219 | NibbleCursor::LS => 220 | hex_char(self.buffer[self.byte_cursor] & 0b0000_1111), 221 | } 222 | }; 223 | 224 | let cursor_style = match self.mode { 225 | SearchMode::Hex => 226 | colors::CURSOR_FOCUS, 227 | SearchMode::Ascii => 228 | colors::CURSOR_NO_FOCUS, 229 | }; 230 | 231 | tb.change_cell( 232 | self.pos_x + start_column + cursor_x, 233 | self.pos_y + cursor_y + 1, 234 | byte as char, 235 | cursor_style.fg, 236 | cursor_style.bg, 237 | ); 238 | } 239 | 240 | pub fn keypressed(&mut self, key: Key) -> SearchRet { 241 | // TODO: We should be able to move cursor and insert at the cursor 242 | // position. 243 | 244 | match key { 245 | Key::Esc => { 246 | return SearchRet::Abort; 247 | } 248 | Key::Char('\r') => { 249 | if !self.buffer.is_empty() { 250 | // do the search 251 | let offsets = self.find_offsets(); 252 | return SearchRet::Highlight { 253 | focus: self.byte_cursor, 254 | all_bytes: offsets, 255 | len: self.buffer.len(), 256 | }; 257 | } 258 | } 259 | Key::Tab => { 260 | let new_sm = match self.mode { 261 | SearchMode::Ascii => 262 | SearchMode::Hex, 263 | SearchMode::Hex => 264 | SearchMode::Ascii, 265 | }; 266 | self.mode = new_sm; 267 | } 268 | Key::Backspace => 269 | match self.mode { 270 | SearchMode::Ascii => 271 | match self.buffer.pop() { 272 | None => 273 | {} 274 | Some(_) => 275 | if self.byte_cursor != 0 { 276 | self.byte_cursor -= 1; 277 | }, 278 | }, 279 | SearchMode::Hex => 280 | match self.nibble_cursor { 281 | NibbleCursor::LS => { 282 | let byte = self.buffer[self.byte_cursor]; 283 | self.buffer[self.byte_cursor] = byte & 0b1111_0000; 284 | self.nibble_cursor = NibbleCursor::MS; 285 | } 286 | NibbleCursor::MS => 287 | if self.byte_cursor >= self.buffer.len() && self.byte_cursor != 0 { 288 | self.byte_cursor -= 1; 289 | self.nibble_cursor = NibbleCursor::LS; 290 | } else { 291 | match self.buffer.pop() { 292 | None => { 293 | self.nibble_cursor = NibbleCursor::MS; 294 | } 295 | Some(_) => 296 | if self.byte_cursor != 0 { 297 | self.byte_cursor -= 1; 298 | self.nibble_cursor = NibbleCursor::LS; 299 | } else { 300 | self.nibble_cursor = NibbleCursor::MS; 301 | }, 302 | } 303 | }, 304 | }, 305 | }, 306 | Key::Char(ch) => { 307 | // FIXME non-ascii chars 308 | let ch = ch as u32; 309 | match self.mode { 310 | SearchMode::Ascii => 311 | if ch <= 0xFF { 312 | self.buffer.push(ch as u8); 313 | self.byte_cursor += 1; 314 | self.nibble_cursor = NibbleCursor::MS; 315 | }, 316 | SearchMode::Hex => { 317 | let nibble = match ch { 318 | 65...70 => { 319 | // A ... F 320 | Some((ch - 65 + 10) as u8) 321 | } 322 | 97...102 => { 323 | // a ... f 324 | Some((ch - 97 + 10) as u8) 325 | } 326 | 48...57 => { 327 | // 0 ... 9 328 | Some((ch - 48) as u8) 329 | } 330 | _ => 331 | None, 332 | }; 333 | 334 | if let Some(nibble) = nibble { 335 | let current_byte = if self.byte_cursor >= self.buffer.len() { 336 | 0 337 | } else { 338 | self.buffer[self.byte_cursor] 339 | }; 340 | 341 | let new_byte = match self.nibble_cursor { 342 | NibbleCursor::MS => 343 | (current_byte & 0b0000_1111) | (nibble << 4), 344 | NibbleCursor::LS => 345 | (current_byte & 0b1111_0000) | nibble, 346 | }; 347 | 348 | if self.byte_cursor >= self.buffer.len() { 349 | self.buffer.push(new_byte); 350 | self.nibble_cursor = NibbleCursor::LS; 351 | } else { 352 | self.buffer[self.byte_cursor] = new_byte; 353 | 354 | match self.nibble_cursor { 355 | NibbleCursor::MS => 356 | self.nibble_cursor = NibbleCursor::LS, 357 | NibbleCursor::LS => { 358 | self.nibble_cursor = NibbleCursor::MS; 359 | self.byte_cursor += 1; 360 | } 361 | } 362 | } 363 | } 364 | } 365 | } 366 | } 367 | _ => 368 | {} 369 | } 370 | 371 | SearchRet::Continue 372 | } 373 | 374 | fn find_offsets(&self) -> Vec { 375 | let mut ret = Vec::new(); 376 | 377 | let first_byte = self.buffer[0]; 378 | 379 | // It seems like Vec API doesn't help us here. As a first 380 | // implementation, I do a O(n * k) search here. 381 | let mut byte_offset = 0; 382 | while byte_offset < self.contents.len() { 383 | let byte = unsafe { *self.contents.get_unchecked(byte_offset) }; 384 | if byte == first_byte && try_match(&self.contents[byte_offset + 1..], &self.buffer[1..]) 385 | { 386 | ret.push(byte_offset); 387 | byte_offset += self.buffer.len(); 388 | continue; 389 | } 390 | 391 | byte_offset += 1; 392 | } 393 | 394 | // writeln!(&mut ::std::io::stderr(), "find_offsets: {:?}", ret); 395 | ret 396 | } 397 | } 398 | 399 | fn try_match(s1: &[u8], s2: &[u8]) -> bool { 400 | if s2.len() > s1.len() { 401 | false 402 | } else { 403 | for (byte1, byte2) in s1.iter().zip(s2.iter()) { 404 | if byte1 != byte2 { 405 | return false; 406 | } 407 | } 408 | 409 | true 410 | } 411 | } 412 | -------------------------------------------------------------------------------- /src/gui/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod hex; 2 | 3 | use termbox_simple::*; 4 | 5 | pub struct Gui<'gui> { 6 | hex_gui: hex::HexGui<'gui>, 7 | } 8 | 9 | impl<'gui> Gui<'gui> { 10 | pub fn new_hex_gui( 11 | tb: Termbox, 12 | contents: &'gui [u8], 13 | path: &'gui str, 14 | width: i32, 15 | height: i32, 16 | ) -> Gui<'gui> { 17 | Gui { 18 | hex_gui: hex::HexGui::new(tb, contents, path, width, height), 19 | } 20 | } 21 | 22 | pub fn mainloop(&mut self) { 23 | self.hex_gui.init(); 24 | self.hex_gui.mainloop(); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | #![feature(alloc_system)] 2 | #![feature(allocator_api)] 3 | 4 | extern crate alloc_system; 5 | 6 | #[global_allocator] 7 | static ALLOC: alloc_system::System = alloc_system::System; 8 | 9 | extern crate libc; 10 | extern crate nix; 11 | extern crate term_input; 12 | extern crate termbox_simple; 13 | 14 | mod colors; 15 | mod gui; 16 | mod utils; 17 | 18 | use std::env::args_os; 19 | use std::ffi::OsString; 20 | use std::fs::File; 21 | use std::io::Read; 22 | use std::path::Path; 23 | 24 | use gui::Gui; 25 | 26 | use termbox_simple::*; 27 | 28 | fn main() { 29 | let args: Vec = args_os().collect(); 30 | if args.len() != 2 { 31 | panic!("USAGE: rhex "); 32 | } 33 | 34 | let path = Path::new(&args[1]); 35 | let contents = match File::open(path) { 36 | Err(err) => 37 | panic!("Can't read file {:?}: {}", path, err), 38 | Ok(mut file) => { 39 | let mut ret = Vec::new(); 40 | file.read_to_end(&mut ret).unwrap(); 41 | ret 42 | } 43 | }; 44 | 45 | let mut tb = Termbox::init().unwrap(); 46 | tb.set_output_mode(OutputMode::Output256); 47 | tb.set_clear_attributes(TB_DEFAULT, TB_DEFAULT); 48 | 49 | let scr_x = tb.width(); 50 | let scr_y = tb.height(); 51 | 52 | let mut gui = Gui::new_hex_gui(tb, &contents, path.to_str().unwrap(), scr_x, scr_y); 53 | gui.mainloop(); 54 | } 55 | -------------------------------------------------------------------------------- /src/utils.rs: -------------------------------------------------------------------------------- 1 | //////////////////////////////////////////////////////////////////////////////// 2 | // Utilities 3 | //////////////////////////////////////////////////////////////////////////////// 4 | 5 | #[inline] 6 | pub fn hex_char(nibble: u8) -> u8 { 7 | if nibble < 10 { 8 | 48 + nibble 9 | } else { 10 | 97 + nibble - 10 11 | } 12 | } 13 | 14 | use colors::Style; 15 | use colors; 16 | use termbox_simple::*; 17 | 18 | pub fn draw_box(tb: &mut Termbox, pos_x: i32, pos_y: i32, width: i32, height: i32) { 19 | let fg = colors::DEFAULT.fg; 20 | let bg = colors::DEFAULT.bg; 21 | 22 | for x in 1..width - 1 { 23 | tb.change_cell(pos_x + x, pos_y, '─', fg, bg); 24 | tb.change_cell(pos_x + x, pos_y + height - 1, '─', fg, bg); 25 | } 26 | 27 | for y in 1..height - 1 { 28 | tb.change_cell(pos_x, pos_y + y, '│', fg, bg); 29 | tb.change_cell(pos_x + width - 1, pos_y + y, '│', fg, bg); 30 | } 31 | 32 | tb.change_cell(pos_x, pos_y, '┌', fg, bg); 33 | tb.change_cell(pos_x + width - 1, pos_y, '┐', fg, bg); 34 | tb.change_cell(pos_x, pos_y + height - 1, '└', fg, bg); 35 | tb.change_cell(pos_x + width - 1, pos_y + height - 1, '┘', fg, bg); 36 | 37 | for x in 1..width - 1 { 38 | for y in 1..height - 1 { 39 | tb.change_cell(pos_x + x, pos_y + y, ' ', fg, bg); 40 | } 41 | } 42 | } 43 | 44 | 45 | pub fn print(tb: &mut Termbox, mut pos_x: i32, pos_y: i32, style: Style, str: &str) { 46 | for char in str.chars() { 47 | tb.change_cell(pos_x, pos_y, char, style.fg, style.bg); 48 | pos_x += 1; 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /ss.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/osa1/rhex/3360577826deaf0dcdd72a92e5210fc8267efb83/ss.png --------------------------------------------------------------------------------