feat(compatibility): add support for osc 8 escape code (#822)
Resolve #645 * add support for osc 8 escape code * refactor, add links to CharacterStyles * cleanup * refactor, add minimal tests
This commit is contained in:
parent
d8959ca6e1
commit
a96b9d8769
5 changed files with 144 additions and 2 deletions
|
|
@ -18,13 +18,13 @@ use zellij_tile::data::{Palette, PaletteColor};
|
||||||
use zellij_utils::{consts::VERSION, shared::version_number};
|
use zellij_utils::{consts::VERSION, shared::version_number};
|
||||||
|
|
||||||
use crate::panes::alacritty_functions::{parse_number, xparse_color};
|
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::{
|
use crate::panes::terminal_character::{
|
||||||
AnsiCode, CharacterStyles, CharsetIndex, Cursor, CursorShape, StandardCharset,
|
AnsiCode, CharacterStyles, CharsetIndex, Cursor, CursorShape, StandardCharset,
|
||||||
TerminalCharacter, EMPTY_TERMINAL_CHARACTER,
|
TerminalCharacter, EMPTY_TERMINAL_CHARACTER,
|
||||||
};
|
};
|
||||||
|
|
||||||
use super::selection::Selection;
|
|
||||||
|
|
||||||
fn get_top_non_canonical_rows(rows: &mut Vec<Row>) -> Vec<Row> {
|
fn get_top_non_canonical_rows(rows: &mut Vec<Row>) -> Vec<Row> {
|
||||||
let mut index_of_last_non_canonical_row = None;
|
let mut index_of_last_non_canonical_row = None;
|
||||||
for (i, row) in rows.iter().enumerate() {
|
for (i, row) in rows.iter().enumerate() {
|
||||||
|
|
@ -394,6 +394,7 @@ pub struct Grid {
|
||||||
pub selection: Selection,
|
pub selection: Selection,
|
||||||
pub title: Option<String>,
|
pub title: Option<String>,
|
||||||
pub is_scrolled: bool,
|
pub is_scrolled: bool,
|
||||||
|
pub link_handler: LinkHandler,
|
||||||
scrollback_buffer_lines: usize,
|
scrollback_buffer_lines: usize,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -440,6 +441,7 @@ impl Grid {
|
||||||
title: None,
|
title: None,
|
||||||
changed_colors: None,
|
changed_colors: None,
|
||||||
is_scrolled: false,
|
is_scrolled: false,
|
||||||
|
link_handler: Default::default(),
|
||||||
scrollback_buffer_lines: 0,
|
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.
|
// Get/set Foreground, Background, Cursor colors.
|
||||||
b"10" | b"11" | b"12" => {
|
b"10" | b"11" | b"12" => {
|
||||||
if params.len() >= 2 {
|
if params.len() >= 2 {
|
||||||
|
|
|
||||||
111
zellij-server/src/panes/link_handler.rs
Normal file
111
zellij-server/src/panes/link_handler.rs
Normal file
|
|
@ -0,0 +1,111 @@
|
||||||
|
use std::collections::HashMap;
|
||||||
|
|
||||||
|
use super::LinkAnchor;
|
||||||
|
|
||||||
|
const TERMINATOR: &str = "\u{1b}\\";
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct LinkHandler {
|
||||||
|
links: HashMap<u16, Link>,
|
||||||
|
link_index: u16,
|
||||||
|
}
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
struct Link {
|
||||||
|
id: Option<String>,
|
||||||
|
uri: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl LinkHandler {
|
||||||
|
pub fn new() -> Self {
|
||||||
|
Self {
|
||||||
|
links: HashMap::new(),
|
||||||
|
link_index: 0,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn dispatch_osc8(&mut self, params: &[&[u8]]) -> Option<LinkAnchor> {
|
||||||
|
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<LinkAnchor>) -> 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
mod alacritty_functions;
|
mod alacritty_functions;
|
||||||
mod grid;
|
mod grid;
|
||||||
|
mod link_handler;
|
||||||
mod plugin_pane;
|
mod plugin_pane;
|
||||||
mod selection;
|
mod selection;
|
||||||
mod terminal_character;
|
mod terminal_character;
|
||||||
|
|
|
||||||
|
|
@ -23,6 +23,7 @@ pub const RESET_STYLES: CharacterStyles = CharacterStyles {
|
||||||
bold: Some(AnsiCode::Reset),
|
bold: Some(AnsiCode::Reset),
|
||||||
dim: Some(AnsiCode::Reset),
|
dim: Some(AnsiCode::Reset),
|
||||||
italic: Some(AnsiCode::Reset),
|
italic: Some(AnsiCode::Reset),
|
||||||
|
link_anchor: Some(LinkAnchor::End),
|
||||||
};
|
};
|
||||||
|
|
||||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||||
|
|
@ -110,6 +111,7 @@ pub struct CharacterStyles {
|
||||||
pub bold: Option<AnsiCode>,
|
pub bold: Option<AnsiCode>,
|
||||||
pub dim: Option<AnsiCode>,
|
pub dim: Option<AnsiCode>,
|
||||||
pub italic: Option<AnsiCode>,
|
pub italic: Option<AnsiCode>,
|
||||||
|
pub link_anchor: Option<LinkAnchor>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for CharacterStyles {
|
impl Default for CharacterStyles {
|
||||||
|
|
@ -126,6 +128,7 @@ impl Default for CharacterStyles {
|
||||||
bold: None,
|
bold: None,
|
||||||
dim: None,
|
dim: None,
|
||||||
italic: None,
|
italic: None,
|
||||||
|
link_anchor: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -178,6 +181,10 @@ impl CharacterStyles {
|
||||||
self.strike = strike_code;
|
self.strike = strike_code;
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
pub fn link_anchor(mut self, link_anchor: Option<LinkAnchor>) -> Self {
|
||||||
|
self.link_anchor = link_anchor;
|
||||||
|
self
|
||||||
|
}
|
||||||
pub fn clear(&mut self) {
|
pub fn clear(&mut self) {
|
||||||
self.foreground = None;
|
self.foreground = None;
|
||||||
self.background = None;
|
self.background = None;
|
||||||
|
|
@ -190,6 +197,7 @@ impl CharacterStyles {
|
||||||
self.bold = None;
|
self.bold = None;
|
||||||
self.dim = None;
|
self.dim = None;
|
||||||
self.italic = None;
|
self.italic = None;
|
||||||
|
self.link_anchor = None;
|
||||||
}
|
}
|
||||||
pub fn update_and_return_diff(
|
pub fn update_and_return_diff(
|
||||||
&mut self,
|
&mut self,
|
||||||
|
|
@ -241,6 +249,9 @@ impl CharacterStyles {
|
||||||
if self.italic != new_styles.italic {
|
if self.italic != new_styles.italic {
|
||||||
diff.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
|
// apply new styles
|
||||||
*self = *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)]
|
#[derive(Clone, Copy, Debug)]
|
||||||
pub enum CharsetIndex {
|
pub enum CharsetIndex {
|
||||||
G0,
|
G0,
|
||||||
|
|
|
||||||
|
|
@ -236,7 +236,10 @@ impl Pane for TerminalPane {
|
||||||
.update_and_return_diff(&t_character.styles, self.grid.changed_colors)
|
.update_and_return_diff(&t_character.styles, self.grid.changed_colors)
|
||||||
{
|
{
|
||||||
vte_output.push_str(&new_styles.to_string());
|
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);
|
vte_output.push(t_character.character);
|
||||||
}
|
}
|
||||||
character_styles.clear();
|
character_styles.clear();
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue