diff --git a/zellij-server/src/panes/grid.rs b/zellij-server/src/panes/grid.rs index c094701f..e3752fef 100644 --- a/zellij-server/src/panes/grid.rs +++ b/zellij-server/src/panes/grid.rs @@ -18,13 +18,13 @@ use zellij_tile::data::{Palette, PaletteColor}; use zellij_utils::{consts::VERSION, shared::version_number}; use crate::panes::alacritty_functions::{parse_number, xparse_color}; +use crate::panes::link_handler::LinkHandler; +use crate::panes::selection::Selection; use crate::panes::terminal_character::{ AnsiCode, CharacterStyles, CharsetIndex, Cursor, CursorShape, StandardCharset, TerminalCharacter, EMPTY_TERMINAL_CHARACTER, }; -use super::selection::Selection; - fn get_top_non_canonical_rows(rows: &mut Vec) -> Vec { let mut index_of_last_non_canonical_row = None; for (i, row) in rows.iter().enumerate() { @@ -394,6 +394,7 @@ pub struct Grid { pub selection: Selection, pub title: Option, pub is_scrolled: bool, + pub link_handler: LinkHandler, scrollback_buffer_lines: usize, } @@ -440,6 +441,7 @@ impl Grid { title: None, changed_colors: None, is_scrolled: false, + link_handler: Default::default(), scrollback_buffer_lines: 0, } } @@ -1549,6 +1551,14 @@ impl Perform for Grid { } } + // define hyperlink + b"8" => { + if params.len() < 3 { + return; + } + self.cursor.pending_styles.link_anchor = self.link_handler.dispatch_osc8(params); + } + // Get/set Foreground, Background, Cursor colors. b"10" | b"11" | b"12" => { if params.len() >= 2 { diff --git a/zellij-server/src/panes/link_handler.rs b/zellij-server/src/panes/link_handler.rs new file mode 100644 index 00000000..4294dbed --- /dev/null +++ b/zellij-server/src/panes/link_handler.rs @@ -0,0 +1,111 @@ +use std::collections::HashMap; + +use super::LinkAnchor; + +const TERMINATOR: &str = "\u{1b}\\"; + +#[derive(Debug, Clone)] +pub struct LinkHandler { + links: HashMap, + link_index: u16, +} +#[derive(Debug, Clone)] +struct Link { + id: Option, + uri: String, +} + +impl LinkHandler { + pub fn new() -> Self { + Self { + links: HashMap::new(), + link_index: 0, + } + } + + pub fn dispatch_osc8(&mut self, params: &[&[u8]]) -> Option { + let (link_params, uri) = (params[1], params[2]); + log::debug!( + "dispatching osc8, params: {:?}, uri: {:?}", + std::str::from_utf8(link_params), + std::str::from_utf8(uri) + ); + + if !uri.is_empty() { + // save the link, and the id if present to hashmap + String::from_utf8(uri.to_vec()).ok().map(|uri| { + let id = link_params + .split(|&b| b == b':') + .find(|kv| kv.starts_with(b"id=")) + .and_then(|kv| String::from_utf8(kv[3..].to_vec()).ok()); + let anchor = LinkAnchor::Start(self.link_index); + self.links.insert(self.link_index, Link { id, uri }); + self.link_index += 1; + anchor + }) + } else { + // there is no link, so consider it a link end + Some(LinkAnchor::End) + } + } + + pub fn output_osc8(&self, link_anchor: Option) -> String { + link_anchor.map_or("".to_string(), |link| match link { + LinkAnchor::Start(index) => { + let link = self.links.get(&index).unwrap(); + let id = link + .id + .as_ref() + .map_or("".to_string(), |id| format!("id={}", id)); + format!("\u{1b}]8;{};{}{}", id, link.uri, TERMINATOR) + } + LinkAnchor::End => format!("\u{1b}]8;;{}", TERMINATOR), + }) + } +} + +impl Default for LinkHandler { + fn default() -> Self { + Self::new() + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn dispatch_osc8_link_start() { + let mut link_handler = LinkHandler::default(); + let link_params = "id=test"; + let uri = "http://test.com"; + let params = vec!["8".as_bytes(), link_params.as_bytes(), uri.as_bytes()]; + + let anchor = link_handler.dispatch_osc8(¶ms); + + match anchor { + Some(LinkAnchor::Start(link_id)) => { + let link = link_handler.links.get(&link_id).expect("link was not some"); + assert_eq!(link.id, Some("test".to_string())); + assert_eq!(link.uri, uri); + } + _ => panic!("pending link handler was not start"), + } + + let expected = format!("\u{1b}]8;id=test;http://test.com{}", TERMINATOR); + assert_eq!(link_handler.output_osc8(anchor), expected); + } + + #[test] + fn dispatch_osc8_link_end() { + let mut link_handler = LinkHandler::default(); + let params = vec!["8".as_bytes(), &[], &[]]; + + let anchor = link_handler.dispatch_osc8(¶ms); + + assert_eq!(anchor, Some(LinkAnchor::End)); + + let expected = format!("\u{1b}]8;;{}", TERMINATOR); + assert_eq!(link_handler.output_osc8(anchor), expected); + } +} diff --git a/zellij-server/src/panes/mod.rs b/zellij-server/src/panes/mod.rs index c918f98f..7ee5ab62 100644 --- a/zellij-server/src/panes/mod.rs +++ b/zellij-server/src/panes/mod.rs @@ -1,5 +1,6 @@ mod alacritty_functions; mod grid; +mod link_handler; mod plugin_pane; mod selection; mod terminal_character; diff --git a/zellij-server/src/panes/terminal_character.rs b/zellij-server/src/panes/terminal_character.rs index f619b891..c9de3760 100644 --- a/zellij-server/src/panes/terminal_character.rs +++ b/zellij-server/src/panes/terminal_character.rs @@ -23,6 +23,7 @@ pub const RESET_STYLES: CharacterStyles = CharacterStyles { bold: Some(AnsiCode::Reset), dim: Some(AnsiCode::Reset), italic: Some(AnsiCode::Reset), + link_anchor: Some(LinkAnchor::End), }; #[derive(Clone, Copy, Debug, PartialEq, Eq)] @@ -110,6 +111,7 @@ pub struct CharacterStyles { pub bold: Option, pub dim: Option, pub italic: Option, + pub link_anchor: Option, } impl Default for CharacterStyles { @@ -126,6 +128,7 @@ impl Default for CharacterStyles { bold: None, dim: None, italic: None, + link_anchor: None, } } } @@ -178,6 +181,10 @@ impl CharacterStyles { self.strike = strike_code; self } + pub fn link_anchor(mut self, link_anchor: Option) -> Self { + self.link_anchor = link_anchor; + self + } pub fn clear(&mut self) { self.foreground = None; self.background = None; @@ -190,6 +197,7 @@ impl CharacterStyles { self.bold = None; self.dim = None; self.italic = None; + self.link_anchor = None; } pub fn update_and_return_diff( &mut self, @@ -241,6 +249,9 @@ impl CharacterStyles { if self.italic != new_styles.italic { diff.italic = new_styles.italic; } + if self.link_anchor != new_styles.link_anchor { + diff.link_anchor = new_styles.link_anchor; + } // apply new styles *self = *new_styles; @@ -556,6 +567,12 @@ impl Display for CharacterStyles { } } +#[derive(Clone, Copy, Debug, PartialEq)] +pub enum LinkAnchor { + Start(u16), + End, +} + #[derive(Clone, Copy, Debug)] pub enum CharsetIndex { G0, diff --git a/zellij-server/src/panes/terminal_pane.rs b/zellij-server/src/panes/terminal_pane.rs index 66a8a3dd..f2974cee 100644 --- a/zellij-server/src/panes/terminal_pane.rs +++ b/zellij-server/src/panes/terminal_pane.rs @@ -236,7 +236,10 @@ impl Pane for TerminalPane { .update_and_return_diff(&t_character.styles, self.grid.changed_colors) { vte_output.push_str(&new_styles.to_string()); + vte_output + .push_str(&self.grid.link_handler.output_osc8(new_styles.link_anchor)) } + vte_output.push(t_character.character); } character_styles.clear();