diff --git a/CHANGELOG.md b/CHANGELOG.md index 048f545..f88c8c6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,7 @@ Format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). ## [Unreleased] ### Added +- SGR double underline (attribute 21), cleared by SGR 24 along with single underline - add `--maximized` and `--fullscreen` flags to start the window in that mode ### Changed diff --git a/src/renderer/draw_fns.rs b/src/renderer/draw_fns.rs index c24998b..89bc6eb 100644 --- a/src/renderer/draw_fns.rs +++ b/src/renderer/draw_fns.rs @@ -20,6 +20,7 @@ pub(super) static BLANK_CELL: Cell = Cell { overline: false, reverse: false, blink: false, + double_underline: false, wide: false, wide_cont: false, url: None, diff --git a/src/renderer/text.rs b/src/renderer/text.rs index acf5825..4695a42 100644 --- a/src/renderer/text.rs +++ b/src/renderer/text.rs @@ -1018,27 +1018,18 @@ fn draw_cell_decorations( dim_color(color_u32(fg), dim_factor) }; + let hline = + |buf: &mut [u32], y: u32| draw_clipped_hline(buf, buf_width, y, cell_x, draw_w, clip, fg32); if cell.underline { - draw_clipped_hline( - buf, - buf_width, - cell_y + m.cell_height.saturating_sub(2), - cell_x, - draw_w, - clip, - fg32, - ); + hline(buf, cell_y + m.cell_height.saturating_sub(2)); + } + if cell.double_underline { + let base = cell_y + m.cell_height.saturating_sub(1); + hline(buf, base); + hline(buf, base.saturating_sub(2)); } if cell.strikethrough { - draw_clipped_hline( - buf, - buf_width, - cell_y + m.cell_height / 2, - cell_x, - draw_w, - clip, - fg32, - ); + hline(buf, cell_y + m.cell_height / 2); } if cell.overline { draw_clipped_hline(buf, buf_width, cell_y, cell_x, draw_w, clip, fg32); diff --git a/src/renderer/text_test.rs b/src/renderer/text_test.rs index 1d7213a..bb6c731 100644 --- a/src/renderer/text_test.rs +++ b/src/renderer/text_test.rs @@ -756,6 +756,18 @@ fn draw_pane_with_underline_cell_does_not_panic() { do_draw(&mut r, &[pane], &InputMode::Insert); } +#[test] +fn draw_pane_with_double_underline_cell_does_not_panic() { + let mut r = make_renderer(); + let m = r.make_metrics(Physical(16.0)); + let (cols, rows) = m.grid_size_for(800, 600u32.saturating_sub(44)); + let mut grid = make_grid(cols, rows); + grid.write_char('D'); + grid.cell_mut(0, 0).double_underline = true; + let pane = make_pane(&grid, &m); + do_draw(&mut r, &[pane], &InputMode::Insert); +} + #[test] fn draw_pane_osc8_link_without_hover_does_not_panic() { let mut r = make_renderer(); diff --git a/src/terminal/grid.rs b/src/terminal/grid.rs index a669ce4..1e3f848 100644 --- a/src/terminal/grid.rs +++ b/src/terminal/grid.rs @@ -66,6 +66,7 @@ pub struct Cell { pub overline: bool, pub reverse: bool, pub blink: bool, + pub double_underline: bool, /// True when this is the left half of a 2-column wide character. pub wide: bool, /// True when this is the right (placeholder) half of a wide character. @@ -88,6 +89,7 @@ impl Default for Cell { overline: false, reverse: false, blink: false, + double_underline: false, wide: false, wide_cont: false, url: None, @@ -108,6 +110,7 @@ fn cell_with_colors(c: char, fg: Color, bg: Color) -> Cell { overline: false, reverse: false, blink: false, + double_underline: false, wide: false, wide_cont: false, url: None, @@ -127,6 +130,7 @@ struct SavedCursor { overline: bool, reverse: bool, blink: bool, + double_underline: bool, } struct SavedScreen { @@ -148,6 +152,7 @@ struct SavedScreen { overline: bool, reverse: bool, blink: bool, + double_underline: bool, scrollback: VecDeque>, scrollback_wrapped: VecDeque, row_wrapped: Vec, @@ -173,6 +178,7 @@ pub struct Grid { pub overline: bool, pub reverse: bool, pub blink: bool, + pub double_underline: bool, // DECSC/DECRC saved cursor (ESC 7 / ESC 8) decsc: Option, // Scrollback: lines that have scrolled off the top (oldest first) @@ -268,6 +274,7 @@ impl Grid { overline: false, reverse: false, blink: false, + double_underline: false, decsc: None, scrollback: VecDeque::new(), scrollback_wrapped: VecDeque::new(), @@ -326,6 +333,7 @@ impl Grid { overline: self.overline, reverse: self.reverse, blink: self.blink, + double_underline: self.double_underline, scrollback: std::mem::take(&mut self.scrollback), scrollback_wrapped: std::mem::take(&mut self.scrollback_wrapped), row_wrapped: std::mem::replace(&mut self.row_wrapped, vec![false; self.rows]), @@ -375,6 +383,7 @@ impl Grid { self.overline = saved.overline; self.reverse = saved.reverse; self.blink = saved.blink; + self.double_underline = saved.double_underline; self.scrollback = saved.scrollback; self.scrollback_wrapped = saved.scrollback_wrapped; self.row_wrapped = saved.row_wrapped; @@ -638,6 +647,7 @@ impl Grid { overline: self.overline, reverse: self.reverse, blink: self.blink, + double_underline: self.double_underline, wide, url: self.current_url.clone(), ..cell_with_colors(c, self.fg, self.bg) @@ -974,6 +984,7 @@ impl Grid { self.overline = false; self.reverse = false; self.blink = false; + self.double_underline = false; } pub fn save_cursor(&mut self) { @@ -990,6 +1001,7 @@ impl Grid { overline: self.overline, reverse: self.reverse, blink: self.blink, + double_underline: self.double_underline, }); } @@ -1007,6 +1019,7 @@ impl Grid { self.overline = s.overline; self.reverse = s.reverse; self.blink = s.blink; + self.double_underline = s.double_underline; } } diff --git a/src/terminal/parser.rs b/src/terminal/parser.rs index 2046d95..d95ce21 100644 --- a/src/terminal/parser.rs +++ b/src/terminal/parser.rs @@ -66,27 +66,19 @@ struct Performer<'a> { impl Performer<'_> { fn handle_dec_private_modes(&mut self, action: char, p0: u16) { + // Only DECSET (h) / DECRST (l) toggle modes; the `('h' | 'l', _)` patterns + // keep other actions out of these arms, so `on` is only used when relevant. + let on = action == 'h'; match (action, p0) { - ('h', 1) => self.grid.application_cursor_keys = true, - ('l', 1) => self.grid.application_cursor_keys = false, - ('h', 7) => self.grid.autowrap = true, - ('l', 7) => self.grid.autowrap = false, - ('h', 25) => self.grid.cursor_visible = true, - ('l', 25) => self.grid.cursor_visible = false, - ('h', 1000) => self.grid.mouse_mode = 1000, - ('l', 1000) => self.grid.mouse_mode = 0, - ('h', 1002) => self.grid.mouse_mode = 1002, - ('l', 1002) => self.grid.mouse_mode = 0, - ('h', 1003) => self.grid.mouse_mode = 1003, - ('l', 1003) => self.grid.mouse_mode = 0, - ('h', 1004) => self.grid.focus_report = true, - ('l', 1004) => self.grid.focus_report = false, - ('h', 1006) => self.grid.mouse_sgr = true, - ('l', 1006) => self.grid.mouse_sgr = false, + ('h' | 'l', 1) => self.grid.application_cursor_keys = on, + ('h' | 'l', 7) => self.grid.autowrap = on, + ('h' | 'l', 25) => self.grid.cursor_visible = on, + ('h' | 'l', 1000 | 1002 | 1003) => self.grid.mouse_mode = if on { p0 } else { 0 }, + ('h' | 'l', 1004) => self.grid.focus_report = on, + ('h' | 'l', 1006) => self.grid.mouse_sgr = on, + ('h' | 'l', 2004) => self.grid.bracketed_paste = on, ('h', 1049) => self.grid.enter_alternate_screen(), ('l', 1049) => self.grid.exit_alternate_screen(), - ('h', 2004) => self.grid.bracketed_paste = true, - ('l', 2004) => self.grid.bracketed_paste = false, _ => {} } } @@ -178,8 +170,12 @@ impl Performer<'_> { self.grid.bold = false; self.grid.dim = false; } + 21 => self.grid.double_underline = true, 23 => self.grid.italic = false, - 24 => self.grid.underline = false, + 24 => { + self.grid.underline = false; + self.grid.double_underline = false; + } 25 => self.grid.blink = false, 27 => self.grid.reverse = false, 29 => self.grid.strikethrough = false, diff --git a/src/terminal/parser_test.rs b/src/terminal/parser_test.rs index 42251b9..50f50c7 100644 --- a/src/terminal/parser_test.rs +++ b/src/terminal/parser_test.rs @@ -201,6 +201,33 @@ fn sgr_reset_clears_attributes() { assert!(!p.grid.blink); } +#[test] +fn sgr_double_underline_stamped_on_cell() { + let mut p = make_parser(10, 5); + p.process(b"\x1b[21mA"); + assert!(p.grid.cell(0, 0).double_underline); + assert!(p.grid.double_underline); +} + +#[test] +fn sgr_24_clears_single_and_double_underline() { + let mut p = make_parser(10, 5); + p.process(b"\x1b[4;21m"); // both underline and double underline on + assert!(p.grid.underline); + assert!(p.grid.double_underline); + p.process(b"\x1b[24m"); + assert!(!p.grid.underline); + assert!(!p.grid.double_underline); +} + +#[test] +fn sgr_reset_clears_double_underline() { + let mut p = make_parser(10, 5); + p.process(b"\x1b[21m"); + p.process(b"\x1b[0m"); + assert!(!p.grid.double_underline); +} + #[test] fn erase_in_line_to_end() { let mut p = make_parser(10, 5);