feat(terminal): multiline hyperlink support (#4264)

* working

* done

* docs(changelog): add PR
This commit is contained in:
Aram Drevekenin 2025-07-04 14:23:55 +02:00 committed by GitHub
parent ed1f067f59
commit 2580564d50
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 1384 additions and 2 deletions

View file

@ -11,6 +11,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/)
* performance: consolidate renders (https://github.com/zellij-org/zellij/pull/4245) * performance: consolidate renders (https://github.com/zellij-org/zellij/pull/4245)
* feat: add plugin API to replace a pane with another existing pane (https://github.com/zellij-org/zellij/pull/4246) * feat: add plugin API to replace a pane with another existing pane (https://github.com/zellij-org/zellij/pull/4246)
* feat: add "stack" keybinding and CLI action to add a stacked pane to the current pane (https://github.com/zellij-org/zellij/pull/4255) * feat: add "stack" keybinding and CLI action to add a stacked pane to the current pane (https://github.com/zellij-org/zellij/pull/4255)
* fix: support multiline hyperlinks (https://github.com/zellij-org/zellij/pull/4264)
## [0.42.2] - 2025-04-15 ## [0.42.2] - 2025-04-15
* refactor(terminal): track scroll_region as tuple rather than Option (https://github.com/zellij-org/zellij/pull/4082) * refactor(terminal): track scroll_region as tuple rather than Option (https://github.com/zellij-org/zellij/pull/4082)

View file

@ -31,6 +31,7 @@ use zellij_utils::{consts::VERSION, shared::version_number};
use crate::output::{CharacterChunk, OutputBuffer, SixelImageChunk}; use crate::output::{CharacterChunk, OutputBuffer, SixelImageChunk};
use crate::panes::alacritty_functions::{parse_number, xparse_color}; use crate::panes::alacritty_functions::{parse_number, xparse_color};
use crate::panes::hyperlink_tracker::HyperlinkTracker;
use crate::panes::link_handler::LinkHandler; use crate::panes::link_handler::LinkHandler;
use crate::panes::search::SearchResult; use crate::panes::search::SearchResult;
use crate::panes::selection::Selection; use crate::panes::selection::Selection;
@ -363,6 +364,7 @@ pub struct Grid {
explicitly_disable_kitty_keyboard_protocol: bool, // has kitty keyboard support been explicitly explicitly_disable_kitty_keyboard_protocol: bool, // has kitty keyboard support been explicitly
// disabled by user config? // disabled by user config?
click: Click, click: Click,
hyperlink_tracker: HyperlinkTracker,
} }
const CLICK_TIME_THRESHOLD: u128 = 400; // Doherty Threshold const CLICK_TIME_THRESHOLD: u128 = 400; // Doherty Threshold
@ -550,6 +552,7 @@ impl Grid {
supports_kitty_keyboard_protocol: false, supports_kitty_keyboard_protocol: false,
explicitly_disable_kitty_keyboard_protocol, explicitly_disable_kitty_keyboard_protocol,
click: Click::default(), click: Click::default(),
hyperlink_tracker: HyperlinkTracker::new(),
} }
} }
pub fn render_full_viewport(&mut self) { pub fn render_full_viewport(&mut self) {
@ -1294,6 +1297,13 @@ impl Grid {
} }
pub fn add_canonical_line(&mut self) { pub fn add_canonical_line(&mut self) {
let (scroll_region_top, scroll_region_bottom) = self.scroll_region; let (scroll_region_top, scroll_region_bottom) = self.scroll_region;
self.hyperlink_tracker.update(
'\n',
&self.cursor,
&mut self.viewport,
&mut self.lines_above,
&mut self.link_handler.borrow_mut(),
);
if self.cursor.y == scroll_region_bottom { if self.cursor.y == scroll_region_bottom {
// end of scroll region // end of scroll region
// when we have a scroll region set and we're at its bottom // when we have a scroll region set and we're at its bottom
@ -1348,6 +1358,13 @@ impl Grid {
terminal_character: TerminalCharacter, terminal_character: TerminalCharacter,
should_insert_character: bool, should_insert_character: bool,
) { ) {
self.hyperlink_tracker.update(
terminal_character.character,
&self.cursor,
&mut self.viewport,
&mut self.lines_above,
&mut self.link_handler.borrow_mut(),
);
// this function assumes the current line has enough room for terminal_character (that its // this function assumes the current line has enough room for terminal_character (that its
// width has been checked beforehand) // width has been checked beforehand)
match self.viewport.get_mut(self.cursor.y) { match self.viewport.get_mut(self.cursor.y) {
@ -1477,6 +1494,7 @@ impl Grid {
if self.cursor.y == self.height.saturating_sub(1) { if self.cursor.y == self.height.saturating_sub(1) {
if self.alternate_screen_state.is_none() { if self.alternate_screen_state.is_none() {
self.transfer_rows_to_lines_above(1); self.transfer_rows_to_lines_above(1);
self.hyperlink_tracker.offset_cursor_lines(1);
} else { } else {
self.viewport.remove(0); self.viewport.remove(0);
} }

File diff suppressed because it is too large Load diff

View file

@ -11,8 +11,8 @@ pub struct LinkHandler {
} }
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub struct Link { pub struct Link {
id: Option<String>, pub id: Option<String>,
uri: String, pub uri: String,
} }
impl LinkHandler { impl LinkHandler {
@ -49,6 +49,19 @@ impl LinkHandler {
} }
} }
pub fn new_link_from_url(&mut self, url: String) -> LinkAnchor {
let anchor = LinkAnchor::Start(self.link_index);
self.links.insert(
self.link_index,
Link {
id: Some(self.link_index.to_string()),
uri: url,
},
);
self.link_index += 1;
anchor
}
pub fn output_osc8(&self, link_anchor: Option<LinkAnchor>) -> Option<String> { pub fn output_osc8(&self, link_anchor: Option<LinkAnchor>) -> Option<String> {
link_anchor.and_then(|link| match link { link_anchor.and_then(|link| match link {
LinkAnchor::Start(index) => { LinkAnchor::Start(index) => {
@ -74,6 +87,11 @@ impl LinkHandler {
LinkAnchor::End => Some(format!("\u{1b}]8;;{}", TERMINATOR)), LinkAnchor::End => Some(format!("\u{1b}]8;;{}", TERMINATOR)),
}) })
} }
#[cfg(test)]
pub fn links(&self) -> HashMap<u16, Link> {
self.links.clone()
}
} }
impl Default for LinkHandler { impl Default for LinkHandler {

View file

@ -1,5 +1,6 @@
pub mod alacritty_functions; pub mod alacritty_functions;
pub mod grid; pub mod grid;
pub mod hyperlink_tracker;
pub mod link_handler; pub mod link_handler;
pub mod selection; pub mod selection;
pub mod sixel; pub mod sixel;