diff options
| author | Aram Drevekenin <aram@poor.dev> | 2026-06-29 18:10:59 +0200 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2026-06-29 18:10:59 +0200 |
| commit | 245f5141c9bb7eb91f003efac4defc94470243fd (patch) | |
| tree | 52ae6d003366f97e41c8a1450602aef63c8fdca2 /zellij-server/src/tab/mod.rs | |
| parent | feat: reload config on theme change (#5135) (diff) | |
| download | sl0ppij-245f5141c9bb7eb91f003efac4defc94470243fd.tar.gz sl0ppij-245f5141c9bb7eb91f003efac4defc94470243fd.tar.xz sl0ppij-245f5141c9bb7eb91f003efac4defc94470243fd.zip | |
Title frames (#5318)
* initial implementation
* improve single line title design
* hint text plugin infra
* add proto regen task
* display alt text in status bar
* don't display text with full frames
* style alt hover text
* fix + style control own pane hover
* make multiplayer indications responsive
* update snapshots
* plugin api
* single pane as tab
* better scroll styling
* fix tests
* tab-bar plus hover
* plugin surface tests
* integration tests
* adjust all integration tests
* reduce hover noise
* add pane id to in place
* more integration tests
* style: rustfmt
* update plugin assets
* update assets and snapshots
* add pr
Diffstat (limited to 'zellij-server/src/tab/mod.rs')
| -rw-r--r-- | zellij-server/src/tab/mod.rs | 230 |
1 files changed, 195 insertions, 35 deletions
diff --git a/zellij-server/src/tab/mod.rs b/zellij-server/src/tab/mod.rs index 02e7bece..a2601966 100644 --- a/zellij-server/src/tab/mod.rs +++ b/zellij-server/src/tab/mod.rs @@ -17,7 +17,7 @@ use uuid::Uuid; use zellij_utils::data::PaneContents; use zellij_utils::data::{ Direction, KeyWithModifier, NewPanePlacement, PaneInfo, PermissionStatus, PermissionType, - PluginPermission, RegexHighlight, ResizeStrategy, Style, WebSharing, + PluginPermission, RegexHighlight, ResizeStrategy, Style, StyledText, WebSharing, }; use zellij_utils::errors::prelude::*; use zellij_utils::input::command::RunCommand; @@ -30,6 +30,9 @@ use crate::background_jobs::BackgroundJob; use crate::pane_groups::PaneGroups; use crate::pty_writer::PtyWriteInstruction; use crate::screen::{CopyOptions, ScreenInstruction}; +use crate::ui::hint_text::{ + held_hint_variants, hover_hint_variants, resize_hint_variants, HintExitStatus, +}; use crate::ui::{loading_indication::LoadingIndication, pane_boundaries_frame::FrameParams}; use layout_applier::LayoutApplier; use swap_layouts::SwapLayouts; @@ -64,6 +67,7 @@ use zellij_utils::{ FloatingPaneLayout, Run, RunPluginOrAlias, SwapFloatingLayout, SwapTiledLayout, TiledPaneLayout, }, + options::PaneFrameStyle, parse_keys, }, pane_size::{Offset, PaneGeom, Size, SizeInPixels, Viewport}, @@ -158,6 +162,7 @@ pub(crate) struct Tab { pub position: usize, pub name: String, pub prev_name: String, + default_name: String, pub size: Size, pub visible_to: Option<HashSet<ClientId>>, tiled_panes: TiledPanes, @@ -176,7 +181,7 @@ pub(crate) struct Tab { default_mode_info: ModeInfo, pub style: Style, connected_clients: Rc<RefCell<HashSet<ClientId>>>, - draw_pane_frames: bool, + pane_frame_style: PaneFrameStyle, auto_layout: bool, pending_vte_events: HashMap<u32, Vec<VteBytes>>, pub selecting_with_mouse_in_pane: Option<PaneId>, // this is only pub for the tests @@ -207,8 +212,12 @@ pub(crate) struct Tab { web_clients_allowed: bool, web_sharing: WebSharing, mouse_hover_pane_id: HashMap<ClientId, PaneId>, + plugin_hover_pane_id: HashMap<ClientId, PaneId>, + mouse_last_pane_id: HashMap<ClientId, PaneId>, mouse_help_text_visible: HashMap<ClientId, bool>, last_mouse_activity_time: HashMap<ClientId, Instant>, + last_hint_text: HashMap<ClientId, BTreeMap<usize, StyledText>>, + last_active_pane_scroll: HashMap<ClientId, Option<(usize, usize)>>, current_pane_group: Rc<RefCell<PaneGroups>>, advanced_mouse_actions: bool, mouse_hover_effects: bool, @@ -620,6 +629,12 @@ pub trait Pane { fn progress_animation_offset(&mut self) {} // only relevant for plugins fn current_title(&self) -> String; fn custom_title(&self) -> Option<String>; + fn has_explicit_title(&self) -> bool { + false + } + fn scroll_position(&self) -> (usize, usize) { + (0, 0) + } fn is_held(&self) -> bool { false } @@ -736,7 +751,7 @@ impl Tab { max_panes: Option<usize>, style: Style, default_mode_info: ModeInfo, - draw_pane_frames: bool, + pane_frame_style: PaneFrameStyle, auto_layout: bool, connected_clients_in_app: Rc<RefCell<HashMap<ClientId, bool>>>, // bool -> is_web_client session_is_mirrored: bool, @@ -764,9 +779,14 @@ impl Tab { web_server_port: u16, mobile_tab_count: usize, ) -> Self { - let name = if name.is_empty() { + let default_name = if name.is_empty() { format!("Tab #{}", (id + 1).saturating_sub(mobile_tab_count)) } else { + String::new() + }; + let name = if name.is_empty() { + default_name.clone() + } else { name }; @@ -790,7 +810,7 @@ impl Tab { character_cell_size.clone(), stacked_resize.clone(), session_is_mirrored, - draw_pane_frames, + pane_frame_style, default_mode_info.clone(), style, os_api.clone(), @@ -824,6 +844,7 @@ impl Tab { suppressed_panes: HashMap::new(), name: name.clone(), prev_name: name, + default_name, size: initial_size, visible_to: None, max_panes, @@ -838,7 +859,7 @@ impl Tab { style, mode_info, default_mode_info, - draw_pane_frames, + pane_frame_style, auto_layout, pending_vte_events: HashMap::new(), connected_clients, @@ -865,8 +886,12 @@ impl Tab { web_clients_allowed, web_sharing, mouse_hover_pane_id: HashMap::new(), + plugin_hover_pane_id: HashMap::new(), + mouse_last_pane_id: HashMap::new(), mouse_help_text_visible: HashMap::new(), last_mouse_activity_time: HashMap::new(), + last_hint_text: HashMap::new(), + last_active_pane_scroll: HashMap::new(), current_pane_group, currently_marking_pane_group, advanced_mouse_actions, @@ -908,7 +933,7 @@ impl Tab { &self.display_area, &mut self.tiled_panes, &mut self.floating_panes, - self.draw_pane_frames, + self.pane_frame_style, &mut self.focus_pane_id, &self.os_api, self.debug, @@ -986,7 +1011,7 @@ impl Tab { &self.display_area, &mut self.tiled_panes, &mut self.floating_panes, - self.draw_pane_frames, + self.pane_frame_style, &mut self.focus_pane_id, &self.os_api, self.debug, @@ -1032,7 +1057,7 @@ impl Tab { self.viewport.clone(), self.display_area.clone(), &mut self.tiled_panes, - self.draw_pane_frames, + self.pane_frame_style, ); self.apply_buffered_instructions().non_fatal(); @@ -1079,7 +1104,7 @@ impl Tab { &self.display_area, &mut self.tiled_panes, &mut self.floating_panes, - self.draw_pane_frames, + self.pane_frame_style, &mut self.focus_pane_id, &self.os_api, self.debug, @@ -1119,7 +1144,7 @@ impl Tab { &self.display_area, &mut self.tiled_panes, &mut self.floating_panes, - self.draw_pane_frames, + self.pane_frame_style, &mut self.focus_pane_id, &self.os_api, self.debug, @@ -1227,6 +1252,7 @@ impl Tab { } else { mode_info.web_server_capability = Some(false); } + mode_info.pane_frame_style = Some(self.pane_frame_style); for plugin_id in &tab_plugin_ids { plugin_updates.push(( Some(*plugin_id), @@ -1242,6 +1268,131 @@ impl Tab { } Ok(()) } + fn resolve_hint_text(&self, client_id: ClientId) -> BTreeMap<usize, StyledText> { + let focused_pane_id = self.get_active_pane_id(client_id); + let hovered_pane_id = self.mouse_hover_pane_id.get(&client_id).copied(); + if let Some(hovered_pane_id) = hovered_pane_id { + if Some(hovered_pane_id) != focused_pane_id { + return hover_hint_variants(); + } + } + if let Some(focused_pane) = self.get_active_pane(client_id) { + if focused_pane.is_held() { + let exited = focused_pane.exited(); + let is_first_run = !exited; + let exit_status = if exited { + Some(match focused_pane.exit_status() { + Some(exit_code) => HintExitStatus::Code(exit_code), + None => HintExitStatus::Exited, + }) + } else { + None + }; + return held_hint_variants(is_first_run, exit_status); + } + } + let resize_help_visible = self + .mouse_help_text_visible + .get(&client_id) + .copied() + .unwrap_or(false); + if resize_help_visible { + let selectable_pane_count = self.get_selectable_tiled_panes().count() + + self.get_selectable_floating_panes().count(); + if selectable_pane_count > 1 && !self.is_fullscreen_active() { + let focused_pane_is_floating = self.floating_panes.panes_are_visible(); + return resize_hint_variants(focused_pane_is_floating); + } + } + BTreeMap::new() + } + pub fn emit_hint_text_if_changed(&mut self) -> Result<()> { + let connected_clients: Vec<ClientId> = + self.connected_clients.borrow().iter().copied().collect(); + let mut plugin_updates = vec![]; + for client_id in connected_clients { + let hint_text = self.resolve_hint_text(client_id); + if self.last_hint_text.get(&client_id) != Some(&hint_text) { + self.last_hint_text.insert(client_id, hint_text.clone()); + plugin_updates.push((None, Some(client_id), Event::HintText(hint_text))); + } + } + if !plugin_updates.is_empty() { + self.senders + .send_to_plugin(PluginInstruction::Update(plugin_updates)) + .with_context(|| format!("failed to update plugins with hint text"))?; + } + Ok(()) + } + pub fn clear_hint_text_cache(&mut self) { + self.last_hint_text.clear(); + } + fn get_selectable_tiled_content_panes( + &self, + ) -> impl Iterator<Item = (&PaneId, &Box<dyn Pane>)> { + self.get_selectable_tiled_panes() + .filter(|(_, p)| !p.borderless()) + } + pub fn single_pane_titles(&self) -> bool { + self.pane_frame_style.draws_titles() + && self.get_selectable_tiled_content_panes().count() == 1 + } + fn tab_name_is_default(&self) -> bool { + !self.name.is_empty() && self.name == self.default_name + } + pub fn single_pane_tab_name(&self) -> Option<String> { + if !self.single_pane_titles() { + return None; + } + let (_, pane) = self.get_selectable_tiled_content_panes().next()?; + let mut base = if self.tab_name_is_default() && pane.has_explicit_title() { + pane.current_title() + } else { + self.name.clone() + }; + if pane.is_held() && pane.exited() { + match pane.exit_status() { + Some(exit_code) => base.push_str(&format!(" [ EXIT CODE: {} ] ", exit_code)), + None => base.push_str(" [ EXITED ] "), + } + } + Some(base) + } + fn resolve_active_pane_scroll(&self, _client_id: ClientId) -> Option<(usize, usize)> { + if !self.single_pane_titles() { + return None; + } + let (_, pane) = self.get_selectable_tiled_content_panes().next()?; + let (position, length) = pane.scroll_position(); + if position > 0 || length > 0 { + Some((position, length)) + } else { + None + } + } + pub fn emit_active_pane_scroll_if_changed(&mut self) -> Result<()> { + let connected_clients: Vec<ClientId> = + self.connected_clients.borrow().iter().copied().collect(); + let mut plugin_updates = vec![]; + for client_id in connected_clients { + let active_pane_scroll = self.resolve_active_pane_scroll(client_id); + if self.last_active_pane_scroll.get(&client_id) != Some(&active_pane_scroll) { + self.last_active_pane_scroll + .insert(client_id, active_pane_scroll); + plugin_updates.push(( + None, + Some(client_id), + Event::ActivePaneScroll(active_pane_scroll), + )); + } + } + if !plugin_updates.is_empty() { + self.senders + .send_to_plugin(PluginInstruction::Update(plugin_updates)) + .with_context(|| format!("failed to update plugins with active pane scroll"))?; + } + Ok(()) + } pub fn add_client(&mut self, client_id: ClientId, mode_info: Option<ModeInfo>) -> Result<()> { let other_clients_exist_in_tab = { !self.connected_clients.borrow().is_empty() }; if other_clients_exist_in_tab { @@ -1314,6 +1465,7 @@ impl Tab { .map(|c| c.change_to_default_mode()); // TODO: no races? self.connected_clients.borrow_mut().remove(&client_id); self.mouse_help_text_visible.remove(&client_id); + self.mouse_last_pane_id.remove(&client_id); self.last_mouse_activity_time.remove(&client_id); self.set_force_render(); } @@ -3404,6 +3556,10 @@ impl Tab { self.hide_cursor_and_clear_display_as_needed(output); } + self.emit_hint_text_if_changed().with_context(err_context)?; + self.emit_active_pane_scroll_if_changed() + .with_context(err_context)?; + Ok(()) } @@ -3605,7 +3761,7 @@ impl Tab { self.viewport.clone(), self.display_area.clone(), &mut self.tiled_panes, - self.draw_pane_frames, + self.pane_frame_style, ); if let Some(pane_id) = fullscreen_pane_to_restore { if self.tiled_panes.panes_contain(&pane_id) { @@ -4058,7 +4214,7 @@ impl Tab { self.viewport.clone(), self.display_area.clone(), &mut self.tiled_panes, - self.draw_pane_frames, + self.pane_frame_style, ); } pub fn set_mouse_selection_support(&mut self, pane_id: PaneId, selection_support: bool) { @@ -4799,25 +4955,26 @@ impl Tab { let is_flexible_in_stack = p.current_geom().is_stacked() && !p.current_geom().rows.is_fixed(); let is_stacked_under = stacked_pane_ids_under_flexible_pane.contains(&p.pid()); - let geom_to_compare_against = if is_stacked_under && !self.draw_pane_frames { - // these sort of panes are one-liner panes under a flexible pane in a stack when we - // don't draw pane frames - because the whole stack's content is offset to allow - // room for the boundary between panes, they are actually drawn 1 line above where - // they are - let mut geom = p.current_geom(); - geom.y = geom.y.saturating_sub(p.get_content_offset().bottom); - geom - } else if is_flexible_in_stack && !self.draw_pane_frames { - // these sorts of panes are flexible panes inside a stack when we don't draw pane - // frames - because the whole stack's content is offset to give room for the - // boundary between panes, we need to take this offset into account when figuring - // out whether the position is inside them - let mut geom = p.current_geom(); - geom.rows.decrease_inner(p.get_content_offset().bottom); - geom - } else { - p.current_geom() - }; + let geom_to_compare_against = + if is_stacked_under && !self.pane_frame_style.draws_full_frames() { + // these sort of panes are one-liner panes under a flexible pane in a stack when we + // don't draw pane frames - because the whole stack's content is offset to allow + // room for the boundary between panes, they are actually drawn 1 line above where + // they are + let mut geom = p.current_geom(); + geom.y = geom.y.saturating_sub(p.get_content_offset().bottom); + geom + } else if is_flexible_in_stack && !self.pane_frame_style.draws_full_frames() { + // these sorts of panes are flexible panes inside a stack when we don't draw pane + // frames - because the whole stack's content is offset to give room for the + // boundary between panes, we need to take this offset into account when figuring + // out whether the position is inside them + let mut geom = p.current_geom(); + geom.rows.decrease_inner(p.get_content_offset().bottom); + geom + } else { + p.current_geom() + }; geom_to_compare_against.contains(point) }; @@ -4975,6 +5132,7 @@ impl Tab { .with_context(|| format!("failed to set visibility of tab to {visible}"))?; if !visible { self.mouse_hover_pane_id.clear(); + self.plugin_hover_pane_id.clear(); } Ok(()) } @@ -5064,9 +5222,9 @@ impl Tab { && column < viewport.x + viewport.cols) } - pub fn set_pane_frames(&mut self, should_set_pane_frames: bool) { - self.tiled_panes.set_pane_frames(should_set_pane_frames); - self.draw_pane_frames = should_set_pane_frames; + pub fn set_pane_frames(&mut self, pane_frame_style: PaneFrameStyle) { + self.tiled_panes.set_pane_frames(pane_frame_style); + self.pane_frame_style = pane_frame_style; self.set_should_clear_display_before_rendering(); self.set_force_render(); } @@ -5847,6 +6005,8 @@ impl Tab { } pub fn clear_mouse_hover_state(&mut self) { self.mouse_hover_pane_id.clear(); + self.plugin_hover_pane_id.clear(); + self.mouse_last_pane_id.clear(); self.mouse_help_text_visible.clear(); } pub fn update_web_sharing(&mut self, web_sharing: WebSharing) { |
