diff options
author | Eatgrass <eatgrass@live.cn> | 2023-11-05 08:30:35 -0600 |
---|---|---|
committer | GitHub <noreply@github.com> | 2023-11-05 15:30:35 +0100 |
commit | 7f87d93a430a47c82ce3f177a6a2609d3f6abade (patch) | |
tree | 677e43c0c0613ef2e20377295c5e2212833c028f | |
parent | 3942000e868ce62b23414ced3bd980d30e0d84b0 (diff) |
feat(terminal): support styled underlines (#2730)
* feat: support styled underlines
* remove deadcode
* Add ansi_underlines config option
* Add missing variables
* Add ansi_underlines on Output and OutputBuffer
* Fix tests
* Add separate styled underline enum
* Remove ansi_underlines from fg and bg
* Remove unneeded variables
* Rename ansi_underlines -> styled_underlines
* Simplify CharacterStyles::new()
* Move styled_underlines config description
* Fix single underline and remove extra field on CharacterStyles
* Read styled-underlines flag from cli opts
* remove extra attribute left from merge conflict
---------
Co-authored-by: Mike Lloyd <mike.lloyd03@pm.me>
Co-authored-by: Mike Lloyd <49411532+mike-lloyd03@users.noreply.github.com>
Co-authored-by: Aram Drevekenin <aram@poor.dev>
18 files changed, 155 insertions, 18 deletions
diff --git a/zellij-server/src/output/mod.rs b/zellij-server/src/output/mod.rs index d75153379..3f99b4013 100644 --- a/zellij-server/src/output/mod.rs +++ b/zellij-server/src/output/mod.rs @@ -83,6 +83,7 @@ fn serialize_chunks_with_newlines( character_chunks: Vec<CharacterChunk>, _sixel_chunks: Option<&Vec<SixelImageChunk>>, // TODO: fix this sometime link_handler: Option<&mut Rc<RefCell<LinkHandler>>>, + styled_underlines: bool, ) -> Result<String> { let err_context = || "failed to serialize input chunks".to_string(); @@ -90,7 +91,8 @@ fn serialize_chunks_with_newlines( let link_handler = link_handler.map(|l_h| l_h.borrow()); for character_chunk in character_chunks { let chunk_changed_colors = character_chunk.changed_colors(); - let mut character_styles = CharacterStyles::new(); + let mut character_styles = + CharacterStyles::new().enable_styled_underlines(styled_underlines); vte_output.push_str("\n\r"); let mut chunk_width = character_chunk.x; for t_character in character_chunk.terminal_characters.iter() { @@ -120,6 +122,7 @@ fn serialize_chunks( sixel_chunks: Option<&Vec<SixelImageChunk>>, link_handler: Option<&mut Rc<RefCell<LinkHandler>>>, sixel_image_store: Option<&mut SixelImageStore>, + styled_underlines: bool, ) -> Result<String> { let err_context = || "failed to serialize input chunks".to_string(); @@ -128,7 +131,8 @@ fn serialize_chunks( let link_handler = link_handler.map(|l_h| l_h.borrow()); for character_chunk in character_chunks { let chunk_changed_colors = character_chunk.changed_colors(); - let mut character_styles = CharacterStyles::new(); + let mut character_styles = + CharacterStyles::new().enable_styled_underlines(styled_underlines); vte_goto_instruction(character_chunk.x, character_chunk.y, &mut vte_output) .with_context(err_context)?; let mut chunk_width = character_chunk.x; @@ -245,16 +249,19 @@ pub struct Output { sixel_image_store: Rc<RefCell<SixelImageStore>>, character_cell_size: Rc<RefCell<Option<SizeInPixels>>>, floating_panes_stack: Option<FloatingPanesStack>, + styled_underlines: bool, } impl Output { pub fn new( sixel_image_store: Rc<RefCell<SixelImageStore>>, character_cell_size: Rc<RefCell<Option<SizeInPixels>>>, + styled_underlines: bool, ) -> Self { Output { sixel_image_store, character_cell_size, + styled_underlines, ..Default::default() } } @@ -417,6 +424,7 @@ impl Output { self.sixel_chunks.get(&client_id), self.link_handler.as_mut(), Some(&mut self.sixel_image_store.borrow_mut()), + self.styled_underlines, ) .with_context(err_context)?, ); // TODO: less allocations? @@ -869,6 +877,7 @@ impl CharacterChunk { pub struct OutputBuffer { pub changed_lines: HashSet<usize>, // line index pub should_update_all_lines: bool, + styled_underlines: bool, } impl Default for OutputBuffer { @@ -876,6 +885,7 @@ impl Default for OutputBuffer { OutputBuffer { changed_lines: HashSet::new(), should_update_all_lines: true, // first time we should do a full render + styled_underlines: true, } } } @@ -913,7 +923,7 @@ impl OutputBuffer { let y = line_index; chunks.push(CharacterChunk::new(terminal_characters, x, y)); } - serialize_chunks_with_newlines(chunks, None, None) + serialize_chunks_with_newlines(chunks, None, None, self.styled_underlines) } pub fn changed_chunks_in_viewport( &self, diff --git a/zellij-server/src/panes/terminal_character.rs b/zellij-server/src/panes/terminal_character.rs index 71aec7a45..5dd6e2825 100644 --- a/zellij-server/src/panes/terminal_character.rs +++ b/zellij-server/src/panes/terminal_character.rs @@ -21,6 +21,7 @@ pub const EMPTY_TERMINAL_CHARACTER: TerminalCharacter = TerminalCharacter { pub const RESET_STYLES: CharacterStyles = CharacterStyles { foreground: Some(AnsiCode::Reset), background: Some(AnsiCode::Reset), + underline_color: Some(AnsiCode::Reset), strike: Some(AnsiCode::Reset), hidden: Some(AnsiCode::Reset), reverse: Some(AnsiCode::Reset), @@ -31,6 +32,7 @@ pub const RESET_STYLES: CharacterStyles = CharacterStyles { dim: Some(AnsiCode::Reset), italic: Some(AnsiCode::Reset), link_anchor: Some(LinkAnchor::End), + styled_underlines_enabled: false, }; #[derive(Clone, Copy, Debug, PartialEq, Eq)] @@ -40,6 +42,15 @@ pub enum AnsiCode { NamedColor(NamedColor), RgbCode((u8, u8, u8)), ColorIndex(u8), + Underline(Option<AnsiStyledUnderline>), +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum AnsiStyledUnderline { + Double, + Undercurl, + Underdotted, + Underdashed, } impl From<PaletteColor> for AnsiCode { @@ -122,6 +133,7 @@ impl NamedColor { pub struct CharacterStyles { pub foreground: Option<AnsiCode>, pub background: Option<AnsiCode>, + pub underline_color: Option<AnsiCode>, pub strike: Option<AnsiCode>, pub hidden: Option<AnsiCode>, pub reverse: Option<AnsiCode>, @@ -132,6 +144,7 @@ pub struct CharacterStyles { pub dim: Option<AnsiCode>, pub italic: Option<AnsiCode>, pub link_anchor: Option<LinkAnchor>, + pub styled_underlines_enabled: bool, } impl CharacterStyles { @@ -146,6 +159,10 @@ impl CharacterStyles { self.background = background_code; self } + pub fn underline_color(mut self, underline_color_code: Option<AnsiCode>) -> Self { + self.underline_color = underline_color_code; + self + } pub fn bold(mut self, bold_code: Option<AnsiCode>) -> Self { self.bold = bold_code; self @@ -186,9 +203,14 @@ impl CharacterStyles { self.link_anchor = link_anchor; self } + pub fn enable_styled_underlines(mut self, enabled: bool) -> Self { + self.styled_underlines_enabled = enabled; + self + } pub fn clear(&mut self) { self.foreground = None; self.background = None; + self.underline_color = None; self.strike = None; self.hidden = None; self.reverse = None; @@ -215,7 +237,8 @@ impl CharacterStyles { } // create diff from all changed styles - let mut diff = CharacterStyles::new(); + let mut diff = + CharacterStyles::new().enable_styled_underlines(self.styled_underlines_enabled); if self.foreground != new_styles.foreground { diff.foreground = new_styles.foreground; @@ -223,6 +246,9 @@ impl CharacterStyles { if self.background != new_styles.background { diff.background = new_styles.background; } + if self.underline_color != new_styles.underline_color { + diff.underline_color = new_styles.underline_color; + } if self.strike != new_styles.strike { diff.strike = new_styles.strike; } @@ -274,6 +300,7 @@ impl CharacterStyles { pub fn reset_all(&mut self) { self.foreground = Some(AnsiCode::Reset); self.background = Some(AnsiCode::Reset); + self.underline_color = Some(AnsiCode::Reset); self.bold = Some(AnsiCode::Reset); self.dim = Some(AnsiCode::Reset); self.italic = Some(AnsiCode::Reset); @@ -291,7 +318,28 @@ impl CharacterStyles { [1] => *self = self.bold(Some(AnsiCode::On)), [2] => *self = self.dim(Some(AnsiCode::On)), [3] => *self = self.italic(Some(AnsiCode::On)), - [4] => *self = self.underline(Some(AnsiCode::On)), + [4, 0] => *self = self.underline(Some(AnsiCode::Reset)), + [4, 1] => *self = self.underline(Some(AnsiCode::Underline(None))), + [4, 2] => { + *self = + self.underline(Some(AnsiCode::Underline(Some(AnsiStyledUnderline::Double)))) + }, + [4, 3] => { + *self = self.underline(Some(AnsiCode::Underline(Some( + AnsiStyledUnderline::Undercurl, + )))) + }, + [4, 4] => { + *self = self.underline(Some(AnsiCode::Underline(Some( + AnsiStyledUnderline::Underdotted, + )))) + }, + [4, 5] => { + *self = self.underline(Some(AnsiCode::Underline(Some( + AnsiStyledUnderline::Underdashed, + )))) + }, + [4] => *self = self.underline(Some(AnsiCode::Underline(None))), [5] => *self = self.blink_slow(Some(AnsiCode::On)), [6] => *self = self.blink_fast(Some(AnsiCode::On)), [7] => *self = self.reverse(Some(AnsiCode::On)), @@ -357,6 +405,21 @@ impl CharacterStyles { } }, [49] => *self = self.background(Some(AnsiCode::Reset)), + [58] => { + let mut iter = params.map(|param| param[0]); + if let Some(ansi_code) = parse_sgr_color(&mut iter) { + *self = self.underline_color(Some(ansi_code)); + } + }, + [58, params @ ..] => { + let rgb_start = if params.len() > 4 { 2 } else { 1 }; + let rgb_iter = params[rgb_start..].iter().copied(); + let mut iter = std::iter::once(params[0]).chain(rgb_iter); + if let Some(ansi_code) = parse_sgr_color(&mut iter) { + *self = self.underline_color(Some(ansi_code)); + } + }, + [59] => *self = self.underline_color(Some(AnsiCode::Reset)), [90] => { *self = self.foreground(Some(AnsiCode::NamedColor(NamedColor::BrightBlack))) }, @@ -409,6 +472,7 @@ impl Display for CharacterStyles { fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { if self.foreground == Some(AnsiCode::Reset) && self.background == Some(AnsiCode::Reset) + && self.underline_color == Some(AnsiCode::Reset) && self.strike == Some(AnsiCode::Reset) && self.hidden == Some(AnsiCode::Reset) && self.reverse == Some(AnsiCode::Reset) @@ -456,6 +520,22 @@ impl Display for CharacterStyles { _ => {}, } } + if self.styled_underlines_enabled { + if let Some(ansi_code) = self.underline_color { + match ansi_code { + AnsiCode::RgbCode((r, g, b)) => { + write!(f, "\u{1b}[58;2;{};{};{}m", r, g, b)?; + }, + AnsiCode::ColorIndex(color_index) => { + write!(f, "\u{1b}[58;5;{}m", color_index)?; + }, + AnsiCode::Reset => { + write!(f, "\u{1b}[59m")?; + }, + _ => {}, + } + }; + } if let Some(ansi_code) = self.strike { match ansi_code { AnsiCode::On => { @@ -529,15 +609,34 @@ impl Display for CharacterStyles { // otherwise if let Some(ansi_code) = self.underline { match ansi_code { - AnsiCode::On => { + AnsiCode::Underline(None) => { write!(f, "\u{1b}[4m")?; }, + AnsiCode::Underline(Some(styled)) => { + if self.styled_underlines_enabled { + match styled { + AnsiStyledUnderline::Double => { + write!(f, "\u{1b}[4:2m")?; + }, + AnsiStyledUnderline::Undercurl => { + write!(f, "\u{1b}[4:3m")?; + }, + AnsiStyledUnderline::Underdotted => { + write!(f, "\u{1b}[4:4m")?; + }, + AnsiStyledUnderline::Underdashed => { + write!(f, "\u{1b}[4:5m")?; + }, + } + } + }, AnsiCode::Reset => { write!(f, "\u{1b}[24m")?; }, _ => {}, } } + if let Some(ansi_code) = self.dim { match ansi_code { AnsiCode::On => { diff --git a/zellij-server/src/screen.rs b/zellij-server/src/screen.rs index d2b35a9ba..fbc355666 100644 --- a/zellij-server/src/screen.rs +++ b/zellij-server/src/screen.rs @@ -566,6 +566,7 @@ pub(crate) struct Screen { // its creation time default_layout: Box<Layout>, default_shell: Option<PathBuf>, + styled_underlines: bool, arrow_fonts: bool, } @@ -586,6 +587,7 @@ impl Screen { session_serialization: bool, serialize_pane_viewport: bool, scrollback_lines_to_serialize: Option<usize>, + styled_underlines: bool, arrow_fonts: bool, ) -> Self { let session_name = mode_info.session_name.clone().unwrap_or_default(); @@ -622,6 +624,7 @@ impl Screen { session_serialization, serialize_pane_viewport, scrollback_lines_to_serialize, + styled_underlines, arrow_fonts, resurrectable_sessions, } @@ -1032,6 +1035,7 @@ impl Screen { let mut output = Output::new( self.sixel_image_store.clone(), self.character_cell_size.clone(), + self.styled_underlines, ); let mut tabs_to_close = vec![]; for (tab_index, tab) in &mut self.tabs { @@ -2067,6 +2071,7 @@ pub(crate) fn screen_thread_main( config_options.copy_clipboard.unwrap_or_default(), config_options.copy_on_select.unwrap_or(true), ); + let styled_underlines = config_options.styled_underlines.unwrap_or(true); let thread_senders = bus.senders.clone(); let mut screen = Screen::new( @@ -2091,6 +2096,7 @@ pub(crate) fn screen_thread_main( session_serialization, serialize_pane_viewport, scrollback_lines_to_serialize, + styled_underlines, arrow_fonts, ); diff --git a/zellij-server/src/tab/unit/tab_integration_tests.rs b/zellij-server/src/tab/unit/tab_integration_tests.rs index 21e7d0d7b..c3309f558 100644 --- a/zellij-server/src/tab/unit/tab_integration_tests.rs +++ b/zellij-server/src/tab/unit/tab_integration_tests.rs @@ -2049,7 +2049,7 @@ fn move_floating_pane_with_sixel_image() { width: 8, height: 21, }))); - let mut output = Output::new(sixel_image_store.clone(), character_cell_size); + let mut output = Output::new(sixel_image_store.clone(), character_cell_size, true); tab.toggle_floating_panes(Some(client_id), None).unwrap(); tab.new_pane(new_pane_id, None, None, None, Some(client_id)) @@ -2087,7 +2087,7 @@ fn floating_pane_above_sixel_image() { width: 8, height: 21, }))); - let mut output = Output::new(sixel_image_store.clone(), character_cell_size); + let mut output = Output::new(sixel_image_store.clone(), character_cell_size, true); tab.toggle_floating_panes(Some(client_id), None).unwrap(); tab.new_pane(new_pane_id, None, None, None, Some(client_id)) diff --git a/zellij-server/src/unit/screen_tests.rs b/zellij-server/src/unit/screen_tests.rs index 689aed79d..cd100ed75 100644 --- a/zellij-server/src/unit/screen_tests.rs +++ b/zellij-server/src/unit/screen_tests.rs @@ -244,6 +244,7 @@ fn create_new_screen(size: Size) -> Screen { let scrollback_lines_to_serialize = None; let debug = false; + let styled_underlines = true; let arrow_fonts = true; let screen = Screen::new( bus, @@ -260,6 +261,7 @@ fn create_new_screen(size: Size) -> Screen { session_serialization, serialize_pane_viewport, scrollback_lines_to_serialize, + styled_underlines, arrow_fonts, ); screen diff --git a/zellij-utils/assets/config/default.kdl b/zellij-utils/assets/config/default.kdl index f27733a0c..04879ecf4 100644 --- a/zellij-utils/assets/config/default.kdl +++ b/zellij-utils/assets/config/default.kdl @@ -343,3 +343,9 @@ plugins { // The folder in which Zellij will look for themes // // theme_dir "/path/to/my/theme_dir" + +// Enable or disable the rendering of styled and colored underlines (undercurl). +// May need to be disabled for certain unsupported terminals +// Default: true +// +// styled_underlines false diff --git a/zellij-utils/src/input/options.rs b/zellij-utils/src/input/options.rs index 7cf0ca8fe..e922bd2cb 100644 --- a/zellij-utils/src/input/options.rs +++ b/zellij-utils/src/input/options.rs @@ -142,6 +142,11 @@ pub struct Options { #[clap(long, value_parser)] #[serde(default)] pub scrollback_lines_to_serialize: Option<usize>, + + /// Whether to use ANSI styled underlines + #[clap(long, value_parser)] + #[serde(default)] + pub styled_underlines: Option<bool>, } #[derive(ArgEnum, Deserialize, Serialize, Debug, Clone, Copy, PartialEq)] @@ -212,6 +217,7 @@ impl Options { let scrollback_lines_to_serialize = other .scrollback_lines_to_serialize .or(self.scrollback_lines_to_serialize); + let styled_underlines = other.styled_underlines.or(self.styled_underlines); Options { simplified_ui, @@ -237,6 +243,7 @@ impl Options { session_serialization, serialize_pane_viewport, scrollback_lines_to_serialize, + styled_underlines, } } @@ -287,6 +294,7 @@ impl Options { let scrollback_lines_to_serialize = other .scrollback_lines_to_serialize .or_else(|| self.scrollback_lines_to_serialize.clone()); + let styled_underlines = other.styled_underlines.or(self.styled_underlines); Options { simplified_ui, @@ -312,6 +320,7 @@ impl Options { session_serialization, serialize_pane_viewport, scrollback_lines_to_serialize, + styled_underlines, } } @@ -374,6 +383,7 @@ impl From<CliOptions> for Options { session_serialization: opts.session_serialization, serialize_pane_viewport: opts.serialize_pane_viewport, scrollback_lines_to_serialize: opts.scrollback_lines_to_serialize, + styled_underlines: opts.styled_underlines, ..Default::default() } } diff --git a/zellij-utils/src/kdl/mod.rs b/zellij-utils/src/kdl/mod.rs index dc99bc66d..0c32bfacb 100644 --- a/zellij-utils/src/kdl/mod.rs +++ b/zellij-utils/src/kdl/mod.rs @@ -1438,6 +1438,9 @@ impl Options { let scrollback_lines_to_serialize = kdl_property_first_arg_as_i64_or_error!(kdl_options, "scrollback_lines_to_serialize") .map(|(v, _)| v as usize); + let styled_underlines = + kdl_property_first_arg_as_bool_or_error!(kdl_options, "styled_underlines") + .map(|(v, _)| v); Ok(Options { simplified_ui, theme, @@ -1462,6 +1465,7 @@ impl Options { session_serialization, serialize_pane_viewport, scrollback_lines_to_serialize, + styled_underlines, }) } } diff --git a/zellij-utils/src/snapshots/zellij_utils__setup__setup_test__cli_arguments_override_config_options.snap b/zellij-utils/src/snapshots/zellij_utils__setup__setup_test__cli_arguments_override_config_options.snap index 6e08b82cd..b17886775 100644 --- a/zellij-utils/src/snapshots/zellij_utils__setup__setup_test__cli_arguments_override_config_options.snap +++ b/zellij-utils/src/snapshots/zellij_utils__setup__setup_test__cli_arguments_override_config_options.snap @@ -1,6 +1,5 @@ --- source: zellij-utils/src/setup.rs -assertion_line: 686 expression: "format!(\"{:#?}\", options)" --- Options { @@ -29,4 +28,5 @@ Options { session_serialization: None, serialize_pane_viewport: None, scrollback_lines_to_serialize: None, + styled_underlines: None, } diff --git a/zellij-utils/src/snapshots/zellij_utils__setup__setup_test__cli_arguments_override_layout_options.snap b/zellij-utils/src/snapshots/zellij_utils__setup__setup_test__cli_arguments_override_layout_options.snap index d02d83bcd..f6f89eaa6 100644 --- a/zellij-utils/src/snapshots/zellij_utils__setup__setup_test__cli_arguments_override_layout_options.snap +++ b/zellij-utils/src/snapshots/zellij_utils__setup__setup_test__cli_arguments_override_layout_options.snap @@ -1,6 +1,5 @@ --- source: zellij-utils/src/setup.rs -assertion_line: 714 expression: "format!(\"{:#?}\", options)" --- Options { @@ -29,4 +28,5 @@ Options { session_serialization: None, serialize_pane_viewport: None, scrollback_lines_to_serialize: None, + styled_underlines: None, } diff --git a/zellij-utils/src/snapshots/zellij_utils__setup__setup_test__defau |