diff --git a/README.md b/README.md index 0e50640..1e16e01 100644 --- a/README.md +++ b/README.md @@ -201,6 +201,7 @@ kibi --version # Print version information and exit | Ctrl-C | Copies the entire line | | Ctrl-X | Cuts the entire line | | Ctrl-V | Will paste the copied line | +| Ctrl-P | Show/Hide nonprintable characters | ### Configuration @@ -218,6 +219,8 @@ quit_times=2 message_duration=3 # Whether to show line numbers. show_line_numbers=true +# Whether to show nonprintable characters +non_printable=false ``` The location of these files is described below. diff --git a/config_example.ini b/config_example.ini index f21b43e..633a243 100644 --- a/config_example.ini +++ b/config_example.ini @@ -10,3 +10,6 @@ message_duration = 3 # Whether to display line numbers. show_line_numbers = true + +# Whether to show nonprintable characters +non_printable = false diff --git a/src/config.rs b/src/config.rs index a687457..6d079bb 100644 --- a/src/config.rs +++ b/src/config.rs @@ -20,12 +20,20 @@ pub struct Config { pub message_dur: Duration, /// Whether to display line numbers. pub show_line_num: bool, + /// Whether to display nonprintable characters. + pub non_printable: bool, } impl Default for Config { /// Default configuration. fn default() -> Self { - Self { tab_stop: 4, quit_times: 2, message_dur: Duration::new(3, 0), show_line_num: true } + Self { + tab_stop: 4, + quit_times: 2, + message_dur: Duration::new(3, 0), + show_line_num: true, + non_printable: false, + } } } diff --git a/src/editor.rs b/src/editor.rs index e9cc605..a9acbac 100644 --- a/src/editor.rs +++ b/src/editor.rs @@ -21,6 +21,7 @@ const PASTE: u8 = ctrl_key(b'V'); const DUPLICATE: u8 = ctrl_key(b'D'); const EXECUTE: u8 = ctrl_key(b'E'); const REMOVE_LINE: u8 = ctrl_key(b'R'); +const NON_PRINTABLE: u8 = ctrl_key(b'P'); const BACKSPACE: u8 = 127; const HELP_MESSAGE: &str = @@ -122,6 +123,8 @@ pub struct Editor { orig_term_mode: Option, /// The copied buffer of a row copied_row: Vec, + /// Whether to display nonprintable characters. + non_printable: bool, } /// Describes a status message, shown at the bottom at the screen. @@ -317,7 +320,7 @@ impl Editor { let mut hl_state = if y > 0 { self.rows[y - 1].hl_state } else { HlState::Normal }; for row in self.rows.iter_mut().skip(y) { let previous_hl_state = row.hl_state; - hl_state = row.update(&self.syntax, hl_state, self.config.tab_stop); + hl_state = row.update(&self.syntax, self.non_printable, hl_state, self.config.tab_stop); if ignore_following_rows || hl_state == previous_hl_state { return; } @@ -330,7 +333,7 @@ impl Editor { fn update_all_rows(&mut self) { let mut hl_state = HlState::Normal; for row in &mut self.rows { - hl_state = row.update(&self.syntax, hl_state, self.config.tab_stop); + hl_state = row.update(&self.syntax, self.non_printable, hl_state, self.config.tab_stop); } } @@ -529,7 +532,7 @@ impl Editor { if let Some(row) = row { // Draw a row of text self.draw_left_padding(buffer, i + 1)?; - row.draw(self.cursor.coff, self.screen_cols, buffer)?; + row.draw(self.cursor.coff, self.non_printable, self.screen_cols, buffer)?; } else { // Draw an empty row self.draw_left_padding(buffer, '~')?; @@ -645,6 +648,10 @@ impl Editor { } Key::Char(COPY) => self.copy_current_row(), Key::Char(PASTE) => self.paste_current_row(), + Key::Char(NON_PRINTABLE) => { + self.non_printable = !self.non_printable; + self.update_all_rows(); + } Key::Char(EXECUTE) => prompt_mode = Some(PromptMode::Execute(String::new())), Key::Char(c) => self.insert_byte(*c), } diff --git a/src/row.rs b/src/row.rs index f1decd0..970c4ae 100644 --- a/src/row.rs +++ b/src/row.rs @@ -52,7 +52,9 @@ impl Row { // TODO: Combine update and update_syntax /// Update the row: convert tabs into spaces and compute highlight symbols /// The `hl_state` argument is the `HLState` for the previous row. - pub fn update(&mut self, syntax: &SyntaxConf, hl_state: HlState, tab: usize) -> HlState { + pub fn update( + &mut self, syntax: &SyntaxConf, non_printable: bool, hl_state: HlState, tab: usize, + ) -> HlState { self.render.clear(); self.cx2rx.clear(); self.rx2cx.clear(); @@ -62,7 +64,23 @@ impl Row { let n_bytes = c.len_utf8(); // The number of rendered characters let n_rend_chars = if c == '\t' { tab - (rx % tab) } else { c.width().unwrap_or(1) }; - self.render.push_str(&(if c == '\t' { " ".repeat(n_rend_chars) } else { c.into() })); + self.render.push_str( + &(if c == '\t' && non_printable { + let mut str; + if n_rend_chars > 1 { + str = String::from('├'); + str.push_str(&"─".repeat(n_rend_chars - 2)); + str.push('┤'); + } else { + str = String::from('─'); + } + str + } else if c == '\t' { + " ".repeat(n_rend_chars) + } else { + c.into() + }), + ); self.cx2rx.extend(std::iter::repeat(rx).take(n_bytes)); self.rx2cx.extend(std::iter::repeat(cx).take(n_rend_chars)); rx += n_rend_chars; @@ -176,13 +194,35 @@ impl Row { /// Draw the row and write the result to a buffer. An `offset` can be given, as well as a limit /// on the length of the row (`max_len`). After writing the characters, clear the rest of the /// line and move the cursor to the start of the next line. - pub fn draw(&self, offset: usize, max_len: usize, buffer: &mut String) -> Result<(), Error> { + pub fn draw( + &self, offset: usize, non_printable: bool, max_len: usize, buffer: &mut String, + ) -> Result<(), Error> { let mut current_hl_type = HlType::Normal; let chars = self.render.chars().skip(offset).take(max_len); let mut rx = self.render.chars().take(offset).map(|c| c.width().unwrap_or(1)).sum(); + let mut rendered_char; for (c, mut hl_type) in chars.zip(self.hl.iter().skip(offset)) { if c.is_ascii_control() { - let rendered_char = if (c as u8) <= 26 { (b'@' + c as u8) as char } else { '?' }; + if !non_printable { + if (c as u8) <= 26 { + rendered_char = (b'@' + c as u8) as char + } else { + rendered_char = '?' + } + } else { + match c { + '\x0D' => rendered_char = '␍', + // null + '\x00' => rendered_char = '␀', + // bell + '\x07' => rendered_char = '␇', + // backspace + '\x08' => rendered_char = '␈', + // escape + '\x1B' => rendered_char = '␛', + _ => rendered_char = '?', + } + }; write!(buffer, "{REVERSE_VIDEO}{rendered_char}{RESET_FMT}")?; // Restore previous color if current_hl_type != HlType::Normal { @@ -202,10 +242,18 @@ impl Row { buffer.push_str(&hl_type.to_string()); current_hl_type = *hl_type; } - buffer.push(c); + if c.is_ascii_whitespace() && non_printable { + rendered_char = '·'; + } else { + rendered_char = c; + } + buffer.push(rendered_char); } rx += c.width().unwrap_or(1); } + if non_printable { + buffer.push('␊') + } buffer.push_str(RESET_FMT); Ok(()) }