aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorAram Drevekenin <aram@poor.dev>2026-03-06 15:23:17 +0100
committerGitHub <noreply@github.com>2026-03-06 15:23:17 +0100
commit85d29a1e660ff2d22900391587bba7639c159c8b (patch)
treebf2da6b2c2d38ed62139abdf2924803ace9cd8c7
parentClick file paths to open in editor, allow plugins to change styles and hover ... (diff)
downloadsl0ppij-85d29a1e660ff2d22900391587bba7639c159c8b.tar.gz
sl0ppij-85d29a1e660ff2d22900391587bba7639c159c8b.tar.xz
sl0ppij-85d29a1e660ff2d22900391587bba7639c159c8b.zip
fix: click links with alt-click (#4809)
-rw-r--r--default-plugins/fixture-plugin-for-tests/src/main.rs1
-rw-r--r--default-plugins/link/src/main.rs2
-rw-r--r--zellij-server/src/panes/grid.rs51
-rw-r--r--zellij-server/src/panes/terminal_pane.rs3
-rw-r--r--zellij-server/src/panes/unit/grid_tests.rs5
-rw-r--r--zellij-server/src/tab/mod.rs3
-rw-r--r--zellij-server/src/tab/mouse_handler.rs41
-rw-r--r--zellij-server/src/tab/unit/snapshots/zellij_server__tab__tab_integration_tests__hover_over_highlight_with_tooltip_caches_tooltip-2.snap24
-rw-r--r--zellij-server/src/tab/unit/snapshots/zellij_server__tab__tab_integration_tests__hover_over_highlight_with_tooltip_caches_tooltip.snap24
-rw-r--r--zellij-server/src/tab/unit/tab_integration_tests.rs179
-rw-r--r--zellij-server/src/ui/pane_boundaries_frame.rs44
-rw-r--r--zellij-server/src/ui/pane_contents_and_ui.rs3
-rw-r--r--zellij-tile/src/shim.rs5
-rwxr-xr-xzellij-utils/assets/plugins/about.wasmbin1453169 -> 1452528 bytes
-rwxr-xr-xzellij-utils/assets/plugins/compact-bar.wasmbin1625878 -> 1627059 bytes
-rwxr-xr-xzellij-utils/assets/plugins/configuration.wasmbin1522706 -> 1523719 bytes
-rwxr-xr-xzellij-utils/assets/plugins/fixture-plugin-for-tests.wasmbin1609922 -> 1610951 bytes
-rwxr-xr-xzellij-utils/assets/plugins/layout-manager.wasmbin1809140 -> 1808446 bytes
-rwxr-xr-xzellij-utils/assets/plugins/link.wasmbin1253321 -> 1251620 bytes
-rwxr-xr-xzellij-utils/assets/plugins/multiple-select.wasmbin1460981 -> 1461537 bytes
-rwxr-xr-xzellij-utils/assets/plugins/plugin-manager.wasmbin1428169 -> 1427468 bytes
-rwxr-xr-xzellij-utils/assets/plugins/session-manager.wasmbin1529189 -> 1528485 bytes
-rwxr-xr-xzellij-utils/assets/plugins/share.wasmbin1423773 -> 1423204 bytes
-rwxr-xr-xzellij-utils/assets/plugins/status-bar.wasmbin1601061 -> 1604046 bytes
-rwxr-xr-xzellij-utils/assets/plugins/strider.wasmbin1600234 -> 1600866 bytes
-rwxr-xr-xzellij-utils/assets/plugins/tab-bar.wasmbin1499270 -> 1500915 bytes
-rw-r--r--zellij-utils/assets/prost/api.plugin_command.rs2
-rw-r--r--zellij-utils/src/data.rs1
-rw-r--r--zellij-utils/src/plugin_api/plugin_command.proto1
-rw-r--r--zellij-utils/src/plugin_api/plugin_command.rs2
30 files changed, 368 insertions, 23 deletions
diff --git a/default-plugins/fixture-plugin-for-tests/src/main.rs b/default-plugins/fixture-plugin-for-tests/src/main.rs
index da0e6482..96ac3fa3 100644
--- a/default-plugins/fixture-plugin-for-tests/src/main.rs
+++ b/default-plugins/fixture-plugin-for-tests/src/main.rs
@@ -601,6 +601,7 @@ impl ZellijPlugin for State {
bold: false,
italic: true,
underline: true,
+ tooltip_text: None,
}];
set_pane_regex_highlights(PaneId::Terminal(1), highlights);
},
diff --git a/default-plugins/link/src/main.rs b/default-plugins/link/src/main.rs
index 3862e5d0..297c2acf 100644
--- a/default-plugins/link/src/main.rs
+++ b/default-plugins/link/src/main.rs
@@ -180,6 +180,7 @@ impl State {
bold: false,
italic: true,
underline: true,
+ tooltip_text: Some("Open".to_string()),
});
// Directory-entry patterns for the pane's current CWD
@@ -194,6 +195,7 @@ impl State {
bold: false,
italic: true,
underline: true,
+ tooltip_text: Some("Open".to_string()),
});
}
}
diff --git a/zellij-server/src/panes/grid.rs b/zellij-server/src/panes/grid.rs
index cb629ac9..b6b64abf 100644
--- a/zellij-server/src/panes/grid.rs
+++ b/zellij-server/src/panes/grid.rs
@@ -533,6 +533,7 @@ pub struct Grid {
pub plugin_highlights: HashMap<u32, Vec<(String, CompiledHighlight)>>,
// key: plugin_id (u32), inner vec: (pattern, compiled) pairs
pub hover_position: Option<Position>, // pane-relative cursor cell; None when outside pane
+ pub cached_hover_tooltip: Option<String>,
}
impl Grid {
@@ -569,6 +570,7 @@ pub struct CompiledHighlight {
pub bold: bool,
pub italic: bool,
pub underline: bool,
+ pub tooltip_text: Option<String>,
}
impl CompiledHighlight {
@@ -813,6 +815,7 @@ impl Grid {
pane_default_bg: None,
plugin_highlights: HashMap::new(),
hover_position: None,
+ cached_hover_tooltip: None,
}
}
pub fn render_full_viewport(&mut self) {
@@ -2095,6 +2098,7 @@ impl Grid {
bold: h.bold,
italic: h.italic,
underline: h.underline,
+ tooltip_text: h.tooltip_text.clone(),
};
} else {
slot.push((
@@ -2108,6 +2112,7 @@ impl Grid {
bold: h.bold,
italic: h.italic,
underline: h.underline,
+ tooltip_text: h.tooltip_text,
},
));
}
@@ -2120,12 +2125,14 @@ impl Grid {
}
}
self.output_buffer.update_all_lines();
+ self.recompute_hover_tooltip();
}
/// Called by the server-side handler for ClearPaneHighlights.
pub fn clear_plugin_highlights(&mut self, plugin_id: u32) {
if self.plugin_highlights.remove(&plugin_id).is_some() {
self.output_buffer.update_all_lines();
+ self.recompute_hover_tooltip();
}
}
@@ -2180,6 +2187,7 @@ impl Grid {
}
self.hover_position = new_pos;
+ self.recompute_hover_tooltip();
}
/// Mark all physical rows belonging to the logical line group that contains
@@ -2194,6 +2202,49 @@ impl Grid {
}
}
+ /// Recompute the cached hover tooltip from the current hover position and
+ /// plugin highlights. Called whenever the hover position, highlight set, or
+ /// highlight clearing changes.
+ fn recompute_hover_tooltip(&mut self) {
+ self.cached_hover_tooltip = None;
+ let hover_pos = match self.hover_position {
+ Some(p) => p,
+ None => return,
+ };
+ if self.mouse_tracking != MouseTracking::Off {
+ return;
+ }
+ if self.plugin_highlights.is_empty() {
+ return;
+ }
+ let hover_row = hover_pos.line.0 as usize;
+ let hover_col = hover_pos.column.0 as usize;
+ let (_canonical, _group_len, logical_text, boundaries) =
+ match collect_and_build_logical_line(&self.viewport, hover_row) {
+ Some(v) => v,
+ None => return,
+ };
+ for (_plugin_id, pattern_map) in &self.plugin_highlights {
+ for (_pattern, compiled) in pattern_map {
+ if !compiled.on_hover || compiled.tooltip_text.is_none() {
+ continue;
+ }
+ for mat in compiled.regex.find_iter(&logical_text) {
+ if let Some((_sel, start_row, start_col, end_row, end_col)) =
+ match_to_selection(&mat, &boundaries, &self.viewport)
+ {
+ if position_in_span(
+ hover_row, hover_col, start_row, start_col, end_row, end_col,
+ ) {
+ self.cached_hover_tooltip = compiled.tooltip_text.clone();
+ return;
+ }
+ }
+ }
+ }
+ }
+ }
+
/// Pre-compute plugin highlight selections across all logical line groups in
/// the viewport. Hover highlights are emitted first so they take priority
/// when the cursor overlaps a match; non-hover highlights follow.
diff --git a/zellij-server/src/panes/terminal_pane.rs b/zellij-server/src/panes/terminal_pane.rs
index f0fd3949..52be384d 100644
--- a/zellij-server/src/panes/terminal_pane.rs
+++ b/zellij-server/src/panes/terminal_pane.rs
@@ -976,6 +976,9 @@ impl Pane for TerminalPane {
self.grid.set_hover_position(position);
self.set_should_render(true);
}
+ fn cached_hover_tooltip(&self) -> Option<String> {
+ self.grid.cached_hover_tooltip.clone()
+ }
fn plugin_highlight_at(
&self,
position: &Position,
diff --git a/zellij-server/src/panes/unit/grid_tests.rs b/zellij-server/src/panes/unit/grid_tests.rs
index 7010cefb..d5ce6a6a 100644
--- a/zellij-server/src/panes/unit/grid_tests.rs
+++ b/zellij-server/src/panes/unit/grid_tests.rs
@@ -4581,6 +4581,7 @@ fn create_highlight(
bold,
italic,
underline,
+ tooltip_text: None,
}
}
@@ -4595,6 +4596,7 @@ fn set_plugin_regex_highlights_basic_match() {
bold: false,
italic: false,
underline: true,
+ tooltip_text: None,
}];
grid.set_plugin_regex_highlights(1, highlights, &Style::default());
let slot = grid.plugin_highlights.get(&1);
@@ -4654,6 +4656,7 @@ fn upsert_replaces_same_pattern() {
bold: false,
italic: false,
underline: true,
+ tooltip_text: None,
}];
grid.set_plugin_regex_highlights(1, h1, &Style::default());
@@ -4665,6 +4668,7 @@ fn upsert_replaces_same_pattern() {
bold: false,
italic: false,
underline: false,
+ tooltip_text: None,
}];
grid.set_plugin_regex_highlights(1, h2, &Style::default());
@@ -4699,6 +4703,7 @@ fn plugin_highlight_at_returns_match() {
bold: false,
italic: false,
underline: true,
+ tooltip_text: None,
}];
grid.set_plugin_regex_highlights(1, highlights, &Style::default());
diff --git a/zellij-server/src/tab/mod.rs b/zellij-server/src/tab/mod.rs
index 861a3103..59a79126 100644
--- a/zellij-server/src/tab/mod.rs
+++ b/zellij-server/src/tab/mod.rs
@@ -615,6 +615,9 @@ pub trait Pane {
}
fn clear_plugin_highlights(&mut self, _plugin_id: u32) {}
fn set_hover_position(&mut self, _position: Option<Position>) {}
+ fn cached_hover_tooltip(&self) -> Option<String> {
+ None
+ }
fn plugin_highlight_at(
&self,
_position: &Position,
diff --git a/zellij-server/src/tab/mouse_handler.rs b/zellij-server/src/tab/mouse_handler.rs
index f98dc3f4..5e575eb7 100644
--- a/zellij-server/src/tab/mouse_handler.rs
+++ b/zellij-server/src/tab/mouse_handler.rs
@@ -643,7 +643,29 @@ impl MouseHandler {
}
match action {
- MouseAction::GroupToggle(pane_id) => Ok(MouseEffect::group_toggle(pane_id)),
+ MouseAction::GroupToggle(pane_id) => {
+ // Check if Alt+Click lands on a plugin highlight
+ if let Some(pane) = tab.get_pane_with_id_mut(pane_id) {
+ let relative_position = pane.relative_position(&event.position);
+ if let Some((hit_plugin_id, pattern, matched_string, context)) =
+ pane.plugin_highlight_at(&relative_position)
+ {
+ let _ = tab
+ .senders
+ .send_to_plugin(PluginInstruction::HighlightClicked {
+ plugin_id: hit_plugin_id,
+ client_id,
+ pane_id,
+ pattern,
+ matched_string,
+ context,
+ });
+ return Ok(MouseEffect::state_changed());
+ }
+ }
+ // No highlight hit — fall through to pane grouping
+ Ok(MouseEffect::group_toggle(pane_id))
+ },
MouseAction::GroupAdd(pane_id) => Ok(MouseEffect::group_add(pane_id)),
MouseAction::Ungroup => Ok(MouseEffect::ungroup()),
MouseAction::StartResize {
@@ -684,23 +706,6 @@ impl MouseHandler {
.ok_or_else(|| anyhow!("Failed to find pane {pane_id:?}"))?;
let relative_position = pane.relative_position(&position);
- // Check if click lands on a plugin highlight
- if let Some((hit_plugin_id, pattern, matched_string, context)) =
- pane.plugin_highlight_at(&relative_position)
- {
- let _ = tab
- .senders
- .send_to_plugin(PluginInstruction::HighlightClicked {
- plugin_id: hit_plugin_id,
- client_id,
- pane_id,
- pattern,
- matched_string,
- context,
- });
- return Ok(MouseEffect::state_changed());
- }
-
let mut leave_clipboard_message = false;
pane.start_selection(&relative_position, client_id);
if pane.get_selected_text(client_id).is_some() {
diff --git a/zellij-server/src/tab/unit/snapshots/zellij_server__tab__tab_integration_tests__hover_over_highlight_with_tooltip_caches_tooltip-2.snap b/zellij-server/src/tab/unit/snapshots/zellij_server__tab__tab_integration_tests__hover_over_highlight_with_tooltip_caches_tooltip-2.snap
new file mode 100644
index 00000000..09b7a7e1
--- /dev/null
+++ b/zellij-server/src/tab/unit/snapshots/zellij_server__tab__tab_integration_tests__hover_over_highlight_with_tooltip_caches_tooltip-2.snap
@@ -0,0 +1,24 @@
+---
+source: zellij-server/src/tab/./unit/tab_integration_tests.rs
+expression: snapshot_without_tooltip
+---
+00 (C): ┌ Pane #1 ──────────────────────────────────────────────────────────────────────────────────────────────────────────────┐
+01 (C): │hello link_text bar │
+02 (C): │ │
+03 (C): │ │
+04 (C): │ │
+05 (C): │ │
+06 (C): │ │
+07 (C): │ │
+08 (C): │ │
+09 (C): │ │
+10 (C): │ │
+11 (C): │ │
+12 (C): │ │
+13 (C): │ │
+14 (C): │ │
+15 (C): │ │
+16 (C): │ │
+17 (C): │ │
+18 (C): │ │
+19 (C): └───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘
diff --git a/zellij-server/src/tab/unit/snapshots/zellij_server__tab__tab_integration_tests__hover_over_highlight_with_tooltip_caches_tooltip.snap b/zellij-server/src/tab/unit/snapshots/zellij_server__tab__tab_integration_tests__hover_over_highlight_with_tooltip_caches_tooltip.snap
new file mode 100644
index 00000000..6d1e2643
--- /dev/null
+++ b/zellij-server/src/tab/unit/snapshots/zellij_server__tab__tab_integration_tests__hover_over_highlight_with_tooltip_caches_tooltip.snap
@@ -0,0 +1,24 @@
+---
+source: zellij-server/src/tab/./unit/tab_integration_tests.rs
+expression: snapshot_with_tooltip
+---
+00 (C): ┌ Pane #1 ──────────────────────────────────────────────────────────────────────────────────────────────────────────────┐
+01 (C): │hello link_text bar │
+02 (C): │ │
+03 (C): │ │
+04 (C): │ │
+05 (C): │ │
+06 (C): │ │
+07 (C): │ │
+08 (C): │ │
+09 (C): │ │
+10 (C): │ │
+11 (C): │ │
+12 (C): │ │
+13 (C): │ │
+14 (C): │ │
+15 (C): │ │
+16 (C): │ │
+17 (C): │ │
+18 (C): │ │
+19 (C): └ Alt <Click> - Open ───────────────────────────────────────────────────────────────────────────────────────────────────┘
diff --git a/zellij-server/src/tab/unit/tab_integration_tests.rs b/zellij-server/src/tab/unit/tab_integration_tests.rs
index ca68538c..da7e7ba7 100644
--- a/zellij-server/src/tab/unit/tab_integration_tests.rs
+++ b/zellij-server/src/tab/unit/tab_integration_tests.rs
@@ -12581,6 +12581,7 @@ fn click_on_plugin_highlight_sends_highlight_clicked() {
bold: false,
italic: false,
underline: true,
+ tooltip_text: None,
}];
tab.set_plugin_regex_highlights_for_pane(
PaneId::Terminal(1),
@@ -12589,13 +12590,13 @@ fn click_on_plugin_highlight_sends_highlight_clicked() {
&Style::default(),
);
- // Left-click at position where "foo" appears
+ // Alt+Click at position where "foo" appears
// With draw_pane_frames=true, content starts at row 1, col 1
// "click here foo bar" -> "foo" starts at offset 11, so col = 1 + 11 = 12
let click_position = Position::new(1, 12);
- let effect = tab
- .handle_mouse_event(&MouseEvent::new_left_press_event(click_position), client_id)
- .unwrap();
+ let mut alt_click = MouseEvent::new_left_press_event(click_position);
+ alt_click.alt = true;
+ let effect = tab.handle_mouse_event(&alt_click, client_id).unwrap();
assert!(effect.state_changed);
@@ -12645,6 +12646,7 @@ fn click_outside_highlight_starts_normal_selection() {
bold: false,
italic: false,
underline: true,
+ tooltip_text: None,
}];
tab.set_plugin_regex_highlights_for_pane(
PaneId::Terminal(1),
@@ -12696,6 +12698,7 @@ fn hover_over_highlight_shows_styling() {
bold: false,
italic: true,
underline: true,
+ tooltip_text: None,
}];
tab.set_plugin_regex_highlights_for_pane(PaneId::Terminal(1), 1, highlights, &Style::default());
@@ -12766,6 +12769,7 @@ fn hover_on_unfocused_pane_no_highlight() {
bold: false,
italic: true,
underline: true,
+ tooltip_text: None,
}];
tab.set_plugin_regex_highlights_for_pane(PaneId::Terminal(1), 1, highlights, &Style::default());
@@ -12815,6 +12819,7 @@ fn highlight_click_in_mouse_mode_pane_suppressed() {
bold: false,
italic: false,
underline: true,
+ tooltip_text: None,
}];
tab.set_plugin_regex_highlights_for_pane(
PaneId::Terminal(1),
@@ -12890,6 +12895,7 @@ fn set_and_clear_highlights_across_tiled_and_floating() {
bold: false,
italic: false,
underline: true,
+ tooltip_text: None,
}];
tab.set_plugin_regex_highlights_for_pane(PaneId::Terminal(1), 1, h1.clone(), &Style::default());
tab.set_plugin_regex_highlights_for_pane(pane2, 1, h1.clone(), &Style::default());
@@ -12904,6 +12910,7 @@ fn set_and_clear_highlights_across_tiled_and_floating() {
bold: false,
italic: false,
underline: false,
+ tooltip_text: None,
}];
tab.set_plugin_regex_highlights_for_pane(PaneId::Terminal(1), 2, h2.clone(), &Style::default());
tab.set_plugin_regex_highlights_for_pane(pane2, 2, h2.clone(), &Style::default());
@@ -12927,3 +12934,167 @@ fn set_and_clear_highlights_across_tiled_and_floating() {
);
assert_snapshot!(snapshot);
}
+
+#[test]
+fn plain_click_on_highlight_does_not_send_highlight_clicked() {
+ let size = Size {
+ cols: 121,
+ rows: 20,
+ };
+ let client_id = 1;
+ let (mut tab, mock_plugin_receiver) =
+ create_new_tab_with_plugin_receiver(size, ModeInfo::default());
+
+ tab.handle_pty_bytes(1, Vec::from("click here foo bar".as_bytes()))
+ .unwrap();
+
+ let highlights = vec![RegexHighlight {
+ pattern: "foo".into(),
+ style: HighlightStyle::Emphasis0,
+ context: BTreeMap::new(),
+ on_hover: false,
+ bold: false,
+ italic: false,
+ underline: true,
+ tooltip_text: None,
+ }];
+ tab.set_plugin_regex_highlights_for_pane(
+ PaneId::Terminal(1),
+ 42,
+ highlights,
+ &Style::default(),
+ );
+
+ // Plain left-click on "foo" — should start selection, NOT send HighlightClicked
+ let click_position = Position::new(1, 12);
+ let _effect = tab
+ .handle_mouse_event(&MouseEvent::new_left_press_event(click_position), client_id)
+ .unwrap();
+
+ let mut found_highlight_clicked = false;
+ while let Ok((instruction, _ctx)) = mock_plugin_receiver.try_recv() {
+ if matches!(instruction, PluginInstruction::HighlightClicked { .. }) {
+ found_highlight_clicked = true;
+ }
+ }
+ assert!(
+ !found_highlight_clicked,
+ "Plain click should NOT send HighlightClicked after Alt+Click change"
+ );
+
+ // Selection should have started instead
+ assert!(tab.selecting_with_mouse_in_pane.is_some());
+}
+
+#[test]
+fn alt_click_outside_highlight_falls_through_to_group_toggle() {
+ let size = Size {
+ cols: 121,
+ rows: 20,
+ };
+ let client_id = 1;
+ let (mut tab, mock_plugin_receiver) =
+ create_new_tab_with_plugin_receiver(size, ModeInfo::default());
+
+ tab.handle_pty_bytes(1, Vec::from("click here foo bar".as_bytes()))
+ .unwrap();
+
+ let highlights = vec![RegexHighlight {
+ pattern: "foo".into(),
+ style: HighlightStyle::Emphasis0,
+ context: BTreeMap::new(),
+ on_hover: false,
+ bold: false,
+ italic: false,
+ underline: true,
+ tooltip_text: None,
+ }];
+ tab.set_plugin_regex_highlights_for_pane(
+ PaneId::Terminal(1),
+ 42,
+ highlights,
+ &Style::default(),
+ );
+
+ // Alt+Click on "click" (col 1), not on "foo" — should fall through to group toggle
+ let click_position = Position::new(1, 1);
+ let mut alt_click = MouseEvent::new_left_press_event(click_position);
+ alt_click.alt = true;
+ let effect = tab.handle_mouse_event(&alt_click, client_id).unwrap();
+
+ // No HighlightClicked should be sent
+ let mut found_highlight_clicked = false;
+ while let Ok((instruction, _ctx)) = mock_plugin_receiver.try_recv() {
+ if matches!(instruction, PluginInstruction::HighlightClicked { .. }) {
+ found_highlight_clicked = true;
+ }
+ }
+ assert!(
+ !found_highlight_clicked,
+ "Alt+Click outside highlight should NOT send HighlightClicked"
+ );
+
+ // Group toggle effect should have been produced
+ assert_eq!(effect.group_toggle, Some(PaneId::Terminal(1)));
+}
+
+#[test]
+fn hover_over_highlight_with_tooltip_caches_tooltip() {
+ let size = Size {
+ cols: 121,
+ rows: 20,
+ };
+ let client_id = 1;
+ let mut tab = create_new_tab(size, ModeInfo::default());
+ let mut output = Output::default();
+
+ tab.handle_pty_bytes(1, Vec::from("hello link_text bar".as_bytes()))
+ .unwrap();
+
+ let highlights = vec![RegexHighlight {
+ pattern: "link_text".into(),
+ style: HighlightStyle::None,
+ context: BTreeMap::new(),
+ on_hover: true,
+ bold: false,
+ italic: true,
+ underline: true,
+ tooltip_text: Some("Open".to_string()),
+ }];
+ tab.set_plugin_regex_highlights_for_pane(PaneId::Terminal(1), 1, highlights, &Style::default());
+
+ // Hover over the match (link_text starts at offset 6, with frame offset col = 7)
+ let hover_position = Position::new(1, 9);
+ let _effect = tab
+ .handle_mouse_event(
+ &MouseEvent::new_buttonless_motion(hover_position),
+ client_id,
+ )
+ .unwrap();
+
+ tab.render(&mut output, None).unwrap();
+ let snapshot_with_tooltip = take_snapshot(
+ output.serialize().unwrap().get(&client_id).unwrap(),
+ size.rows,
+ size.cols,
+ Palette::default(),
+ );
+ // The bottom frame row should contain the tooltip text
+ assert_snapshot!(snapshot_with_tooltip);
+
+ // Move hover away — tooltip should disappear
+ let mut output2 = Output::default();
+ let far_position = Position::new(1, 1);
+ let _effect2 = tab
+ .handle_mouse_event(&MouseEvent::new_buttonless_motion(far_position), client_id)
+ .unwrap();
+
+ tab.render(&mut output2, None).unwrap();
+ let snapshot_without_tooltip = take_snapshot(
+ output2.serialize().unwrap().get(&client_id).unwrap(),
+ size.rows,
+ size.cols,
+ Palette::default(),
+ );
+ assert_snapshot!(snapshot_without_tooltip);
+}
diff --git a/zellij-server/src/ui/pane_boundaries_frame.rs b/zellij-server/src/ui/pane_boundaries_frame.rs
index 57a69559..a464e4b0 100644
--- a/zellij-server/src/ui/pane_boundaries_frame.rs
+++ b/zellij-server/src/ui/pane_boundaries_frame.rs
@@ -66,6 +66,7 @@ pub struct FrameParams {
pub mouse_is_hovering_over_pane: bool,
pub pane_is_selectable: bool,
pub show_help_text: bool,
+ pub highlight_tooltip: Option<String>,
}
#[derive(Default, PartialEq)]
@@ -90,6 +91,7 @@ pub struct PaneFrame {
mouse_is_hovering_over_pane: bool,
is_selectable: bool,
show_help_text: bool,
+ highlight_tooltip: Option<String>,
}
impl PaneFrame {
@@ -120,6 +122,7 @@ impl PaneFrame {
mouse_is_hovering_over_pane: frame_params.mouse_is_hovering_over_pane,
is_selectable: frame_params.pane_is_selectable,
show_help_text: frame_params.show_help_text,
+ highlight_tooltip: frame_params.highlight_tooltip,
}
}
pub fn is_pinned(mut self, is_pinned: bool) -> Self {
@@ -708,6 +711,36 @@ impl PaneFrame {
.or_else(|| Some(self.title_line_without_middle()))
.with_context(|| format!("failed to render title '{}'", self.title))
}
+ fn render_highlight_tooltip_undertitle(&self) -> Result<Vec<TerminalCharacter>> {
+ let max_undertitle_length = self.geom.cols.saturating_sub(2);
+
+ let mut left_boundary =
+ foreground_color(self.get_corner(boundary_type::BOTTOM_LEFT), self.color);
+ let mut right_boundary =
+ foreground_color(self.get_corner(boundary_type::BOTTOM_RIGHT), self.color);
+
+ let tooltip_text = self.highlight_tooltip.as_deref().unwrap_or("");
+ let text = format!(" Alt <Click> - {} ", tooltip_text);
+ let text_len = text.chars().count();
+ if text_len > max_undertitle_length {
+ return Ok(self.empty_undertitle(max_undertitle_length));
+ }
+
+ let mut text_characters = foreground_color(&text, self.color);
+
+ let padding_len = max_undertitle_length.saturating_sub(text_len);
+ let mut padding = String::new();
+ for _ in 0..padding_len {
+ padding.push_str(boundary_type::HORIZONTAL);
+ }
+
+ let mut ret = vec![];
+ ret.append(&mut left_boundary);
+ ret.append(&mut text_characters);
+ ret.append(&mut foreground_color(&padding, self.color));
+ ret.append(&mut right_boundary);
+ Ok(ret)
+ }
fn render_help_text_undertitle(&self) -> Result<Vec<TerminalCharacter>> {
let max_undertitle_length = self.geom.cols.saturating_sub(2);
@@ -932,7 +965,16 @@ impl PaneFrame {
character_chunks.push(CharacterChunk::new(title, x, y));
} else if row == self.geom.rows - 1 {
// bottom row
- if self.show_help_text && self.is_main_client {
+ if self.highlight_tooltip.is_some() && self.is_main_client {
+ let x = self.geom.x;
+ let y = self.geom.y + row;
+ character_chunks.push(CharacterChunk::new(
+ self.render_highlight_tooltip_undertitle()
+ .with_context(err_context)?,
+ x,
+ y,
+ ));
+ } else if self.show_help_text && self.is_main_client {
let x = self.geom.x;
let y = self.geom.y + row;
character_chunks.push(CharacterChunk::new(
diff --git a/zellij-server/src/ui/pane_contents_and_ui.rs b/zellij-server/src/ui/pane_contents_and_ui.rs
index 420ba6e4..694592af 100644
--- a/zellij-server/src/ui/pane_contents_and_ui.rs
+++ b/zellij-server/src/ui/pane_contents_and_ui.rs
@@ -227,6 +227,7 @@ impl<'a> PaneContentsAndUi<'a> {
let pane_focused_for_differet_client = !other_focused_clients.is_empty();
let frame_color = self.frame_color(client_id, client_mode, session_is_mirrored);
+ let highlight_tooltip = self.pane.cached_hover_tooltip();
let focused_client = if pane_focused_for_client_id {
Some(client_id)
} else if pane_focused_for_differet_client {
@@ -252,6 +253,7 @@ impl<'a> PaneContentsAndUi<'a> {
.contains(&client_id),
pane_is_selectable,
show_help_text: self.show_help_text,
+ highlight_tooltip: highlight_tooltip.clone(),
}
} else {
FrameParams {
@@ -271,6 +273,7 @@ impl<'a> PaneContentsAndUi<'a> {
.contains(&client_id),
pane_is_selectable,
show_help_text: self.show_help_text,
+ highlight_tooltip,
}
};
diff --git a/zellij-tile/src/shim.rs b/zellij-tile/src/shim.rs
index 8c40b28a..167df3ec 100644
--- a/zellij-tile/src/shim.rs
+++ b/zellij-tile/src/shim.rs
@@ -2760,6 +2760,11 @@ pub fn hide_floating_panes(tab_id: Option<usize>) -> Result<bool, String> {
/// render time. The plugin never handles coordinates, so there is no race
/// condition between content changes and highlight application.
///
+/// When `on_hover` is `true` and `tooltip_text` is `Some(...)`, the tooltip
+/// text is displayed at the bottom of the pane frame (formatted as
+/// `" Alt <Click> - {tooltip_text} "`) whenever the mouse cursor hovers over
+/// the highlighted region.
+///
/// Requires `ChangeApplicationState` permission.
pub fn set_pane_regex_highlights(pane_id: PaneId, highlights: Vec<RegexHighlight>) {
let plugin_command = PluginCommand::SetPaneRegexHighlights(pane_id, highlights);
diff --git a/zellij-utils/assets/plugins/about.wasm b/zellij-utils/assets/plugins/about.wasm
index 393ff56e..aa506b12 100755
--- a/zellij-utils/assets/plugins/about.wasm
+++ b/zellij-utils/assets/plugins/about.wasm
Binary files differ
diff --git a/zellij-utils/assets/plugins/compact-bar.wasm b/zellij-utils/assets/plugins/compact-bar.wasm
index a5ae98c5..b411e2f3 100755
--- a/zellij-utils/assets/plugins/compact-bar.wasm
+++ b/zellij-utils/assets/plugins/compact-bar.wasm
Binary files differ
diff --git a/zellij-utils/assets/plugins/configuration.wasm b/zellij-utils/assets/plugins/configuration.wasm
index bc47d8ce..5539c523 100755
--- a/zellij-utils/assets/plugins/configuration.wasm
+++ b/zellij-utils/assets/plugins/configuration.wasm
Binary files differ
diff --git a/zellij-utils/assets/plugins/fixture-plugin-for-tests.wasm b/zellij-utils/assets/plugins/fixture-plugin-for-tests.wasm
index d84e92dd..c2212a76 100755
--- a/zellij-utils/assets/plugins/fixture-plugin-for-tests.wasm
+++ b/zellij-utils/assets/plugins/fixture-plugin-for-tests.wasm
Binary files differ
diff --git a/zellij-utils/assets/plugins/layout-manager.wasm b/zellij-utils/assets/plugins/layout-manager.wasm
index a7f0164f..e91f8970 100755
--- a/zellij-utils/assets/plugins/layout-manager.wasm
+++ b/zellij-utils/assets/plugins/layout-manager.wasm
Binary files differ
diff --git a/zellij-utils/assets/plugins/link.wasm b/zellij-utils/assets/plugins/link.wasm
index 0b8be5c3..f22e299a 100755
--- a/zellij-utils/assets/plugins/link.wasm
+++ b/zellij-utils/assets/plugins/link.wasm
Binary files differ
diff --git a/zellij-utils/assets/plugins/multiple-select.wasm b/zellij-utils/assets/plugins/multiple-select.wasm
index 45bb9a2a..47298430 100755
--- a/zellij-utils/assets/plugins/multiple-select.wasm
+++ b/zellij-utils/assets/plugins/multiple-select.wasm
Binary files differ
diff --git a/zellij-utils/assets/plugins/plugin-manager.wasm b/zellij-utils/assets/plugins/plugin-manager.wasm
index 706563c1..918a7087 100755
--- a/zellij-utils/assets/plugins/plugin-manager.wasm
+++ b/zellij-utils/assets/plugins/plugin-manager.wasm
Binary files differ
diff --git a/zellij-utils/assets/plugins/session-manager.wasm b/zellij-utils/assets/plugins/session-manager.wasm
index 04494e13..d4e29478 100755
--- a/zellij-utils/assets/plugins/session-manager.wasm
+++ b/zellij-utils/assets/plugins/session-manager.wasm
Binary files differ
diff --git a/zellij-utils/assets/plugins/share.wasm b/zellij-utils/assets/plugins/share.wasm
index 9ee67571..4333880d 100755
--- a/zellij-utils/assets/plugins/share.wasm
+++ b/zellij-utils/assets/plugins/share.wasm
Binary files differ
diff --git a/zellij-utils/assets/plugins/status-bar.wasm b/zellij-utils/assets/plugins/status-bar.wasm
index 69036d5c..8476c66d 100755
--- a/zellij-utils/assets/plugins/status-bar.wasm
+++ b/zellij-utils/assets/plugins/status-bar.wasm
Binary files differ
diff --git a/zellij-utils/assets/plugins/strider.wasm b/zellij-utils/assets/plugins/strider.wasm
index 5e457f65..7aa561df 100755
--- a/zellij-utils/assets/plugins/strider.wasm
+++ b/zellij-utils/assets/plugins/strider.wasm
Binary files differ
diff --git a/zellij-utils/assets/plugins/tab-bar.wasm b/zellij-utils/assets/plugins/tab-bar.wasm
index 6e111f04..af1e7cc5 100755
--- a/zellij-utils/assets/plugins/tab-bar.wasm
+++ b/zellij-utils/assets/plugins/tab-bar.wasm
Binary files differ
diff --git a/zellij-utils/assets/prost/api.plugin_command.rs b/zellij-utils/assets/prost/api.plugin_command.rs
index 2296b2da..eb26fcbd 100644
--- a/zellij-utils/assets/prost/api.plugin_command.rs
+++ b/zellij-utils/assets/prost/api.plugin_command.rs
@@ -1860,6 +1860,8 @@ pub struct RegexHighlight {
pub italic: bool,
#[prost(bool, tag="7")]
pub underline: bool,
+ #[prost(string, optional, tag="8")]
+ pub tooltip_text: ::core::option::Option<::prost::alloc::string::String>,
}
#[allow(clippy::derive_partial_eq_without_eq)]
#[derive(Clone, PartialEq, ::prost::Message)]
diff --git a/zellij-utils/src/data.rs b/zellij-utils/src/data.rs
index 64f1d33c..67af1d8a 100644
--- a/zellij-utils/src/data.rs
+++ b/zellij-utils/src/data.rs
@@ -1236,6 +1236,7 @@ pub struct RegexHighlight {
pub bold: bool,
pub italic: bool,
pub underline: bool,
+ pub tooltip_text: Option<String>, // shown at bottom of pane frame when hovering over match
}
// these are used for the web client
diff --git a/zellij-utils/src/plugin_api/plugin_command.proto b/zellij-utils/src/plugin_api/plugin_command.proto
index c4fa949f..89836c90 100644
--- a/zellij-utils/src/plugin_api/plugin_command.proto
+++ b/zellij-utils/src/plugin_api/plugin_command.proto
@@ -1263,6 +1263,7 @@ message RegexHighlight {
bool bold = 5;
bool italic = 6;
bool underline = 7;
+ optional string tooltip_text = 8;
}
message SetPaneRegexHighlightsPayload {
diff --git a/zellij-utils/src/plugin_api/plugin_command.rs b/zellij-utils/src/plugin_api/plugin_command.rs
index b535e14a..2ce11c99 100644
--- a/zellij-utils/src/plugin_api/plugin_command.rs
+++ b/zellij-utils/src/plugin_api/plugin_command.rs
@@ -2515,6 +2515,7 @@ impl TryFrom<ProtobufPluginCommand> for PluginCommand {
bold: h.bold,
italic: h.italic,
underline: h.underline,
+ tooltip_text: h.tooltip_text,
}
})
.collect();
@@ -4165,6 +4166,7 @@ impl TryFrom<PluginCommand> for ProtobufPluginCommand {
bold: h.bold,
italic: h.italic,
underline: h.underline,
+ tooltip_text: h.tooltip_text,
}
})
.collect(),