summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorEatgrass <eatgrass@live.cn>2023-11-05 08:30:35 -0600
committerGitHub <noreply@github.com>2023-11-05 15:30:35 +0100
commit7f87d93a430a47c82ce3f177a6a2609d3f6abade (patch)
tree677e43c0c0613ef2e20377295c5e2212833c028f
parent3942000e868ce62b23414ced3bd980d30e0d84b0 (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>
-rw-r--r--zellij-server/src/output/mod.rs16
-rw-r--r--zellij-server/src/panes/terminal_character.rs105
-rw-r--r--zellij-server/src/screen.rs6
-rw-r--r--zellij-server/src/tab/unit/tab_integration_tests.rs4
-rw-r--r--zellij-server/src/unit/screen_tests.rs2
-rw-r--r--zellij-utils/assets/config/default.kdl6
-rw-r--r--zellij-utils/src/input/options.rs10
-rw-r--r--zellij-utils/src/kdl/mod.rs4
-rw-r--r--zellij-utils/src/snapshots/zellij_utils__setup__setup_test__cli_arguments_override_config_options.snap2
-rw-r--r--zellij-utils/src/snapshots/zellij_utils__setup__setup_test__cli_arguments_override_layout_options.snap2
-rw-r--r--zellij-utils/src/snapshots/zellij_utils__setup__setup_test__default_config_with_no_cli_arguments-3.snap2
-rw-r--r--zellij-utils/src/snapshots/zellij_utils__setup__setup_test__default_config_with_no_cli_arguments.snap2
-rw-r--r--zellij-utils/src/snapshots/zellij_utils__setup__setup_test__layout_env_vars_override_config_env_vars.snap2
-rw-r--r--zellij-utils/src/snapshots/zellij_utils__setup__setup_test__layout_keybinds_override_config_keybinds.snap2
-rw-r--r--zellij-utils/src/snapshots/zellij_utils__setup__setup_test__layout_options_override_config_options.snap2
-rw-r--r--zellij-utils/src/snapshots/zellij_utils__setup__setup_test__layout_plugins_override_config_plugins.snap2
-rw-r--r--zellij-utils/src/snapshots/zellij_utils__setup__setup_test__layout_themes_override_config_themes.snap2
-rw-r--r--zellij-utils/src/snapshots/zellij_utils__setup__setup_test__layout_ui_config_overrides_config_ui_config.snap2
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