use crate::output::{CharacterChunk, Output}; use crate::panes::terminal_character::AnsiCode; use crate::panes::PaneId; use crate::tab::Pane; use crate::ui::grayscale::grayscale_character_chunks; use crate::ui::boundaries::Boundaries; use crate::ui::pane_boundaries_frame::{FrameParams, StackListEntry}; use crate::ClientId; use std::collections::{HashMap, HashSet}; use zellij_utils::data::{client_id_to_colors, InputMode, PaletteColor, Style}; use zellij_utils::input::theme::StackListStyle; use zellij_utils::errors::prelude::*; use zellij_utils::pane_size::PaneGeom; pub fn dim_character_chunks(character_chunks: &mut Vec) { for chunk in character_chunks.iter_mut() { for terminal_character in chunk.terminal_characters.iter_mut() { terminal_character.styles.update(|styles| { styles.dim = Some(AnsiCode::On); }); } } } pub struct PaneContentsAndUi<'a> { pane: &'a mut Box, output: &'a mut Output, style: Style, focused_clients: Vec, multiple_users_exist_in_session: bool, z_index: Option, pane_is_stacked_under: bool, pane_is_stacked_over: bool, should_draw_pane_frames: bool, mouse_is_hovering_over_pane_for_clients: HashSet, current_pane_group: HashMap>, show_help_text: bool, omit_title: bool, frame_geom_override: Option, stack_list_entry_width: Option, stack_list_entry_is_selected: bool, stack_list_entry_stack_is_focused: bool, stack_list_entry_index: Option, stack_list_entry_style: StackListStyle, blank_title: bool, mouse_scroll_resize: bool, dimmed_for_clients: HashSet, } impl<'a> PaneContentsAndUi<'a> { pub fn new( pane: &'a mut Box, output: &'a mut Output, style: Style, active_panes: &HashMap, multiple_users_exist_in_session: bool, z_index: Option, pane_is_stacked_under: bool, pane_is_stacked_over: bool, should_draw_pane_frames: bool, mouse_hover_pane_id: &HashMap, current_pane_group: HashMap>, show_help_text: bool, omit_title: bool, mouse_scroll_resize: bool, dimmed_for_clients: HashSet, ) -> Self { let mut focused_clients: Vec = active_panes .iter() .filter(|(_c_id, p_id)| **p_id == pane.pid()) .map(|(c_id, _p_id)| *c_id) .collect(); focused_clients.sort_unstable(); let mouse_is_hovering_over_pane_for_clients = mouse_hover_pane_id .iter() .filter_map(|(client_id, pane_id)| { if pane_id == &pane.pid() { Some(*client_id) } else { None } }) .collect(); PaneContentsAndUi { pane, output, style, focused_clients, multiple_users_exist_in_session, z_index, pane_is_stacked_under, pane_is_stacked_over, should_draw_pane_frames, mouse_is_hovering_over_pane_for_clients, current_pane_group, show_help_text, omit_title, frame_geom_override: None, stack_list_entry_width: None, stack_list_entry_is_selected: false, stack_list_entry_stack_is_focused: false, stack_list_entry_index: None, stack_list_entry_style: StackListStyle::default(), blank_title: false, mouse_scroll_resize, dimmed_for_clients, } } fn frame_is_dimmed_for_client(&self, client_id: ClientId) -> bool { self.dimmed_for_clients.contains(&client_id) && !self.focused_clients.contains(&client_id) } pub fn set_frame_geom_override(&mut self, frame_geom_override: Option) { self.frame_geom_override = frame_geom_override; } pub fn set_blank_title(&mut self, blank_title: bool) { self.blank_title = blank_title; } pub fn set_stack_list_entry( &mut self, stack_list_entry_width: Option, stack_list_entry_is_selected: bool, stack_list_entry_stack_is_focused: bool, stack_list_entry_index: Option, stack_list_entry_style: StackListStyle, ) { self.stack_list_entry_width = stack_list_entry_width; self.stack_list_entry_is_selected = stack_list_entry_is_selected; self.stack_list_entry_stack_is_focused = stack_list_entry_stack_is_focused; self.stack_list_entry_index = stack_list_entry_index; self.stack_list_entry_style = stack_list_entry_style; } pub fn render_pane_contents_to_multiple_clients( &mut self, clients: impl Iterator, ) -> Result<()> { let err_context = "failed to render pane contents to multiple clients"; // here we drop the fake cursors so that their lines will be updated // and we can clear them from the UI below drop(self.pane.drain_fake_cursors()); if let Some((character_chunks, raw_vte_output, sixel_image_chunks, kitty_image_chunks)) = self.pane.render(None).context(err_context)? { let clients: Vec = clients.collect(); self.output .add_character_chunks_to_multiple_clients( character_chunks, clients.iter().copied(), self.z_index, ) .context(err_context)?; self.output.add_sixel_image_chunks_to_multiple_clients( sixel_image_chunks, clients.iter().copied(), self.z_index, ); self.output.add_kitty_image_chunks_to_multiple_clients( self.pane.pid(), kitty_image_chunks, clients.iter().copied(), self.z_index, ); if let Some(raw_vte_output) = raw_vte_output { if !raw_vte_output.is_empty() { self.output.add_post_vte_instruction_to_multiple_clients( clients.iter().copied(), &format!( "\u{1b}[{};{}H\u{1b}[m{}", self.pane.y() + 1, self.pane.x() + 1, raw_vte_output ), ); } } } Ok(()) } pub fn render_pane_contents_for_client(&mut self, client_id: ClientId) -> Result<()> { let err_context = || format!("failed to render pane contents for client {client_id}"); if let Some((mut character_chunks, raw_vte_output, sixel_image_chunks, kitty_image_chunks)) = self.pane .render(Some(client_id)) .with_context(err_context)? { // Drain the colour out of panes this client is not focused on. // NOTE this cannot be done on the shared multi-client path: focus // is per client, so two clients looking at the same pane may need // different output. let grayscale = self.style.inactive_pane_grayscale; if grayscale > 0 && !self.focused_clients.contains(&client_id) { grayscale_character_chunks(&mut character_chunks, grayscale); } self.output .add_character_chunks_to_client(client_id, character_chunks, self.z_index) .with_context(err_context)?; self.output.add_sixel_image_chunks_to_client( client_id, sixel_image_chunks, self.z_index, ); self.output.add_kitty_image_chunks_to_client( client_id, self.pane.pid(), kitty_image_chunks, self.z_index, ); if let Some(raw_vte_output) = raw_vte_output { self.output.add_post_vte_instruction_to_client( client_id, &format!( "\u{1b}[{};{}H\u{1b}[m{}", self.pane.y() + 1, self.pane.x() + 1, raw_vte_output ), ); } } Ok(()) } pub fn client_has_guest_modal(&self, client_id: ClientId) -> bool { self.pane.guest_modal_selection(client_id).is_some() } pub fn drain_pane_render_state(&mut self) { drop(self.pane.drain_fake_cursors()); let _ = self.pane.render(None); } pub fn render_guest_modal_for_client(&mut self, client_id: ClientId) -> Result<()> { let err_context = || format!("failed to render guest modal for client {client_id}"); let selection = self.pane.guest_modal_selection(client_id).unwrap_or(0); let session_name = self .pane .guest_session_name() .unwrap_or_else(|| String::from("unknown")); let columns = self.pane.get_content_columns(); let rows = self.pane.get_content_rows(); let content_x = self.pane.get_content_x(); let content_y = self.pane.get_content_y(); let shortcuts = self.pane.guest_modal_shortcuts(); let chunks = crate::panes::nested_session_modal::guest_modal_chunks( columns, rows, content_x, content_y, &self.style, &session_name, selection, &shortcuts, ); self.output .add_character_chunks_to_client(client_id, chunks, self.z_index) .with_context(err_context)?; Ok(()) } pub fn render_fake_cursor_if_needed(&mut self, client_id: ClientId) -> Result<()> { let pane_focused_for_client_id = self.focused_clients.contains(&client_id); let pane_focused_for_different_client = self .focused_clients .iter() .filter(|&&c_id| c_id != client_id) .count() > 0; if pane_focused_for_different_client && !pane_focused_for_client_id { let fake_cursor_client_id = self .focused_clients .iter() .find(|&&c_id| c_id != client_id) .with_context(|| { format!("failed to render fake cursor if needed for client {client_id}") })?; if let Some(colors) = client_id_to_colors( *fake_cursor_client_id, self.style.colors.multiplayer_user_colors, ) { let cursor_is_visible = self .pane .cursor_coordinates(Some(*fake_cursor_client_id)) .and_then(|(x, y, is_visible)| { if is_visible { Some(self.output.cursor_is_visible( self.pane.x() + x, self.pane.y() + y, self.z_index, )) } else { None } }) .unwrap_or(false); if cursor_is_visible { if let Some(vte_output) = self.pane.render_fake_cursor(colors.0, colors.1) { self.output.add_post_vte_instruction_to_client( client_id, &format!( "\u{1b}[{};{}H\u{1b}[m{}", self.pane.y() + 1, self.pane.x() + 1, vte_output ), ); } } } } Ok(()) } pub fn render_terminal_title_if_needed( &mut self, client_id: ClientId, client_mode: InputMode, previous_title: &mut Option, ) { if !self.focused_clients.contains(&client_id) { return; } let vte_output = self.pane.render_terminal_title(client_mode); if let Some(previous_title) = previous_title { if *previous_title == vte_output { return; } } *previous_title = Some(vte_output.clone()); self.output .add_post_vte_instruction_to_client(client_id, &vte_output); } pub fn render_pane_frame( &mut self, client_id: ClientId, client_mode: InputMode, session_is_mirrored: bool, pane_is_floating: bool, pane_is_selectable: bool, ) -> Result<()> { let err_context = || format!("failed to render pane frame for client {client_id}"); let pane_focused_for_client_id = self.focused_clients.contains(&client_id); let other_focused_clients: Vec = self .focused_clients .iter() .filter(|&&c_id| c_id != client_id) .copied() .collect(); 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 { Some(*other_focused_clients.first().with_context(err_context)?) } else { None }; let pane_is_stacked = self.pane.current_geom().is_stacked(); let pane_is_in_group = self .current_pane_group .get(&client_id) .map(|p| p.contains(&self.pane.pid())) .unwrap_or(false); let stack_list_entry = self.stack_list_entry_width.map(|width| StackListEntry { width, label: self.pane.stack_list_entry_label(), is_selected: self.stack_list_entry_is_selected, stack_is_focused: self.stack_list_entry_stack_is_focused, index: self.stack_list_entry_index, style: self.stack_list_entry_style, is_emphasized: pane_is_in_group || (self .mouse_is_hovering_over_pane_for_clients .contains(&client_id) && !pane_focused_for_client_id), }); let frame_is_dimmed = self.frame_is_dimmed_for_client(client_id); let guest_choice_indicator = self.pane.guest_choice_indicator(client_id); let frame_params = if session_is_mirrored { FrameParams { focused_client, is_main_client: pane_focused_for_client_id, other_focused_clients: vec![], style: self.style, color: frame_color.map(|c| c.0), other_cursors_exist_in_session: false, pane_is_stacked_over: self.pane_is_stacked_over, pane_is_stacked_under: self.pane_is_stacked_under, pane_is_stacked, should_draw_pane_frames: self.should_draw_pane_frames, pane_is_floating, content_offset: self.pane.get_content_offset(), mouse_is_hovering_over_pane: self .mouse_is_hovering_over_pane_for_clients .contains(&client_id), pane_is_selectable, show_help_text: self.show_help_text, status_bar: self.pane.status_bar_text(), status_bar_buttons: self.pane.status_bar_buttons(), hovered_status_bar_button: self.pane.hovered_status_bar_button(client_id), highlight_tooltip: highlight_tooltip.clone(), omit_title: self.omit_title, frame_geom_override: self.frame_geom_override, stack_list_entry: stack_list_entry.clone(), blank_title: self.blank_title, mouse_scroll_resize: self.mouse_scroll_resize, dimmed: frame_is_dimmed, guest_choice_indicator, } } else { FrameParams { focused_client, is_main_client: pane_focused_for_client_id, other_focused_clients, style: self.style, color: frame_color.map(|c| c.0), other_cursors_exist_in_session: self.multiple_users_exist_in_session, pane_is_stacked_over: self.pane_is_stacked_over, pane_is_stacked_under: self.pane_is_stacked_under, pane_is_stacked, should_draw_pane_frames: self.should_draw_pane_frames, pane_is_floating, content_offset: self.pane.get_content_offset(), mouse_is_hovering_over_pane: self .mouse_is_hovering_over_pane_for_clients .contains(&client_id), pane_is_selectable, show_help_text: self.show_help_text, status_bar: self.pane.status_bar_text(), status_bar_buttons: self.pane.status_bar_buttons(), hovered_status_bar_button: self.pane.hovered_status_bar_button(client_id), highlight_tooltip, omit_title: self.omit_title, frame_geom_override: self.frame_geom_override, stack_list_entry, blank_title: self.blank_title, mouse_scroll_resize: self.mouse_scroll_resize, dimmed: frame_is_dimmed, guest_choice_indicator, } }; if let Some((mut frame_terminal_characters, vte_output)) = self .pane .render_frame(client_id, frame_params, client_mode) .with_context(err_context)? { if self.frame_is_dimmed_for_client(client_id) { dim_character_chunks(&mut frame_terminal_characters); } self.output .add_character_chunks_to_client(client_id, frame_terminal_characters, self.z_index) .with_context(err_context)?; if let Some(vte_output) = vte_output { self.output .add_post_vte_instruction_to_client(client_id, &vte_output); } } Ok(()) } pub fn render_pane_boundaries( &self, client_id: ClientId, client_mode: InputMode, boundaries: &mut Boundaries, session_is_mirrored: bool, pane_is_on_top_of_stack: bool, pane_is_on_bottom_of_stack: bool, ) { let color = self.frame_color(client_id, client_mode, session_is_mirrored); boundaries.add_rect( self.pane.as_ref(), color, pane_is_on_top_of_stack, pane_is_on_bottom_of_stack, self.pane_is_stacked_under, ); } fn frame_color( &self, client_id: ClientId, mode: InputMode, session_is_mirrored: bool, ) -> Option<(PaletteColor, usize)> { // (color, color_precedence) (the color_precedence is used // for the no-pane-frames mode) let pane_focused_for_client_id = self.focused_clients.contains(&client_id); let pane_is_in_group = self .current_pane_group .get(&client_id) .map(|p| p.contains(&self.pane.pid())) .unwrap_or(false); if self.pane.frame_color_override().is_some() && !pane_is_in_group { self.pane .frame_color_override() .map(|override_color| (override_color, 4)) } else if pane_is_in_group && !pane_focused_for_client_id { Some((self.style.colors.frame_highlight.emphasis_0, 2)) } else if pane_is_in_group && pane_focused_for_client_id { Some((self.style.colors.frame_highlight.emphasis_1, 3)) } else if pane_focused_for_client_id { match mode { InputMode::Normal | InputMode::Locked => { if session_is_mirrored || !self.multiple_users_exist_in_session { Some((self.style.colors.frame_selected.base, 3)) } else { let colors = client_id_to_colors( client_id, self.style.colors.multiplayer_user_colors, ); colors.map(|colors| (colors.0, 3)) } }, _ => Some((self.style.colors.frame_highlight.base, 3)), } } else if self .mouse_is_hovering_over_pane_for_clients .contains(&client_id) { Some((self.style.colors.frame_highlight.base, 1)) } else { self.style .colors .frame_unselected .map(|frame| (frame.base, 0)) } } }