Skip to content

Commit

Permalink
feat(terminal): support styled underlines (#2730)
Browse files Browse the repository at this point in the history
* 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>
  • Loading branch information
4 people authored Nov 5, 2023
1 parent 3942000 commit 7f87d93
Show file tree
Hide file tree
Showing 18 changed files with 155 additions and 18 deletions.
16 changes: 13 additions & 3 deletions zellij-server/src/output/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -83,14 +83,16 @@ 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();

let mut vte_output = String::new();
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() {
Expand Down Expand Up @@ -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();

Expand All @@ -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;
Expand Down Expand Up @@ -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()
}
}
Expand Down Expand Up @@ -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?
Expand Down Expand Up @@ -869,13 +877,15 @@ 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 {
fn default() -> Self {
OutputBuffer {
changed_lines: HashSet::new(),
should_update_all_lines: true, // first time we should do a full render
styled_underlines: true,
}
}
}
Expand Down Expand Up @@ -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,
Expand Down
105 changes: 102 additions & 3 deletions zellij-server/src/panes/terminal_character.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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),
Expand All @@ -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)]
Expand All @@ -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 {
Expand Down Expand Up @@ -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>,
Expand All @@ -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 {
Expand All @@ -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
Expand Down Expand Up @@ -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;
Expand All @@ -215,14 +237,18 @@ 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;
}
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;
}
Expand Down Expand Up @@ -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);
Expand All @@ -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)),
Expand Down Expand Up @@ -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)))
},
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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 => {
Expand Down Expand Up @@ -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 => {
Expand Down
6 changes: 6 additions & 0 deletions zellij-server/src/screen.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
}

Expand All @@ -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();
Expand Down Expand Up @@ -622,6 +624,7 @@ impl Screen {
session_serialization,
serialize_pane_viewport,
scrollback_lines_to_serialize,
styled_underlines,
arrow_fonts,
resurrectable_sessions,
}
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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(
Expand All @@ -2091,6 +2096,7 @@ pub(crate) fn screen_thread_main(
session_serialization,
serialize_pane_viewport,
scrollback_lines_to_serialize,
styled_underlines,
arrow_fonts,
);

Expand Down
4 changes: 2 additions & 2 deletions zellij-server/src/tab/unit/tab_integration_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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))
Expand Down Expand Up @@ -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))
Expand Down
2 changes: 2 additions & 0 deletions zellij-server/src/unit/screen_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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
Expand Down
Loading

0 comments on commit 7f87d93

Please sign in to comment.