aboutsummaryrefslogtreecommitdiffstats
path: root/zellij-server
diff options
context:
space:
mode:
authorAram Drevekenin <aram@poor.dev>2026-07-04 16:27:08 +0200
committerGitHub <noreply@github.com>2026-07-04 16:27:08 +0200
commit82c4a24d701ecf9a48aa01bcc5c0bb3882747fe7 (patch)
tree906e2f207dc1f5bf01112d7889663a2ce0a4cf2f /zellij-server
parentReduce compilation time (#5321) (diff)
downloadsl0ppij-82c4a24d701ecf9a48aa01bcc5c0bb3882747fe7.tar.gz
sl0ppij-82c4a24d701ecf9a48aa01bcc5c0bb3882747fe7.tar.xz
sl0ppij-82c4a24d701ecf9a48aa01bcc5c0bb3882747fe7.zip
Stacked lists (#5331)
* initial implementation * improve ui * fix: stacked resize interaction * title list boundaries fix * fix BEL * mouse parity * float/embed + bulk-op features * fix in-place bug * improve ui * config live reload * improve ui * fix issues, add exit status * fix for serialization * fix tests * integration tests * comments * refactor: duplicate title logic * refactor: consolidate logic * fix: flaky tests * add a stacked swap layout * rustfmt * fix e2e * add pr
Diffstat (limited to 'zellij-server')
-rw-r--r--zellij-server/src/lib.rs1
-rw-r--r--zellij-server/src/panes/plugin_pane.rs8
-rw-r--r--zellij-server/src/panes/terminal_pane.rs29
-rw-r--r--zellij-server/src/panes/tiled_panes/mod.rs60
-rw-r--r--zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__override_layout_plugin_command.snap108
-rw-r--r--zellij-server/src/screen.rs48
-rw-r--r--zellij-server/src/tab/mod.rs879
-rw-r--r--zellij-server/src/tab/mouse_handler.rs13
-rw-r--r--zellij-server/src/tab/unit/layout_applier_tests.rs8
-rw-r--r--zellij-server/src/tab/unit/tab_integration_tests.rs9
-rw-r--r--zellij-server/src/tab/unit/tab_tests.rs3
-rw-r--r--zellij-server/src/ui/pane_boundaries_frame.rs178
-rw-r--r--zellij-server/src/ui/pane_contents_and_ui.rs46
-rw-r--r--zellij-server/src/unit/screen_tests.rs6
14 files changed, 1317 insertions, 79 deletions
diff --git a/zellij-server/src/lib.rs b/zellij-server/src/lib.rs
index 62f8d651..783080dc 100644
--- a/zellij-server/src/lib.rs
+++ b/zellij-server/src/lib.rs
@@ -427,6 +427,7 @@ impl SessionMetaData {
rounded_corners: new_config.ui.pane_frames.rounded_corners,
hide_session_name: new_config.ui.pane_frames.hide_session_name,
stacked_resize: new_config.options.stacked_resize.unwrap_or(true),
+ stacked_pane_list: new_config.options.stacked_pane_list.unwrap_or(true),
default_editor: new_config.options.scrollback_editor.clone(),
advanced_mouse_actions: new_config
.options
diff --git a/zellij-server/src/panes/plugin_pane.rs b/zellij-server/src/panes/plugin_pane.rs
index 6f2d5980..8eec16fc 100644
--- a/zellij-server/src/panes/plugin_pane.rs
+++ b/zellij-server/src/panes/plugin_pane.rs
@@ -428,10 +428,14 @@ impl Pane for PluginPane {
if self.borderless {
return Ok(None);
}
- let frame_geom = self.current_geom();
+ let frame_geom = frame_params
+ .frame_geom_override
+ .unwrap_or_else(|| self.current_geom());
let grid = get_or_create_grid!(self, client_id);
let err_context = || format!("failed to render frame for client {client_id}");
- let pane_title = if let Some(text_color_override) = self
+ let pane_title = if frame_params.blank_title {
+ String::new()
+ } else if let Some(text_color_override) = self
.pane_frame_color_override
.as_ref()
.and_then(|(_color, text)| text.as_ref())
diff --git a/zellij-server/src/panes/terminal_pane.rs b/zellij-server/src/panes/terminal_pane.rs
index 84c2e970..429d025b 100644
--- a/zellij-server/src/panes/terminal_pane.rs
+++ b/zellij-server/src/panes/terminal_pane.rs
@@ -414,27 +414,24 @@ impl Pane for TerminalPane {
modifier_text.push(']');
}
format!("SEARCHING: {}{}", self.search_term, modifier_text)
- } else if self.pane_name.is_empty() {
- self.grid
- .title
- .clone()
- .unwrap_or_else(|| self.pane_title.clone())
} else {
- self.pane_name.clone()
+ self.current_title()
};
- let pane_title = if let Some(text_color_override) = self
+ let pane_title = if frame_params.blank_title {
+ String::new()
+ } else if let Some(text_color_override) = self
.pane_frame_color_override
.as_ref()
.and_then(|(_color, text)| text.as_ref())
{
text_color_override.into()
- } else if self.has_bell_notification {
- format!("{} [!]", normal_title)
} else {
- normal_title
+ self.title_with_bell_indicator(normal_title)
};
- let frame_geom = self.current_geom();
+ let frame_geom = frame_params
+ .frame_geom_override
+ .unwrap_or_else(|| self.current_geom());
let is_pinned = frame_geom.is_pinned;
let mut frame = PaneFrame::new(
frame_geom.into(),
@@ -897,6 +894,9 @@ impl Pane for TerminalPane {
self.pane_name.to_owned()
}
}
+ fn stack_list_entry_label(&self) -> String {
+ self.title_with_bell_indicator(self.current_title())
+ }
fn custom_title(&self) -> Option<String> {
if self.pane_name.is_empty() {
None
@@ -1079,6 +1079,13 @@ impl Pane for TerminalPane {
}
impl TerminalPane {
+ fn title_with_bell_indicator(&self, title: String) -> String {
+ if self.has_bell_notification {
+ format!("{} [!]", title)
+ } else {
+ title
+ }
+ }
#[allow(clippy::too_many_arguments)]
pub fn new(
pid: u32,
diff --git a/zellij-server/src/panes/tiled_panes/mod.rs b/zellij-server/src/panes/tiled_panes/mod.rs
index e3f66c37..0c17aa01 100644
--- a/zellij-server/src/panes/tiled_panes/mod.rs
+++ b/zellij-server/src/panes/tiled_panes/mod.rs
@@ -63,6 +63,8 @@ pub struct TiledPanes {
mode_info: Rc<RefCell<HashMap<ClientId, ModeInfo>>>,
character_cell_size: Rc<RefCell<Option<SizeInPixels>>>,
stacked_resize: Rc<RefCell<bool>>,
+ stacked_pane_list: Rc<RefCell<bool>>,
+ reserved_top_rows: Rc<RefCell<HashMap<PaneId, usize>>>,
default_mode_info: ModeInfo,
style: Style,
session_is_mirrored: bool,
@@ -87,6 +89,8 @@ impl TiledPanes {
mode_info: Rc<RefCell<HashMap<ClientId, ModeInfo>>>,
character_cell_size: Rc<RefCell<Option<SizeInPixels>>>,
stacked_resize: Rc<RefCell<bool>>,
+ stacked_pane_list: Rc<RefCell<bool>>,
+ reserved_top_rows: Rc<RefCell<HashMap<PaneId, usize>>>,
session_is_mirrored: bool,
pane_frame_style: PaneFrameStyle,
default_mode_info: ModeInfo,
@@ -103,6 +107,8 @@ impl TiledPanes {
mode_info,
character_cell_size,
stacked_resize,
+ stacked_pane_list,
+ reserved_top_rows,
default_mode_info,
style,
session_is_mirrored,
@@ -570,13 +576,40 @@ impl TiledPanes {
.positions_and_sizes_of_all_stacks()
.unwrap_or_else(|| Default::default())
};
+ let reserved_top_rows = if self.fullscreen_is_active.is_some() {
+ HashMap::new()
+ } else {
+ self.reserved_top_rows.borrow().clone()
+ };
for pane in self.panes.values_mut() {
if !pane.borderless() {
pane.set_frame(draws_full_frames);
}
+ let reserved_rows = reserved_top_rows.get(&pane.pid()).copied().unwrap_or(0);
+
#[allow(clippy::if_same_then_else)]
- if draws_full_frames && !pane.borderless() {
+ if reserved_rows > 0 {
+ if draws_full_frames && !pane.borderless() {
+ pane.set_content_offset(Offset {
+ top: 1 + reserved_rows,
+ bottom: 1,
+ left: 1,
+ right: 1,
+ });
+ } else {
+ let position_and_size = pane.current_geom();
+ let (pane_columns_offset, pane_rows_offset) =
+ pane_content_offset(&position_and_size, &viewport);
+ let visible_member_title_rows = if draws_full_frames { 1 } else { 0 };
+ pane.set_content_offset(Offset {
+ top: visible_member_title_rows + reserved_rows,
+ bottom: pane_rows_offset,
+ left: 0,
+ right: pane_columns_offset,
+ });
+ }
+ } else if draws_full_frames && !pane.borderless() {
pane.set_content_offset(Offset::frame(1));
} else if draws_full_frames && pane.borderless() {
pane.set_content_offset(Offset::default());
@@ -1124,6 +1157,19 @@ impl TiledPanes {
&& help_text_visible.get(client_id).copied().unwrap_or(false)
}) && selectable_pane_count > 1
&& self.fullscreen_is_active.is_none();
+ let reserved_rows_for_pane = if self.fullscreen_is_active.is_some() {
+ 0
+ } else {
+ { self.reserved_top_rows.borrow().get(&pane.pid()).copied() }.unwrap_or(0)
+ };
+ let visible_member_frame_override = if reserved_rows_for_pane > 0 {
+ let mut geom = pane.current_geom();
+ geom.y += reserved_rows_for_pane;
+ geom.rows.decrease_inner(reserved_rows_for_pane);
+ Some(geom)
+ } else {
+ None
+ };
let mut pane_contents_and_ui = PaneContentsAndUi::new(
pane,
output,
@@ -1137,8 +1183,10 @@ impl TiledPanes {
&mouse_hover_pane_id,
current_pane_group.clone(),
show_help_text,
- omit_pane_title,
+ omit_pane_title && reserved_rows_for_pane == 0,
);
+ pane_contents_and_ui.set_frame_geom_override(visible_member_frame_override);
+ pane_contents_and_ui.set_blank_title(reserved_rows_for_pane > 0);
for client_id in &connected_clients {
let client_mode = self
.mode_info
@@ -1166,7 +1214,9 @@ impl TiledPanes {
pane_is_selectable,
)
.with_context(err_context)?;
- } else if self.pane_frame_style.draws_titles() || pane_is_stacked {
+ } else if (self.pane_frame_style.draws_titles() || pane_is_stacked)
+ && reserved_rows_for_pane == 0
+ {
pane_contents_and_ui
.render_pane_frame(
*client_id,
@@ -1241,6 +1291,10 @@ impl TiledPanes {
pub fn get_panes(&self) -> impl Iterator<Item = (&PaneId, &Box<dyn Pane>)> {
self.panes.iter()
}
+ pub fn position_and_size_of_stack(&mut self, pane_id: &PaneId) -> Option<PaneGeom> {
+ StackedPanes::new_from_btreemap(&mut self.panes, &self.panes_to_hide)
+ .position_and_size_of_stack(pane_id)
+ }
pub fn set_geom_for_pane_with_run(
&mut self,
run: Option<Run>,
diff --git a/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__override_layout_plugin_command.snap b/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__override_layout_plugin_command.snap
index f4d7fae5..2ef7c7a3 100644
--- a/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__override_layout_plugin_command.snap
+++ b/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__override_layout_plugin_command.snap
@@ -1375,6 +1375,112 @@ Some(
(
{
MinPanes(
+ 3,
+ ): TiledPaneLayout {
+ children_split_direction: Horizontal,
+ name: None,
+ children: [
+ TiledPaneLayout {
+ children_split_direction: Horizontal,
+ name: None,
+ children: [
+ TiledPaneLayout {
+ children_split_direction: Horizontal,
+ name: None,
+ children: [],
+ split_size: None,
+ run: None,
+ borderless: None,
+ focus: None,
+ external_children_index: Some(
+ 0,
+ ),
+ children_are_stacked: true,
+ is_expanded_in_stack: false,
+ exclude_from_sync: None,
+ run_instructions_to_ignore: [],
+ hide_floating_panes: false,
+ pane_initial_contents: None,
+ default_fg: None,
+ default_bg: None,
+ },
+ ],
+ split_size: None,
+ run: None,
+ borderless: None,
+ focus: None,
+ external_children_index: None,
+ children_are_stacked: false,
+ is_expanded_in_stack: false,
+ exclude_from_sync: None,
+ run_instructions_to_ignore: [],
+ hide_floating_panes: false,
+ pane_initial_contents: None,
+ default_fg: None,
+ default_bg: None,
+ },
+ TiledPaneLayout {
+ children_split_direction: Horizontal,
+ name: None,
+ children: [],
+ split_size: Some(
+ Fixed(
+ 1,
+ ),
+ ),
+ run: Some(
+ Plugin(
+ Alias(
+ PluginAlias {
+ name: "compact-bar",
+ configuration: Some(
+ PluginUserConfiguration(
+ {},
+ ),
+ ),
+ initial_cwd: None,
+ run_plugin: None,
+ },
+ ),
+ ),
+ ),
+ borderless: Some(
+ true,
+ ),
+ focus: None,
+ external_children_index: None,
+ children_are_stacked: false,
+ is_expanded_in_stack: false,
+ exclude_from_sync: None,
+ run_instructions_to_ignore: [],
+ hide_floating_panes: false,
+ pane_initial_contents: None,
+ default_fg: None,
+ default_bg: None,
+ },
+ ],
+ split_size: None,
+ run: None,
+ borderless: None,
+ focus: None,
+ external_children_index: None,
+ children_are_stacked: false,
+ is_expanded_in_stack: false,
+ exclude_from_sync: None,
+ run_instructions_to_ignore: [],
+ hide_floating_panes: false,
+ pane_initial_contents: None,
+ default_fg: None,
+ default_bg: None,
+ },
+ },
+ Some(
+ "stacked",
+ ),
+ ),
+ (
+ {
+ MinPanes(
4,
): TiledPaneLayout {
children_split_direction: Horizontal,
@@ -1512,7 +1618,7 @@ Some(
},
},
Some(
- "stacked",
+ "half-stacked",
),
),
],
diff --git a/zellij-server/src/screen.rs b/zellij-server/src/screen.rs
index 97831ba7..34c9ced5 100644
--- a/zellij-server/src/screen.rs
+++ b/zellij-server/src/screen.rs
@@ -744,6 +744,7 @@ pub enum ScreenInstruction {
rounded_corners: bool,
hide_session_name: bool,
stacked_resize: bool,
+ stacked_pane_list: bool,
default_editor: Option<PathBuf>,
advanced_mouse_actions: bool,
mouse_hover_effects: bool,
@@ -1411,6 +1412,7 @@ pub(crate) struct Screen {
pixel_dimensions: PixelDimensions,
character_cell_size: Rc<RefCell<Option<SizeInPixels>>>,
stacked_resize: Rc<RefCell<bool>>,
+ stacked_pane_list: Rc<RefCell<bool>>,
sixel_image_store: Rc<RefCell<SixelImageStore>>,
terminal_emulator_colors: Rc<RefCell<Palette>>,
terminal_emulator_color_codes: Rc<RefCell<HashMap<usize, String>>>,
@@ -1549,6 +1551,7 @@ impl Screen {
layout_dir: Option<PathBuf>,
explicitly_disable_kitty_keyboard_protocol: bool,
stacked_resize: bool,
+ stacked_pane_list: bool,
default_editor: Option<PathBuf>,
web_clients_allowed: bool,
web_sharing: WebSharing,
@@ -1573,6 +1576,7 @@ impl Screen {
pixel_dimensions: Default::default(),
character_cell_size: Rc::new(RefCell::new(None)),
stacked_resize: Rc::new(RefCell::new(stacked_resize)),
+ stacked_pane_list: Rc::new(RefCell::new(stacked_pane_list)),
sixel_image_store: Rc::new(RefCell::new(SixelImageStore::default())),
style: client_attributes.style,
connected_clients: Rc::new(RefCell::new(HashMap::new())),
@@ -2022,6 +2026,9 @@ impl Screen {
.with_context(err_context)?;
}
destination_tab.set_force_render();
+ if destination_tab.has_stack_lists() {
+ destination_tab.set_should_clear_display_before_rendering();
+ }
destination_tab.visible(true).with_context(err_context)?;
}
Ok(())
@@ -3305,6 +3312,7 @@ impl Screen {
self.size,
self.character_cell_size.clone(),
self.stacked_resize.clone(),
+ self.stacked_pane_list.clone(),
self.sixel_image_store.clone(),
self.bus
.os_input
@@ -4604,12 +4612,13 @@ impl Screen {
) -> Result<()> {
let err_context = || "failed break pane out of tab".to_string();
let active_tab = self.get_active_tab_mut(client_id)?;
+ let active_pane_id = active_tab
+ .get_active_pane_id(client_id)
+ .with_context(err_context)?;
if active_tab.get_selectable_tiled_panes_count() > 1
|| active_tab.get_visible_selectable_floating_panes_count() > 0
+ || active_tab.pane_is_stack_list_member(&active_pane_id)
{
- let active_pane_id = active_tab
- .get_active_pane_id(client_id)
- .with_context(err_context)?;
let active_pane = active_tab
.extract_pane(active_pane_id, false)
.with_context(err_context)?;
@@ -4650,9 +4659,6 @@ impl Screen {
None,
))?;
} else {
- let active_pane_id = active_tab
- .get_active_pane_id(client_id)
- .with_context(err_context)?;
self.bus
.senders
.send_to_background_jobs(BackgroundJob::DisplayPaneError(
@@ -5027,6 +5033,7 @@ impl Screen {
rounded_corners: bool,
hide_session_name: bool,
stacked_resize: bool,
+ stacked_pane_list: bool,
default_editor: Option<PathBuf>,
advanced_mouse_actions: bool,
mouse_hover_effects: bool,
@@ -5065,6 +5072,9 @@ impl Screen {
{
*self.stacked_resize.borrow_mut() = stacked_resize;
}
+ {
+ *self.stacked_pane_list.borrow_mut() = stacked_pane_list;
+ }
if let Some(copy_to_clipboard) = copy_to_clipboard {
self.copy_options.clipboard = copy_to_clipboard;
}
@@ -5081,6 +5091,7 @@ impl Screen {
tab.update_mouse_hover_effects(mouse_hover_effects);
tab.update_focus_follows_mouse(focus_follows_mouse);
tab.update_mouse_click_through(mouse_click_through);
+ tab.sync_stacked_pane_list_mode();
}
// Clear hover state when disabled
@@ -5527,6 +5538,7 @@ impl Screen {
);
}
+ let stack_list_geoms = tab.stack_list_serialization_geoms();
let tiled_panes: Vec<PaneLayoutMetadata> = tab
.get_tiled_panes()
.map(|(pane_id, p)| {
@@ -5535,14 +5547,21 @@ impl Screen {
// is currently only the case the scrollback editing panes, and
// when dumping the layout we want the "real" pane and not the
// editor pane
- match suppressed_panes.remove(pane_id) {
+ let geom_override = stack_list_geoms.get(pane_id).copied();
+ let (pane_id, p) = match suppressed_panes.remove(pane_id) {
Some((is_scrollback_editor, suppressed_pane)) if *is_scrollback_editor => {
(suppressed_pane.pid(), suppressed_pane)
},
_ => (*pane_id, p),
- }
+ };
+ (
+ pane_id,
+ p,
+ geom_override.unwrap_or_else(|| p.position_and_size()),
+ )
})
- .map(|(pane_id, p)| {
+ .chain(tab.hidden_stack_list_members_for_serialization())
+ .map(|(pane_id, p, geom)| {
let focused_clients: Vec<ClientId> = active_pane_ids
.iter()
.filter_map(|(c_id, p_id)| {
@@ -5552,7 +5571,7 @@ impl Screen {
let (default_fg, default_bg) = p.get_pane_default_colors();
PaneLayoutMetadata::new(
pane_id,
- p.position_and_size(),
+ geom,
p.borderless(),
p.invoked_with().clone(),
p.custom_title(),
@@ -6076,6 +6095,11 @@ fn find_already_running_panes(
let running_tiled_instructions: Vec<Option<Run>> = active_tab
.get_tiled_panes()
.map(|(_, pane)| pane.invoked_with().clone())
+ .chain(
+ active_tab
+ .suppressed_stack_list_members()
+ .map(|(_, pane)| pane.invoked_with().clone()),
+ )
.collect();
let mut tiled_to_ignore = Vec::new();
@@ -6191,6 +6215,7 @@ pub(crate) fn screen_thread_main(
.unwrap_or(false); // by default, we try to support this if the terminal supports it and
// the program running inside a pane requests it
let stacked_resize = config_options.stacked_resize.unwrap_or(true);
+ let stacked_pane_list = config_options.stacked_pane_list.unwrap_or(true);
let web_clients_allowed = config_options
.web_sharing
.map(|s| s.web_clients_allowed())
@@ -6234,6 +6259,7 @@ pub(crate) fn screen_thread_main(
layout_dir,
explicitly_disable_kitty_keyboard_protocol,
stacked_resize,
+ stacked_pane_list,
default_editor,
web_clients_allowed,
web_sharing,
@@ -9379,6 +9405,7 @@ pub(crate) fn screen_thread_main(
rounded_corners,
hide_session_name,
stacked_resize,
+ stacked_pane_list,
default_editor,
advanced_mouse_actions,
mouse_hover_effects,
@@ -9403,6 +9430,7 @@ pub(crate) fn screen_thread_main(
rounded_corners,
hide_session_name,
stacked_resize,
+ stacked_pane_list,
default_editor,
advanced_mouse_actions,
mouse_hover_effects,
diff --git a/zellij-server/src/tab/mod.rs b/zellij-server/src/tab/mod.rs
index a2601966..18f3e0a7 100644
--- a/zellij-server/src/tab/mod.rs
+++ b/zellij-server/src/tab/mod.rs
@@ -13,6 +13,7 @@ pub use mouse_handler::{MouseEffect, MouseHandler, PaneEdge, PaneResizeState};
use std::env::temp_dir;
use std::net::IpAddr;
use std::path::PathBuf;
+use unicode_width::UnicodeWidthStr;
use uuid::Uuid;
use zellij_utils::data::PaneContents;
use zellij_utils::data::{
@@ -33,7 +34,10 @@ 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 crate::ui::{
+ loading_indication::LoadingIndication, pane_boundaries_frame::FrameParams,
+ pane_contents_and_ui::PaneContentsAndUi,
+};
use layout_applier::LayoutApplier;
use swap_layouts::SwapLayouts;
@@ -70,7 +74,7 @@ use zellij_utils::{
options::PaneFrameStyle,
parse_keys,
},
- pane_size::{Offset, PaneGeom, Size, SizeInPixels, Viewport},
+ pane_size::{Dimension, Offset, PaneGeom, Size, SizeInPixels, Viewport},
};
#[macro_export]
@@ -151,6 +155,26 @@ const MAX_PENDING_VTE_EVENTS: usize = 7000;
type HoldForCommand = Option<RunCommand>;
pub type SuppressedPanes = HashMap<PaneId, (bool, Box<dyn Pane>)>; // bool => is scrollback editor
+pub type StackListId = usize;
+
+#[derive(Debug, Clone)]
+pub struct StackList {
+ pub members: Vec<PaneId>,
+ pub visible: PaneId,
+}
+
+impl StackList {
+ pub fn rank_of(&self, pane_id: &PaneId) -> Option<usize> {
+ self.members.iter().position(|p| p == pane_id)
+ }
+}
+
+enum StackListRemoval {
+ NotRemoved,
+ DissolvedInPlace,
+ Removed(Option<Box<dyn Pane>>),
+}
+
enum BufferedTabInstruction {
SetPaneSelectable(PaneId, bool),
HandlePtyBytes(u32, VteBytes),
@@ -168,6 +192,13 @@ pub(crate) struct Tab {
tiled_panes: TiledPanes,
floating_panes: FloatingPanes,
suppressed_panes: SuppressedPanes,
+ stacked_pane_list: Rc<RefCell<bool>>,
+ reserved_top_rows: Rc<RefCell<HashMap<PaneId, usize>>>,
+ stack_lists: HashMap<StackListId, StackList>,
+ stack_list_of_member: HashMap<PaneId, StackListId>,
+ stack_list_parked_pairs: HashMap<PaneId, PaneId>,
+ next_stack_list_id: StackListId,
+ stack_list_session_is_mirrored: bool,
max_panes: Option<usize>,
viewport: Rc<RefCell<Viewport>>, // includes all non-UI panes
display_area: Rc<RefCell<Size>>, // includes all panes (including eg. the status bar and tab bar in the default layout)
@@ -628,6 +659,9 @@ pub trait Pane {
fn start_loading_indication(&mut self, _loading_indication: LoadingIndication) {} // only relevant for plugins
fn progress_animation_offset(&mut self) {} // only relevant for plugins
fn current_title(&self) -> String;
+ fn stack_list_entry_label(&self) -> String {
+ self.current_title()
+ }
fn custom_title(&self) -> Option<String>;
fn has_explicit_title(&self) -> bool {
false
@@ -745,6 +779,7 @@ impl Tab {
display_area: Size,
character_cell_size: Rc<RefCell<Option<SizeInPixels>>>,
stacked_resize: Rc<RefCell<bool>>,
+ stacked_pane_list: Rc<RefCell<bool>>,
sixel_image_store: Rc<RefCell<SixelImageStore>>,
os_api: Box<dyn ServerOsApi>,
senders: ThreadSenders,
@@ -800,6 +835,8 @@ impl Tab {
let display_area = Rc::new(RefCell::new(display_area));
let connected_clients = Rc::new(RefCell::new(connected_clients));
let mode_info = Rc::new(RefCell::new(HashMap::new()));
+ let reserved_top_rows: Rc<RefCell<HashMap<PaneId, usize>>> =
+ Rc::new(RefCell::new(HashMap::new()));
let tiled_panes = TiledPanes::new(
display_area.clone(),
@@ -809,6 +846,8 @@ impl Tab {
mode_info.clone(),
character_cell_size.clone(),
stacked_resize.clone(),
+ stacked_pane_list.clone(),
+ reserved_top_rows.clone(),
session_is_mirrored,
pane_frame_style,
default_mode_info.clone(),
@@ -842,6 +881,13 @@ impl Tab {
tiled_panes,
floating_panes,
suppressed_panes: HashMap::new(),
+ stacked_pane_list,
+ reserved_top_rows,
+ stack_lists: HashMap::new(),
+ stack_list_of_member: HashMap::new(),
+ stack_list_parked_pairs: HashMap::new(),
+ next_stack_list_id: 0,
+ stack_list_session_is_mirrored: session_is_mirrored,
name: name.clone(),
prev_name: name,
default_name,
@@ -908,6 +954,645 @@ impl Tab {
}
}
+ pub fn stacked_pane_list_is_active(&self) -> bool {
+ *self.stacked_pane_list.borrow()
+ }
+ pub fn pane_is_stack_list_member(&self, pane_id: &PaneId) -> bool {
+ self.stack_list_of_member.contains_key(pane_id)
+ }
+ pub fn has_stack_lists(&self) -> bool {
+ !self.stack_lists.is_empty()
+ }
+ fn stack_list_id_of_member(&self, pane_id: &PaneId) -> Option<StackListId> {
+ self.stack_list_of_member.get(pane_id).copied()
+ }
+ pub fn pane_is_hidden_stack_list_member(&self, pane_id: &PaneId) -> bool {
+ self.stack_list_id_of_member(pane_id)
+ .and_then(|stack_list_id| self.stack_lists.get(&stack_list_id))
+ .map(|list| list.visible != *pane_id)
+ .unwrap_or(false)
+ }
+ pub fn focus_hidden_stack_list_member(&mut self, pane_id: PaneId, client_id: ClientId) -> bool {
+ self.swap_in_hidden_stack_list_member(pane_id, Some(client_id))
+ }
+ fn swap_in_hidden_stack_list_member(
+ &mut self,
+ pane_id: PaneId,
+ client_id: Option<ClientId>,
+ ) -> bool {
+ if !self.pane_is_hidden_stack_list_member(&pane_id) {
+ return false;
+ }
+ match self.stack_list_id_of_member(&pane_id) {
+ Some(stack_list_id) => {
+ self.select_stack_list_member(stack_list_id, pane_id, client_id);
+ true
+ },
+ None => false,
+ }
+ }
+ fn stack_list_member_at_point(&self, pane_id: PaneId, point: &Position) -> PaneId {
+ let Some(stack_list_id) = self.stack_list_id_of_member(&pane_id) else {
+ return pane_id;
+ };
+ let Some(list) = self.stack_lists.get(&stack_list_id) else {
+ return pane_id;
+ };
+ let Some(pane) = self.tiled_panes.get_pane(pane_id) else {
+ return pane_id;
+ };
+ let geom = pane.current_geom();
+ if point.line() < geom.y as isize {
+ return pane_id;
+ }
+ let rank = (point.line() - geom.y as isize) as usize;
+ list.members.get(rank).copied().unwrap_or(pane_id)
+ }
+ pub fn suppressed_stack_list_members(&self) -> impl Iterator<Item = (&PaneId, &Box<dyn Pane>)> {
+ self.suppressed_panes
+ .iter()
+ .filter(|(pane_id, _)| self.stack_list_of_member.contains_key(pane_id))
+ .map(|(pane_id, (_, pane))| (pane_id, pane))
+ }
+ pub fn stack_list_serialization_geoms(&self) -> HashMap<PaneId, PaneGeom> {
+ let mut synthetic_geoms = HashMap::new();
+ if self.stack_lists.is_empty() {
+ return synthetic_geoms;
+ }
+ let mut next_synthetic_stack_id = self
+ .tiled_panes
+ .get_panes()
+ .filter_map(|(_, pane)| pane.position_and_size().stacked)
+ .max()
+ .map(|highest_stack_id| highest_stack_id + 1)
+ .unwrap_or(0);
+ for list in self.stack_lists.values() {
+ let Some(visible_pane) = self.tiled_panes.get_pane(list.visible) else {
+ continue;
+ };
+ let full_rect = visible_pane.position_and_size();
+ let collapsed_member_count = list.members.len().saturating_sub(1);
+ let mut running_y = full_rect.y;
+ for member in &list.members {
+ let mut member_geom = full_rect;
+ member_geom.stacked = Some(next_synthetic_stack_id);
+ member_geom.y = running_y;
+ if *member == list.visible {
+ member_geom.rows.set_inner(
+ full_rect
+ .rows
+ .as_usize()
+ .saturating_sub(collapsed_member_count),
+ );
+ } else {
+ member_geom.rows = Dimension::fixed(1);
+ member_geom.logical_position = self
+ .suppressed_panes
+ .get(member)
+ .and_then(|(_, pane)| pane.position_and_size().logical_position);
+ }
+ running_y += member_geom.rows.as_usize();
+ synthetic_geoms.insert(*member, member_geom);
+ }
+ next_synthetic_stack_id += 1;
+ }
+ synthetic_geoms
+ }
+ pub fn hidden_stack_list_members_for_serialization(
+ &self,
+ ) -> Vec<(PaneId, &Box<dyn Pane>, PaneGeom)> {
+ let synthetic_geoms = self.stack_list_serialization_geoms();
+ let mut hidden_members = vec![];
+ for list in self.stack_lists.values() {
+ for member in &list.members {
+ if *member == list.visible {
+ continue;
+ }
+ let Some(member_geom) = synthetic_geoms.get(member) else {
+ continue;
+ };
+ let resolved_pane = self
+ .stack_list_parked_pairs
+ .get(member)
+ .and_then(|parked_pid| self.suppressed_panes.get(parked_pid))
+ .filter(|(is_scrollback_editor, _)| *is_scrollback_editor)
+ .or_else(|| self.suppressed_panes.get(member));
+ if let Some((_, pane)) = resolved_pane {
+ hidden_members.push((pane.pid(), pane, *member_geom));
+ }
+ }
+ }
+ hidden_members
+ }
+ fn pane_parked_by(&self, id: &PaneId) -> Option<PaneId> {
+ self.suppressed_panes
+ .get(id)
+ .map(|(_, parked)| parked.pid())
+ .filter(|parked_pid| parked_pid != id)
+ }
+ fn adopt_parked_pair(&mut self, member: PaneId) {
+ if let Some(parked_pid) = self.stack_list_parked_pairs.remove(&member) {
+ if let Some(parked_entry) = self.suppressed_panes.remove(&parked_pid) {
+ self.suppressed_panes.insert(member, parked_entry);
+ }
+ }
+ }
+ fn refresh_or_dissolve_stack_list(&mut self, stack_list_id: StackListId) {
+ let (member_count, visible) = match self.stack_lists.get(&stack_list_id) {
+ Some(list) => (list.members.len(), list.visible),
+ None => return,
+ };
+ if member_count <= 1 {
+ self.dissolve_stack_list(stack_list_id);
+ } else {
+ self.reserved_top_rows
+ .borrow_mut()
+ .insert(visible, member_count);
+ self.tiled_panes.reapply_pane_frames();
+ self.resize_stack_list_hidden_members(stack_list_id);
+ }
+ }
+ fn select_stack_list_member(
+ &mut self,
+ stack_list_id: StackListId,
+ target_member: PaneId,
+ client_id: Option<ClientId>,
+ ) {
+ let current_visible = match self.stack_lists.get(&stack_list_id) {
+ Some(list) => list.visible,
+ None => return,
+ };
+ if current_visible == target_member {
+ return;
+ }
+ let pane_parked_by_current_visible = self.pane_parked_by(&current_visible);
+ let target_box = match self.suppressed_panes.remove(&target_member) {
+ Some((_is_scrollback_editor, pane)) => pane,
+ None => {
+ log::error!(
+ "stack-list member {:?} not found in suppressed panes",
+ target_member
+ );
+ return;
+ },
+ };
+ let removed_visible = self.tiled_panes.replace_pane(current_visible, target_box);
+ if let Some(removed_visible) = removed_visible {
+ self.insert_suppressed_pane(current_visible, (false, removed_visible));
+ if let Some(parked_pid) = pane_parked_by_current_visible {
+ self.stack_list_parked_pairs
+ .insert(current_visible, parked_pid);
+ }
+ }
+ self.adopt_parked_pair(target_member);
+ if let Some(list) = self.stack_lists.get_mut(&stack_list_id) {
+ list.visible = target_member;
+ }
+ self.reserved_top_rows.borrow_mut().remove(&current_visible);
+ if let Some(client_id) = client_id {
+ self.tiled_panes.focus_pane(target_member, client_id);
+ }
+ self.refresh_or_dissolve_stack_list(stack_list_id);
+ self.set_force_render();
+ self.set_should_clear_display_before_rendering();
+ }
+ fn move_focus_within_stack_list(&mut self, client_id: ClientId, down: bool) -> bool {
+ let active_pane_id = match self.tiled_panes.get_active_pane_id(client_id) {
+ Some(id) => id,
+ None => return false,
+ };
+ let stack_list_id = match self.stack_list_id_of_member(&active_pane_id) {
+ Some(id) => id,
+ None => return false,
+ };
+ let target_member = {
+ let list = match self.stack_lists.get(&stack_list_id) {
+ Some(l) => l,
+ None => return false,
+ };
+ if list.visible != active_pane_id {
+ return false;
+ }
+ let rank = match list.rank_of(&active_pane_id) {
+ Some(r) => r,
+ None => return false,
+ };
+ let target_rank = if down {
+ rank + 1
+ } else if rank == 0 {
+ return false;
+ } else {
+ rank - 1
+ };
+ match list.members.get(target_rank) {
+ Some(member) => *member,
+ None => return false,
+ }
+ };
+ self.select_stack_list_member(stack_list_id, target_member, Some(client_id));
+ true
+ }
+ fn close_stack_list_member(&mut self, id: PaneId, exit_status: Option<i32>) -> bool {
+ match self.remove_stack_list_member(id) {
+ StackListRemoval::Removed(removed) => {
+ if let (Some(exit_status), Some(mut removed)) = (exit_status, removed) {
+ removed.update_exit_status(exit_status);
+ }
+ true
+ },
+ StackListRemoval::DissolvedInPlace | StackListRemoval::NotRemoved => false,
+ }
+ }
+ fn remove_stack_list_member(&mut self, id: PaneId) -> StackListRemoval {
+ let stack_list_id = match self.stack_list_id_of_member(&id) {
+ Some(sid) => sid,
+ None => return StackListRemoval::NotRemoved,
+ };
+ let is_visible = self
+ .stack_lists
+ .get(&stack_list_id)
+ .map(|l| l.visible == id)
+ .unwrap_or(false);
+ let removed = if is_visible {
+ let (members, rank) = {
+ let list = match self.stack_lists.get(&stack_list_id) {
+ Some(l) => l,
+ None => return StackListRemoval::NotRemoved,
+ };
+ (list.members.clone(), list.rank_of(&id).unwrap_or(0))
+ };
+ let promote_target = members
+ .iter()
+ .skip(rank + 1)
+ .next()
+ .copied()
+ .or_else(|| members[..rank].iter().last().copied());
+ let promote_target = match promote_target {
+ Some(target) => target,
+ None => {
+ self.dissolve_stack_list(stack_list_id);
+ self.reserved_top_rows.borrow_mut().remove(&id);
+ return StackListRemoval::DissolvedInPlace;
+ },
+ };
+ let target_box = match self.suppressed_panes.remove(&promote_target) {
+ Some((_is_scrollback_editor, pane)) => pane,
+ None => {
+ log::error!("stack-list promote target not found in suppressed panes");
+ return StackListRemoval::NotRemoved;
+ },
+ };
+ let removed_visible = self.tiled_panes.replace_pane(id, target_box);
+ self.adopt_parked_pair(promote_target);
+ self.stack_list_of_member.remove(&id);
+ self.reserved_top_rows.borrow_mut().remove(&id);
+ if let Some(list) = self.stack_lists.get_mut(&stack_list_id) {
+ list.members.retain(|m| *m != id);
+ list.visible = promote_target;
+ }
+ self.refresh_or_dissolve_stack_list(stack_list_id);
+ removed_visible
+ } else {
+ let removed = self.suppressed_panes.remove(&id).map(|(_, p)| p);
+ let substitute_member = self
+ .stack_list_parked_pairs
+ .remove(&id)
+ .filter(|parked_pid| self.suppressed_panes.contains_key(parked_pid));
+ self.stack_list_of_member.remove(&id);
+ self.reserved_top_rows.borrow_mut().remove(&id);
+ if let Some(substitute_member) = substitute_member {
+ self.stack_list_of_member
+ .insert(substitute_member, stack_list_id);
+ if let Some(list) = self.stack_lists.get_mut(&stack_list_id) {
+ for member in list.members.iter_mut() {
+ if *member == id {
+ *member = substitute_member;
+ }
+ }
+ }
+ } else if let Some(list) = self.stack_lists.get_mut(&stack_list_id) {
+ list.members.retain(|m| *m != id);
+ }
+ self.refresh_or_dissolve_stack_list(stack_list_id);
+ removed
+ };
+ self.set_force_render();
+ self.set_should_clear_display_before_rendering();
+ StackListRemoval::Removed(removed)
+ }
+ fn track_stack_list_member_id_swap(&mut self, old_id: PaneId, new_id: PaneId) {
+ let stack_list_id = match self.stack_list_id_of_member(&old_id) {
+ Some(sid) => sid,
+ None => return,
+ };
+ self.stack_list_of_member.remove(&old_id);
+ self.stack_list_of_member.insert(new_id, stack_list_id);
+ let mut swapped_visible_member = false;
+ if let Some(list) = self.stack_lists.get_mut(&stack_list_id) {
+ for member in list.members.iter_mut() {
+ if *member == old_id {
+ *member = new_id;
+ }
+ }
+ if list.visible == old_id {
+ list.visible = new_id;
+ swapped_visible_member = true;
+ }
+ }
+ {
+ let mut reserved = self.reserved_top_rows.borrow_mut();
+ if let Some(reserved_rows) = reserved.remove(&old_id) {
+ reserved.insert(new_id, reserved_rows);
+ }
+ }
+ if swapped_visible_member {
+ self.refresh_or_dissolve_stack_list(stack_list_id);
+ self.set_force_render();
+ self.set_should_clear_display_before_rendering();
+ }
+ }
+ fn dissolve_stack_list(&mut self, stack_list_id: StackListId) {
+ if let Some(list) = self.stack_lists.get(&stack_list_id) {
+ let members = list.members.clone();
+ self.drop_stack_list_ledger(stack_list_id, &members);
+ self.tiled_panes.reapply_pane_frames();
+ }
+ }
+ fn drop_stack_list_ledger(&mut self, stack_list_id: StackListId, members: &[PaneId]) {
+ self.stack_lists.remove(&stack_list_id);
+ let mut reserved = self.reserved_top_rows.borrow_mut();
+ for member in members {
+ self.stack_list_of_member.remove(member);
+ self.stack_list_parked_pairs.remove(member);
+ reserved.remove(member);
+ }
+ }
+ fn degroupify(&mut self, stack_list_id: StackListId) {
+ let (members, visible) = match self.stack_lists.get(&stack_list_id) {
+ Some(list) => (list.members.clone(), list.visible),
+ None => return,
+ };
+ let n = members.len();
+ if n == 0 || !self.tiled_panes.panes_contain(&visible) {
+ self.drop_stack_list_ledger(stack_list_id, &members);
+ return;
+ }
+ let geoms = self.tiled_panes.stack_panes(visible, n);
+ if geoms.len() != n {
+ log::error!("degroupify: could not compute classic stack geoms");
+ return;
+ }
+ for (i, member) in members.iter().enumerate() {
+ let mut geom = geoms[i];
+ if *member == visible {
+ geom.logical_position = self
+ .tiled_panes
+ .get_pane(*member)
+ .and_then(|p| p.position_and_size().logical_position);
+ self.tiled_panes.set_geom_for_pane_with_id(member, geom);
+ } else if let Some((_is_scrollback_editor, mut pane)) =
+ self.suppressed_panes.remove(member)
+ {
+ geom.logical_position = pane.position_and_size().logical_position;
+ pane.set_geom(geom);
+ self.tiled_panes.add_pane_with_existing_geom(*member, pane);
+ self.adopt_parked_pair(*member);
+ }
+ }
+ self.drop_stack_list_ledger(stack_list_id, &members);
+ self.tiled_panes.expand_pane_in_stack(visible);
+ self.tiled_panes.reapply_pane_frames();
+ self.set_force_render();
+ self.set_should_clear_display_before_rendering();
+ }
+ // the classic layout solver cannot see suppressed stack-list members; dissolve
+ // lists into in-grid stacks first, groupify_all re-forms them on mode entry
+ fn degroupify_all(&mut self) {
+ let ids: Vec<StackListId> = self.stack_lists.keys().copied().collect();
+ for id in ids {
+ self.degroupify(id);
+ }
+ }
+ fn dissolve_stack_lists_for_classic_mutation(&mut self) {
+ if self.stacked_pane_list_is_active() {
+ self.degroupify_all();
+ }
+ }
+ pub fn sync_stacked_pane_list_mode(&mut self) {
+ if self.stacked_pane_list_is_active() {
+ self.groupify_all();
+ } else if self.has_stack_lists() {
+ self.degroupify_all();
+ }
+ }
+ // the inverse of degroupify_all: adopts classic in-grid stacks into stack
+ // lists, suppressing all but the visible member of each
+ fn groupify_all(&mut self) {
+ if self.tiled_panes.fullscreen_is_active() {
+ return;
+ }
+ let mut stacks: HashMap<usize, Vec<(usize, PaneId, bool)>> = HashMap::new();
+ for (id, pane) in self.tiled_panes.get_panes() {
+ let geom = pane.position_and_size();
+ if let Some(stack_id) = geom.stacked {
+ let is_expanded = !geom.rows.is_fixed();
+ stacks
+ .entry(stack_id)
+ .or_default()
+ .push((geom.y, *id, is_expanded));
+ }
+ }
+ let mut converted_any = false;
+ for (_stack_id, mut members) in stacks {
+ if members.len() < 2 {
+ continue;
+ }
+ members.sort_by_key(|(y, _, _)| *y);
+ let representative = members[0].1;
+ let rect = match self.tiled_panes.position_and_size_of_stack(&representative) {
+ Some(r) => r,
+ None => continue,
+ };
+ let visible = members
+ .iter()
+ .find(|(_, _, expanded)| *expanded)
+ .map(|(_, id, _)| *id)
+ .unwrap_or_else(|| members.last().map(|(_, id, _)| *id).unwrap());
+ let ordered_ids: Vec<PaneId> = members.iter().map(|(_, id, _)| *id).collect();
+ let logical_position = self
+ .tiled_panes
+ .get_pane(visible)
+ .and_then(|p| p.position_and_size().logical_position);
+ for (_, id, _) in &members {
+ if *id == visible {
+ continue;
+ }
+ if let Some(pane) = self.tiled_panes.extract_pane(*id) {
+ let pane_parked_by_member = self.pane_parked_by(id);
+ self.insert_suppressed_pane(*id, (false, pane));
+ if let Some(parked_pid) = pane_parked_by_member {
+ self.stack_list_parked_pairs.insert(*id, parked_pid);
+ }
+ }
+ }
+ if let Some(vpane) = self.tiled_panes.get_pane_mut(visible) {
+ let mut g = rect;
+ g.stacked = None;
+ g.logical_position = logical_position;
+ vpane.set_geom(g);
+ }
+ let stack_list_id = self.next_stack_list_id;
+ self.next_stack_list_id += 1;
+ for id in &ordered_ids {
+ self.stack_list_of_member.insert(*id, stack_list_id);
+ }
+ self.stack_lists.insert(
+ stack_list_id,
+ StackList {
+ members: ordered_ids,
+ visible,
+ },
+ );
+ self.refresh_or_dissolve_stack_list(stack_list_id);
+ converted_any = true;
+ }
+ if converted_any {
+ self.set_force_render();
+ self.set_should_clear_display_before_rendering();
+ }
+ }
+ fn resize_all_stack_list_hidden_members(&mut self) {
+ let ids: Vec<StackListId> = self.stack_lists.keys().copied().collect();
+ for id in ids {
+ self.resize_stack_list_hidden_members(id);
+ }
+ }
+ fn resize_stack_list_hidden_members(&mut self, stack_list_id: StackListId) {
+ let (visible_geom, visible_offset, hidden_members) = {
+ let list = match self.stack_lists.get(&stack_list_id) {
+ Some(l) => l,
+ None => return,
+ };
+ let (geom, offset) = match self.tiled_panes.get_pane(list.visible) {
+ Some(p) => (p.position_and_size(), p.get_content_offset()),
+ None => return,
+ };
+ let hidden: Vec<PaneId> = list
+ .members
+ .iter()
+ .copied()
+ .filter(|m| *m != list.visible)
+ .collect();
+ (geom, offset, hidden)
+ };
+ for member in hidden_members {
+ if let Some((_is_scrollback_editor, pane)) = self.suppressed_panes.get_mut(&member) {
+ pane.set_geom(visible_geom);
+ pane.set_content_offset(visible_offset);
+ let _ = resize_pty!(pane, self.os_api, self.senders, self.character_cell_size);
+ }
+ }
+ }
+ fn render_stack_list_headers(
+ &mut self,
+ output: &mut Output,
+ client_id_override: Option<ClientId>,
+ ) -> Result<()> {
+ if self.stack_lists.is_empty() || self.tiled_panes.fullscreen_is_active() {
+ return Ok(());
+ }
+ let mut connected_clients: HashSet<ClientId> =
+ { self.connected_clients.borrow().iter().copied().collect() };
+ if let Some(override_id) = client_id_override {
+ connected_clients.insert(override_id);
+ }
+ if connected_clients.is_empty() {
+ return Ok(());
+ }
+ let multiple_users_exist_in_session = { self.connected_clients_in_app.borrow().len() > 1 };
+ let should_draw_pane_frames = self.pane_frame_style.draws_full_frames();
+ let session_is_mirrored = self.stack_list_session_is_mirrored;
+ let current_pane_group: HashMap<ClientId, Vec<PaneId>> =
+ { self.current_pane_group.borrow().clone_inner() };
+ let active_panes: HashMap<ClientId, PaneId> = connected_clients
+ .iter()
+ .filter_map(|c| self.tiled_panes.get_active_pane_id(*c).map(|p| (*c, p)))
+ .collect();
+ let client_modes: Vec<(ClientId, InputMode)> = {
+ let mode_info = self.mode_info.borrow();
+ connected_clients
+ .iter()
+ .map(|c| {
+ let mode = mode_info.get(c).unwrap_or(&self.default_mode_info).mode;
+ (*c, mode)
+ })
+ .collect()
+ };
+ let lists: Vec<StackList> = self.stack_lists.values().cloned().collect();
+ for list in lists {
+ let rect = match self.tiled_panes.get_pane(list.visible) {
+ Some(p) => p.position_and_size(),
+ None => continue,
+ };
+ let widest_member_title = list
+ .members
+ .iter()
+ .filter_map(|member| {
+ if *member == list.visible {
+ self.tiled_panes.get_pane(*member).map(|p| &**p)
+ } else {
+ self.suppressed_panes.get(member).map(|(_, p)| &**p)
+ }
+ })
+ .map(|pane| pane.stack_list_entry_label().width())
+ .max()
+ .unwrap_or(0);
+ for (rank, member) in list.members.iter().enumerate() {
+ let mut header_geom = rect;
+ header_geom.y = rect.y + rank;
+ header_geom.rows = Dimension::fixed(1);
+ header_geom.stacked = None;
+ let pane_box = if *member == list.visible {
+ self.tiled_panes.get_pane_mut(*member)
+ } else {
+ self.suppressed_panes.get_mut(member).map(|(_, p)| p)
+ };
+ let pane_box = match pane_box {
+ Some(p) => p,
+ None => continue,
+ };
+ let mut pane_contents_and_ui = PaneContentsAndUi::new(
+ pane_box,
+ output,
+ self.style,
+ &active_panes,
+ multiple_users_exist_in_session,
+ None,
+ false,
+ false,
+ should_draw_pane_frames,
+ &self.mouse_hover_pane_id,
+ current_pane_group.clone(),
+ false,
+ false,
+ );
+ pane_contents_and_ui.set_frame_geom_override(Some(header_geom));
+ pane_contents_and_ui
+ .set_stack_list_entry(Some(widest_member_title), *member == list.visible);
+ for (client_id, client_mode) in &client_modes {
+ pane_contents_and_ui.render_pane_frame(
+ *client_id,
+ *client_mode,
+ session_is_mirrored,
+ false,
+ true,
+ )?;
+ }
+ }
+ }
+ Ok(())
+ }
+
pub fn apply_layout(
&mut self,
layout: TiledPaneLayout,
@@ -1078,7 +1763,7 @@ impl Tab {
} else {
let selectable_tiled_panes =
self.tiled_panes.get_panes().filter(|(_, p)| p.selectable());
- if selectable_tiled_panes.count() > 1 {
+ if selectable_tiled_panes.count() + self.suppressed_stack_list_members().count() > 1 {
self.swap_layouts.tiled_layout_info()
} else {
// no layout for single pane
@@ -1127,6 +1812,7 @@ impl Tab {
if self.tiled_panes.fullscreen_is_active() {
self.tiled_panes.unset_fullscreen();
}
+ self.dissolve_stack_lists_for_classic_mutation();
if let Some(layout_candidate) = self
.swap_layouts
.swap_tiled_panes(&self.tiled_panes, search_backwards)
@@ -1522,7 +2208,9 @@ impl Tab {
}
}
} else if let Some(focused_pane_id) = self.tiled_panes.focused_pane_id(client_id) {
- if self.get_selectable_tiled_panes().count() <= 1 {
+ if self.get_selectable_tiled_panes().count() <= 1
+ && !self.pane_is_stack_list_member(&focused_pane_id)
+ {
// don't close the only pane on screen...
return Ok(());
}
@@ -1564,7 +2252,9 @@ impl Tab {
self.add_tiled_pane(floating_pane_to_embed, pane_id, false, client_id)?;
}
} else if self.tiled_panes.panes_contain(&pane_id) {
- if self.get_selectable_tiled_panes().count() <= 1 {
+ if self.get_selectable_tiled_panes().count() <= 1
+ && !self.pane_is_stack_list_member(&pane_id)
+ {
log::error!("Cannot float the last tiled pane...");
// don't close the only pane on screen...
return Ok(());
@@ -1572,6 +2262,10 @@ impl Tab {
if let Some(embedded_pane_to_float) = self.extract_pane(pane_id, true) {
self.add_floating_pane(embedded_pane_to_float, pane_id, None, true, client_id)?;
}
+ } else if self.pane_is_hidden_stack_list_member(&pane_id) {
+ if let Some(hidden_member_to_float) = self.extract_pane(pane_id, true) {
+ self.add_floating_pane(hidden_member_to_float, pane_id, None, true, client_id)?;
+ }
}
Ok(())
}
@@ -2236,6 +2930,10 @@ impl Tab {
};
match replaced_pane {
Some(replaced_pane) => {
+ self.track_stack_list_member_id_swap(
+ replaced_pane.pid(),
+ PaneId::Terminal(pid),
+ );
self.insert_scrollback_editor_replaced_pane(replaced_pane, pid);
self.get_active_pane(client_id)
.with_context(|| format!("no active pane found for client {client_id}"))
@@ -2304,6 +3002,10 @@ impl Tab {
self.character_cell_size
)
.non_fatal();
+ self.track_stack_list_member_id_swap(
+ pane_id_to_replace,
+ PaneId::Terminal(pid),
+ );
self.insert_scrollback_editor_replaced_pane(replaced_pane, pid);
},
None => {
@@ -2367,6 +3069,12 @@ impl Tab {
self.tiled_panes
.replace_pane(old_pane_id, Box::new(new_pane))
};
+ if replaced_pane.is_some() {
+ self.track_stack_list_member_id_swap(
+ old_pane_id,
+ PaneId::Terminal(new_pane_id),
+ );
+ }
if close_replaced_pane {
if let Some(pid) = replaced_pane.as_ref().map(|p| p.pid()) {
self.senders
@@ -2437,6 +3145,9 @@ impl Tab {
self.tiled_panes
.replace_pane(old_pane_id, Box::new(new_pane))
};
+ if replaced_pane.is_some() {
+ self.track_stack_list_member_id_swap(old_pane_id, PaneId::Plugin(plugin_pid));
+ }
if close_replaced_pane {
drop(replaced_pane);
} else {
@@ -2473,6 +3184,7 @@ impl Tab {
pane_to_replace_with: Box<dyn Pane>,
completion_tx: Option<NotificationEnd>,
) {
+ let replacement_pane_id = pane_to_replace_with.pid();
let mut replaced_pane = if self.floating_panes.panes_contain(&pane_id_to_replace) {
self.floating_panes
.replace_pane(pane_id_to_replace, pane_to_replace_with)
@@ -2481,6 +3193,9 @@ impl Tab {
self.tiled_panes
.replace_pane(pane_id_to_replace, pane_to_replace_with)
};
+ if replaced_pane.is_some() {
+ self.track_stack_list_member_id_swap(pane_id_to_replace, replacement_pane_id);
+ }
if let Some(replaced_pane) = replaced_pane.take() {
let pane_id = replaced_pane.pid();
let _ = self
@@ -2537,6 +3252,7 @@ impl Tab {
if self.tiled_panes.fullscreen_is_active() {
self.toggle_active_pane_fullscreen(client_id);
}
+ self.dissolve_stack_lists_for_classic_mutation();
if self.tiled_panes.can_split_pane_horizontally(client_id) {
if let PaneId::Terminal(term_pid) = pid {
let next_terminal_position = self.get_next_terminal_position();
@@ -2604,6 +3320,7 @@ impl Tab {
if self.tiled_panes.fullscreen_is_active() {
self.toggle_active_pane_fullscreen(client_id);
}
+ self.dissolve_stack_lists_for_classic_mutation();
if self.tiled_panes.can_split_pane_vertically(client_id) {
if let PaneId::Terminal(term_pid) = pid {
let next_terminal_position = self.get_next_terminal_position();
@@ -2732,6 +3449,7 @@ impl Tab {
.tiled_panes
.get_panes()
.chain(self.floating_panes.get_panes())
+ .chain(self.suppressed_stack_list_members())
.filter(|(_, pane)| pane.has_bell())
.map(|(pane_id, _)| *pane_id)
.collect();
@@ -2781,6 +3499,7 @@ impl Tab {
.tiled_panes
.get_panes()
.chain(self.floating_panes.get_panes())
+ .chain(self.suppressed_stack_list_members())
.filter(|(_, pane)| pane.has_bell())
.map(|(pane_id, _)| *pane_id)
.collect();
@@ -3073,7 +3792,13 @@ impl Tab {
// returns true if a UI update should be triggered (eg. when closing a command pane with
// ctrl-c)
let mut should_trigger_ui_change = false;
- let pane_ids = self.get_static_and_floating_pane_ids();
+ let mut pane_ids = self.get_static_and_floating_pane_ids();
+ pane_ids.extend(
+ self.stack_list_of_member
+ .keys()
+ .filter(|member| self.pane_is_hidden_stack_list_member(member))
+ .copied(),
+ );
for pane_id in pane_ids {
let ui_change_triggered = self
.write_to_pane_id(
@@ -3393,9 +4118,14 @@ impl Tab {
if self.floating_panes.panes_are_visible() {
return;
}
+ self.dissolve_stack_lists_for_classic_mutation();
self.tiled_panes.toggle_active_pane_fullscreen(client_id);
}
pub fn toggle_pane_fullscreen(&mut self, pane_id: PaneId) {
+ if self.pane_is_hidden_stack_list_member(&pane_id) {
+ self.make_hidden_stack_list_member_visible(pane_id);
+ }
+ self.dissolve_stack_lists_for_classic_mutation();
if self.tiled_panes.panes_contain(&pane_id) {
self.tiled_panes.toggle_pane_fullscreen(pane_id);
} else {
@@ -3460,6 +4190,12 @@ impl Tab {
pub fn set_force_render(&mut self) {
self.tiled_panes.set_force_render();
self.floating_panes.set_force_render();
+ for member in self.stack_list_of_member.keys() {
+ if let Some((_is_scrollback_editor, pane)) = self.suppressed_panes.get_mut(member) {
+ pane.set_should_render(true);
+ pane.render_full_viewport();
+ }
+ }
}
pub fn set_should_clear_display_before_rendering(&mut self) {
self.should_clear_display_before_rendering = true;
@@ -3515,6 +4251,9 @@ impl Tab {
if connected_clients.is_empty() || !self.tiled_panes.has_active_panes() {
return Ok(());
}
+ if self.stacked_pane_list_is_active() {
+ self.groupify_all();
+ }
self.update_active_panes_in_pty_thread()
.with_context(err_context)?;
@@ -3537,6 +4276,8 @@ impl Tab {
&self.mouse_help_text_visible,
)
.with_context(err_context)?;
+ self.render_stack_list_headers(output, client_id_override)
+ .with_context(err_context)?;
if (self.floating_panes.panes_are_visible() && self.floating_panes.has_active_panes())
|| self.floating_panes.has_pinned_panes()
{
@@ -3572,7 +4313,7 @@ impl Tab {
hide_cursor,
);
if self.should_clear_display_before_rendering {
- let clear_display = "\u{1b}[2J";
+ let clear_display = "\u{1b}[m\u{1b}[2J";
output.add_pre_vte_instruction_to_multiple_clients(
connected_clients.iter().copied(),
clear_display,
@@ -3707,7 +4448,11 @@ impl Tab {
PaneId::Terminal(_) => true,
})
.count();
- tiled_panes_count + floating_panes_count + 1
+ let hidden_stack_list_members_count = self
+ .suppressed_stack_list_members()
+ .filter(|(pane_id, _)| matches!(pane_id, PaneId::Terminal(_)))
+ .count();
+ tiled_panes_count + floating_panes_count + hidden_stack_list_members_count + 1
}
pub fn has_selectable_panes(&self) -> bool {
let selectable_tiled_panes = self.tiled_panes.get_panes().filter(|(_, p)| p.selectable());
@@ -3768,6 +4513,7 @@ impl Tab {
self.tiled_panes.toggle_pane_fullscreen(pane_id);
}
}
+ self.resize_all_stack_list_hidden_members();
Ok(())
}
pub fn resize(&mut self, client_id: ClientId, strategy: ResizeStrategy) -> Result<()> {
@@ -3782,6 +4528,7 @@ impl Tab {
self.set_force_render(); // we force render here to make sure the panes under the floating pane render and don't leave "garbage" in case of a decrease
}
} else {
+ self.dissolve_stack_lists_for_classic_mutation();
match self.tiled_panes.resize_active_pane(client_id, &strategy) {
Ok(_) => {
self.swap_layouts.set_is_tiled_damaged();
@@ -3896,6 +4643,11 @@ impl Tab {
self.focus_pane_down_fullscreen(client_id);
return Ok(true);
}
+ if self.stacked_pane_list_is_active()
+ && self.move_focus_within_stack_list(client_id, true)
+ {
+ return Ok(true);
+ }
Ok(self.tiled_panes.move_focus_down(client_id))
}
}
@@ -3918,6 +4670,11 @@ impl Tab {
self.focus_pane_up_fullscreen(client_id);
return Ok(true);
}
+ if self.stacked_pane_list_is_active()
+ && self.move_focus_within_stack_list(client_id, false)
+ {
+ return Ok(true);
+ }
Ok(self.tiled_panes.move_focus_up(client_id))
}
}
@@ -3955,6 +4712,7 @@ impl Tab {
self.floating_panes
.move_active_pane(search_backwards, &mut self.os_api, client_id);
} else {
+ self.dissolve_stack_lists_for_classic_mutation();
self.tiled_panes
.move_active_pane(search_backwards, client_id);
}
@@ -3970,6 +4728,7 @@ impl Tab {
if self.floating_panes.panes_are_visible() {
self.floating_panes.move_pane(search_backwards, pane_id);
} else {
+ self.dissolve_stack_lists_for_classic_mutation();
self.tiled_panes.move_pane(search_backwards, pane_id);
}
}
@@ -3985,6 +4744,7 @@ impl Tab {
self.floating_panes
.move_active_pane(search_backwards, &mut self.os_api, client_id);
} else {
+ self.dissolve_stack_lists_for_classic_mutation();
self.tiled_panes
.move_active_pane(search_backwards, client_id);
}
@@ -4001,6 +4761,7 @@ impl Tab {
if self.tiled_panes.fullscreen_is_active() {
return;
}
+ self.dissolve_stack_lists_for_classic_mutation();
self.tiled_panes.move_active_pane_down(client_id);
}
}
@@ -4016,6 +4777,7 @@ impl Tab {
if self.tiled_panes.fullscreen_is_active() {
return;
}
+ self.dissolve_stack_lists_for_classic_mutation();
self.tiled_panes.move_pane_down(pane_id);
}
}
@@ -4031,6 +4793,7 @@ impl Tab {
if self.tiled_panes.fullscreen_is_active() {
return;
}
+ self.dissolve_stack_lists_for_classic_mutation();
self.tiled_panes.move_active_pane_up(client_id);
}
}
@@ -4046,6 +4809,7 @@ impl Tab {
if self.tiled_panes.fullscreen_is_active() {
return;
}
+ self.dissolve_stack_lists_for_classic_mutation();
self.tiled_panes.move_pane_up(pane_id);
}
}
@@ -4061,6 +4825,7 @@ impl Tab {
if self.tiled_panes.fullscreen_is_active() {
return;
}
+ self.dissolve_stack_lists_for_classic_mutation();
self.tiled_panes.move_active_pane_right(client_id);
}
}
@@ -4076,6 +4841,7 @@ impl Tab {
if self.tiled_panes.fullscreen_is_active() {
return;
}
+ self.dissolve_stack_lists_for_classic_mutation();
self.tiled_panes.move_pane_right(pane_id);
}
}
@@ -4091,6 +4857,7 @@ impl Tab {
if self.tiled_panes.fullscreen_is_active() {
return;
}
+ self.dissolve_stack_lists_for_classic_mutation();
self.tiled_panes.move_active_pane_left(client_id);
}
}
@@ -4106,11 +4873,13 @@ impl Tab {
if self.tiled_panes.fullscreen_is_active() {
return;
}
+ self.dissolve_stack_lists_for_classic_mutation();
self.tiled_panes.move_pane_left(pane_id);
}
}
fn close_down_to_max_terminals(&mut self) -> Result<()> {
if let Some(max_panes) = self.max_panes {
+ self.dissolve_stack_lists_for_classic_mutation();
let terminals = self.get_tiled_pane_ids();
for &pid in terminals.iter().skip(max_panes - 1) {
self.senders
@@ -4226,6 +4995,25 @@ impl Tab {
ignore_suppressed_panes: bool,
exit_status: Option<i32>,
) {
+ let id_parks_a_different_pane = self.pane_parked_by(&id).is_some();
+ if !ignore_suppressed_panes
+ && !id_parks_a_different_pane
+ && self.pane_is_stack_list_member(&id)
+ {
+ if self.close_stack_list_member(id, exit_status) {
+ let _ = self.senders.send_to_plugin(PluginInstruction::Update(vec![(
+ None,
+ None,
+ Event::PaneClosed(id.into()),
+ )]));
+ let _ =
+ self.senders
+ .send_to_screen(ScreenInstruction::NotifyPaneClosedToSubscribers {
+ pane_id: id.into(),
+ });
+ return;
+ }
+ }
// we need to ignore suppressed panes when we toggle a pane to be floating/embedded(tiled)
// this is because in that case, while we do use this logic, we're not actually closing the
// pane, we're moving it
@@ -4271,6 +5059,7 @@ impl Tab {
// confusing
let _ = self.relayout_tiled_panes(false);
}
+ self.resize_all_stack_list_hidden_members();
closed_pane
};
if let Some(exit_status) = exit_status {
@@ -4295,6 +5084,17 @@ impl Tab {
id: PaneId,
dont_swap_if_suppressed: bool,
) -> Option<Box<dyn Pane>> {
+ let id_parks_a_different_pane = self.pane_parked_by(&id).is_some();
+ if self.pane_is_stack_list_member(&id)
+ && (dont_swap_if_suppressed || !id_parks_a_different_pane)
+ {
+ if let StackListRemoval::Removed(removed) = self.remove_stack_list_member(id) {
+ return removed.map(|mut pane| {
+ pane.reset_logical_position();
+ pane
+ });
+ }
+ }
if !dont_swap_if_suppressed && self.suppressed_panes.contains_key(&id) {
// this is done for the scrollback editor
return match self.replace_pane_with_suppressed_pane(id) {
@@ -4349,6 +5149,7 @@ impl Tab {
// confusing
let _ = self.relayout_tiled_panes(false);
}
+ self.resize_all_stack_list_hidden_members();
// we do this so that the logical index will not affect ordering in the target tab
if let Some(closed_pane) = closed_pane.as_mut() {
closed_pane.reset_logical_position();
@@ -4413,6 +5214,9 @@ impl Tab {
} else {
self.tiled_panes.replace_pane(pane_id, suppressed_pane)
};
+ if replaced_pane.is_some() {
+ self.track_stack_list_member_id_swap(pane_id, suppressed_pane_id);
+ }
if let Some(suppressed_pane) = self
.floating_panes
.get_pane(suppressed_pane_id)
@@ -4636,6 +5440,7 @@ impl Tab {
completion_tx: Option<NotificationEnd>,
) -> Result<()> {
if let PaneId::Terminal(_terminal_pane_id) = pane_id {
+ self.make_hidden_stack_list_member_visible(pane_id);
let mut file = temp_dir();
file.push(format!("{}.dump", Uuid::new_v4()));
self.dump_terminal_screen(Some(String::from(file.to_string_lossy())), pane_id, true)
@@ -4660,6 +5465,7 @@ impl Tab {
completion_tx: Option<NotificationEnd>,
) -> Result<()> {
if let PaneId::Terminal(_terminal_pane_id) = pane_id {
+ self.make_hidden_stack_list_member_visible(pane_id);
let mut file = temp_dir();
file.push(format!("{}.dump", Uuid::new_v4()));
self.dump_with_ansi_terminal_screen(
@@ -4978,16 +5784,24 @@ impl Tab {
geom_to_compare_against.contains(point)
};
- if search_selectable {
- Ok(self
- .get_selectable_tiled_panes()
+ let found_pane_id = if search_selectable {
+ self.get_selectable_tiled_panes()
.find(|(_, p)| pane_contains_point(p, point, &stacked_pane_ids_under_flexible_pane))
- .map(|(&id, _)| id))
+ .map(|(&id, _)| id)
} else {
- Ok(self
- .get_tiled_panes()
+ self.get_tiled_panes()
.find(|(_, p)| pane_contains_point(p, point, &stacked_pane_ids_under_flexible_pane))
- .map(|(&id, _)| id))
+ .map(|(&id, _)| id)
+ };
+ let resolved_pane_id = found_pane_id.map(|id| self.stack_list_member_at_point(id, point));
+ if search_selectable {
+ Ok(resolved_pane_id.filter(|id| {
+ self.get_pane_with_id(*id)
+ .map(|p| p.selectable())
+ .unwrap_or(false)
+ }))
+ } else {
+ Ok(resolved_pane_id)
}
}
@@ -5521,6 +6335,9 @@ impl Tab {
client_id: ClientId,
) -> Result<()> {
// TODO: should error if pane is not selectable
+ if self.focus_hidden_stack_list_member(pane_id, client_id) {
+ return Ok(());
+ }
self.tiled_panes
.focus_pane_if_exists(pane_id, client_id)
.map(|_| self.hide_floating_panes())
@@ -5556,6 +6373,7 @@ impl Tab {
};
if let Some(replaced_pane) = replaced_pane {
let is_scrollback_editor = false;
+ self.track_stack_list_member_id_swap(replaced_pane.pid(), pane_id);
self.insert_suppressed_pane(
pane_id,
(is_scrollback_editor, replaced_pane),
@@ -5574,6 +6392,10 @@ impl Tab {
})
}
pub fn focus_suppressed_pane_for_all_clients(&mut self, pane_id: PaneId) {
+ if self.make_hidden_stack_list_member_visible(pane_id) {
+ self.tiled_panes.focus_pane_for_all_clients(pane_id);
+ return;
+ }
match self.suppressed_panes.remove(&pane_id) {
Some(pane) => {
self.show_floating_panes();
@@ -5595,8 +6417,14 @@ impl Tab {
self.insert_suppressed_pane(pane_id, (false, pane));
}
}
+ fn make_hidden_stack_list_member_visible(&mut self, pane_id: PaneId) -> bool {
+ self.swap_in_hidden_stack_list_member(pane_id, None)
+ }
pub fn unsuppress_pane(&mut self, pane_id: PaneId, should_float_if_hidden: bool) {
// removes a pane from being suppressed (hidden) but does not focus it
+ if self.make_hidden_stack_list_member_visible(pane_id) {
+ return;
+ }
match self
.suppressed_panes
.extract_if(|_key, (_, pane)| pane.pid() == pane_id)
@@ -5618,6 +6446,9 @@ impl Tab {
}
pub fn unsuppress_or_expand_pane(&mut self, pane_id: PaneId, should_float_if_hidden: bool) {
// removes a pane from being suppressed (hidden) but does not focus it
+ if self.make_hidden_stack_list_member_visible(pane_id) {
+ return;
+ }
match self
.suppressed_panes
.extract_if(|_key, (_, pane)| pane.pid() == pane_id)
@@ -5767,6 +6598,7 @@ impl Tab {
if self.tiled_panes.fullscreen_is_active() {
self.tiled_panes.unset_fullscreen();
}
+ self.dissolve_stack_lists_for_classic_mutation();
let should_auto_layout =
self.auto_layout && !self.swap_layouts.is_tiled_damaged() && !without_relayout;
if self.tiled_panes.has_room_for_new_pane() {
@@ -5779,6 +6611,7 @@ impl Tab {
} else {
self.tiled_panes.insert_pane(pane_id, pane, client_id);
}
+ self.tiled_panes.reapply_pane_frames();
self.set_should_clear_display_before_rendering();
if let Some(client_id) = client_id {
self.tiled_panes.focus_pane(pane_id, client_id);
@@ -5802,6 +6635,7 @@ impl Tab {
if self.tiled_panes.fullscreen_is_active() {
self.tiled_panes.unset_fullscreen();
}
+ self.dissolve_stack_lists_for_classic_mutation();
self.tiled_panes
.add_pane_to_stack_of_pane_id(pane_id, pane, root_pane_id);
self.set_should_clear_display_before_rendering();
@@ -5819,6 +6653,7 @@ impl Tab {
if self.tiled_panes.fullscreen_is_active() {
self.tiled_panes.unset_fullscreen();
}
+ self.dissolve_stack_lists_for_classic_mutation();
self.tiled_panes
.add_pane_to_stack_of_active_pane(pane_id, pane, client_id);
self.tiled_panes.focus_pane(pane_id, client_id);
@@ -5891,6 +6726,9 @@ impl Tab {
}
pub fn resize_pane_with_id(&mut self, strategy: ResizeStrategy, pane_id: PaneId) -> Result<()> {
let err_context = || format!("unable to resize pane");
+ if self.pane_is_stack_list_member(&pane_id) {
+ self.dissolve_stack_lists_for_classic_mutation();
+ }
if self.floating_panes.panes_contain(&pane_id) {
let successfully_resized = self
.floating_panes
@@ -6017,7 +6855,10 @@ impl Tab {
}
}
pub fn extract_suppressed_panes(&mut self) -> SuppressedPanes {
- self.suppressed_panes.drain().collect()
+ let stack_list_members = &self.stack_list_of_member;
+ self.suppressed_panes
+ .extract_if(|key, _| !stack_list_members.contains_key(key))
+ .collect()
}
pub fn add_suppressed_panes(&mut self, mut suppressed_panes: SuppressedPanes) {
for (pane_id, suppressed_pane_entry) in suppressed_panes.drain() {
@@ -6037,6 +6878,7 @@ impl Tab {
}
}
pub fn has_room_for_stack(&mut self, root_pane_id: PaneId, stack_size: usize) -> bool {
+ self.dissolve_stack_lists_for_classic_mutation();
if self.floating_panes.panes_contain(&root_pane_id)
|| self.suppressed_panes.contains_key(&root_pane_id)
{
@@ -6063,6 +6905,7 @@ impl Tab {
// nothing to do
return;
}
+ self.dissolve_stack_lists_for_classic_mutation();
self.swap_layouts.set_is_tiled_damaged(); // TODO: verify we can do all the below first
if self.pane_is_stacked(root_pane_id) {
for pane in panes_to_stack.drain(..) {
@@ -6345,6 +7188,9 @@ impl Tab {
if self.tiled_panes.fullscreen_is_active() {
return;
}
+ if !self.floating_panes.panes_contain(&pane_id) {
+ self.dissolve_stack_lists_for_classic_mutation();
+ }
match direction {
Some(Direction::Left) => {
if self.floating_panes.panes_contain(&pane_id) {
@@ -6403,6 +7249,7 @@ impl Tab {
if self.floating_panes.panes_contain(&pane_id) {
self.floating_panes.move_pane(search_backwards, pane_id);
} else {
+ self.dissolve_stack_lists_for_classic_mutation();
self.tiled_panes.move_pane(search_backwards, pane_id);
}
}
diff --git a/zellij-server/src/tab/mouse_handler.rs b/zellij-server/src/tab/mouse_handler.rs
index 61718864..d04b511c 100644
--- a/zellij-server/src/tab/mouse_handler.rs
+++ b/zellij-server/src/tab/mouse_handler.rs
@@ -417,9 +417,10 @@ impl MouseHandler {
client_id: ClientId,
) -> Option<ClickedPaneDetails> {
let is_floating = tab.floating_panes.panes_contain(&pane_id);
+ let is_hidden_stack_list_member = tab.pane_is_hidden_stack_list_member(&pane_id);
let pane = Self::get_pane_at(tab, position, false).ok()??;
- let on_frame = pane.position_is_on_frame(position);
+ let on_frame = !is_hidden_stack_list_member && pane.position_is_on_frame(position);
let frame_intercepted = on_frame && pane.intercept_mouse_event_on_frame(event, client_id);
let edge = if on_frame {
pane.get_edge_at_position(position)
@@ -1053,6 +1054,10 @@ impl MouseHandler {
return Self::execute_update_hover(tab, Some(pane_id), Some(position), client_id);
}
+ if tab.pane_is_hidden_stack_list_member(&pane_id) {
+ return Self::execute_update_hover(tab, Some(pane_id), Some(position), client_id);
+ }
+
let active_pane_id = tab.get_active_pane_id(client_id);
if active_pane_id == Some(pane_id) {
return Self::execute_update_hover(tab, Some(pane_id), Some(position), client_id);
@@ -1543,7 +1548,9 @@ impl MouseHandler {
}
}
if let Some(clicked_pane) = tab.get_pane_id_at(point, true).with_context(err_context)? {
- tab.tiled_panes.focus_pane(clicked_pane, client_id);
+ if !tab.focus_hidden_stack_list_member(clicked_pane, client_id) {
+ tab.tiled_panes.focus_pane(clicked_pane, client_id);
+ }
tab.set_pane_active_at(clicked_pane);
if tab.floating_panes.panes_are_visible() {
tab.hide_floating_panes();
@@ -1737,7 +1744,7 @@ impl MouseHandler {
.get_pane_id_at(point, search_selectable)
.with_context(err_context)?
{
- Ok(tab.tiled_panes.get_pane_mut(pane_id))
+ Ok(tab.get_pane_with_id_mut(pane_id))
} else {
Ok(None)
}
diff --git a/zellij-server/src/tab/unit/layout_applier_tests.rs b/zellij-server/src/tab/unit/layout_applier_tests.rs
index 33ad8ee4..f67a9a17 100644
--- a/zellij-server/src/tab/unit/layout_applier_tests.rs
+++ b/zellij-server/src/tab/unit/layout_applier_tests.rs
@@ -187,6 +187,8 @@ fn create_layout_applier_fixtures(
let connected_clients_set = Rc::new(RefCell::new(HashSet::from([client_id])));
let mode_info = Rc::new(RefCell::new(HashMap::new()));
let stacked_resize = Rc::new(RefCell::new(false));
+ let stacked_pane_list = Rc::new(RefCell::new(false));
+ let reserved_top_rows = Rc::new(RefCell::new(HashMap::new()));
let session_is_mirrored = true;
let draw_pane_frames = PaneFrameStyle::Full;
let default_mode_info = ModeInfo::default();
@@ -199,6 +201,8 @@ fn create_layout_applier_fixtures(
mode_info.clone(),
character_cell_size.clone(),
stacked_resize,
+ stacked_pane_list,
+ reserved_top_rows,
session_is_mirrored,
draw_pane_frames,
default_mode_info.clone(),
@@ -317,6 +321,8 @@ fn create_layout_applier_fixtures_with_receivers(
let connected_clients_set = Rc::new(RefCell::new(HashSet::from([client_id])));
let mode_info = Rc::new(RefCell::new(HashMap::new()));
let stacked_resize = Rc::new(RefCell::new(false));
+ let stacked_pane_list = Rc::new(RefCell::new(false));
+ let reserved_top_rows = Rc::new(RefCell::new(HashMap::new()));
let session_is_mirrored = true;
let draw_pane_frames = PaneFrameStyle::Full;
let default_mode_info = ModeInfo::default();
@@ -329,6 +335,8 @@ fn create_layout_applier_fixtures_with_receivers(
mode_info.clone(),
character_cell_size.clone(),
stacked_resize,
+ stacked_pane_list,
+ reserved_top_rows,
session_is_mirrored,
draw_pane_frames,
default_mode_info.clone(),
diff --git a/zellij-server/src/tab/unit/tab_integration_tests.rs b/zellij-server/src/tab/unit/tab_integration_tests.rs
index 5b623b00..35a2e9bd 100644
--- a/zellij-server/src/tab/unit/tab_integration_tests.rs
+++ b/zellij-server/src/tab/unit/tab_integration_tests.rs
@@ -246,6 +246,7 @@ fn create_new_tab(size: Size, default_mode: ModeInfo) -> Tab {
size,
character_cell_info,
stacked_resize,
+ Rc::new(RefCell::new(false)),
sixel_image_store,
os_api,
senders,
@@ -334,6 +335,7 @@ fn create_new_tab_without_pane_frames(size: Size, default_mode: ModeInfo) -> Tab
size,
character_cell_info,
stacked_resize,
+ Rc::new(RefCell::new(false)),
sixel_image_store,
os_api,
senders,
@@ -437,6 +439,7 @@ fn create_new_tab_with_swap_layouts(
size,
character_cell_info,
stacked_resize,
+ Rc::new(RefCell::new(false)),
sixel_image_store,
os_api,
senders,
@@ -545,6 +548,7 @@ fn create_new_tab_with_os_api(
size,
character_cell_info,
stacked_resize,
+ Rc::new(RefCell::new(false)),
sixel_image_store,
os_api,
senders,
@@ -635,6 +639,7 @@ fn create_new_tab_with_layout(size: Size, default_mode: ModeInfo, layout: &str)
size,
character_cell_info,
stacked_resize,
+ Rc::new(RefCell::new(false)),
sixel_image_store,
os_api,
senders,
@@ -739,6 +744,7 @@ fn create_new_tab_with_mock_pty_writer(
size,
character_cell_info,
stacked_resize,
+ Rc::new(RefCell::new(false)),
sixel_image_store,
os_api,
senders,
@@ -834,6 +840,7 @@ fn create_new_tab_with_sixel_support(
size,
character_cell_size,
stacked_resize,
+ Rc::new(RefCell::new(false)),
sixel_image_store,
os_api,
senders,
@@ -12727,6 +12734,7 @@ fn create_new_tab_with_plugin_receiver(
size,
character_cell_info,
stacked_resize,
+ Rc::new(RefCell::new(false)),
sixel_image_store,
os_api,
senders,
@@ -14512,6 +14520,7 @@ fn create_new_tab_with_server_receiver(
size,
character_cell_info,
stacked_resize,
+ Rc::new(RefCell::new(false)),
sixel_image_store,
os_api,
senders,
diff --git a/zellij-server/src/tab/unit/tab_tests.rs b/zellij-server/src/tab/unit/tab_tests.rs
index 0d18897e..75a6695b 100644
--- a/zellij-server/src/tab/unit/tab_tests.rs
+++ b/zellij-server/src/tab/unit/tab_tests.rs
@@ -188,6 +188,7 @@ fn create_new_tab(size: Size, stacked_resize: bool) -> Tab {
size,
character_cell_info,
stacked_resize,
+ Rc::new(RefCell::new(false)),
sixel_image_store,
os_api,
senders,
@@ -275,6 +276,7 @@ fn create_new_tab_with_layout(size: Size, layout: TiledPaneLayout) -> Tab {
size,
character_cell_info,
stacked_resize,
+ Rc::new(RefCell::new(false)),
sixel_image_store,
os_api,
senders,
@@ -368,6 +370,7 @@ fn create_new_tab_with_cell_size(
size,
character_cell_size,
stacked_resize,
+ Rc::new(RefCell::new(false)),
sixel_image_store,
os_api,
senders,
diff --git a/zellij-server/src/ui/pane_boundaries_frame.rs b/zellij-server/src/ui/pane_boundaries_frame.rs
index d7abdfd1..e4e182dc 100644
--- a/zellij-server/src/ui/pane_boundaries_frame.rs
+++ b/zellij-server/src/ui/pane_boundaries_frame.rs
@@ -1,5 +1,7 @@
use crate::output::CharacterChunk;
-use crate::panes::{AnsiCode, RcCharacterStyles, TerminalCharacter, EMPTY_TERMINAL_CHARACTER};
+use crate::panes::{
+ AnsiCode, CharacterStyles, RcCharacterStyles, TerminalCharacter, EMPTY_TERMINAL_CHARACTER,
+};
use crate::ui::boundaries::boundary_type;
use crate::ui::hint_text::{
exit_code_segments, hover_segments, rerun_segments, HintExitStatus, HintLevel, HintSegment,
@@ -8,45 +10,53 @@ use crate::ui::hint_text::{
use crate::ClientId;
use zellij_utils::data::{client_id_to_colors, PaletteColor, Style};
use zellij_utils::errors::prelude::*;
-use zellij_utils::pane_size::{Offset, Viewport};
+use zellij_utils::pane_size::{Offset, PaneGeom, Viewport};
use zellij_utils::position::Position;
use unicode_width::{UnicodeWidthChar, UnicodeWidthStr};
+fn styled_characters(
+ characters: &str,
+ style_character: impl Fn(&mut CharacterStyles),
+) -> Vec<TerminalCharacter> {
+ characters
+ .chars()
+ .map(|character| {
+ let mut styles = RcCharacterStyles::reset();
+ styles.update(|styles| style_character(styles));
+ TerminalCharacter::new_styled(character, styles)
+ })
+ .collect()
+}
+
fn foreground_color(characters: &str, color: Option<PaletteColor>) -> Vec<TerminalCharacter> {
- let mut colored_string = Vec::new();
- for character in characters.chars() {
- let mut styles = RcCharacterStyles::reset();
- styles.update(|styles| {
- styles.bold = Some(AnsiCode::On);
- match color {
- Some(palette_color) => {
- styles.foreground = Some(AnsiCode::from(palette_color));
- },
- None => {},
- }
- });
- let terminal_character = TerminalCharacter::new_styled(character, styles);
- colored_string.push(terminal_character);
- }
- colored_string
+ styled_characters(characters, |styles| {
+ styles.bold = Some(AnsiCode::On);
+ if let Some(palette_color) = color {
+ styles.foreground = Some(AnsiCode::from(palette_color));
+ }
+ })
+}
+
+fn dimmed_foreground_color(
+ characters: &str,
+ color: Option<PaletteColor>,
+) -> Vec<TerminalCharacter> {
+ styled_characters(characters, |styles| {
+ styles.dim = Some(AnsiCode::On);
+ if let Some(palette_color) = color {
+ styles.foreground = Some(AnsiCode::from(palette_color));
+ }
+ })
}
fn background_color(characters: &str, color: Option<PaletteColor>) -> Vec<TerminalCharacter> {
- let mut colored_string = Vec::new();
- for character in characters.chars() {
- let mut styles = RcCharacterStyles::reset();
- styles.update(|styles| match color {
- Some(palette_color) => {
- styles.background = Some(AnsiCode::from(palette_color));
- styles.bold(Some(AnsiCode::On));
- },
- None => {},
- });
- let terminal_character = TerminalCharacter::new_styled(character, styles);
- colored_string.push(terminal_character);
- }
- colored_string
+ styled_characters(characters, |styles| {
+ if let Some(palette_color) = color {
+ styles.background = Some(AnsiCode::from(palette_color));
+ styles.bold(Some(AnsiCode::On));
+ }
+ })
}
#[derive(Debug, Clone, Copy, PartialEq)]
@@ -55,6 +65,14 @@ enum ExitStatus {
Exited,
}
+#[derive(Clone, PartialEq)]
+pub struct StackListEntry {
+ pub width: usize,
+ pub label: String,
+ pub is_selected: bool,
+ pub is_emphasized: bool,
+}
+
pub struct FrameParams {
pub focused_client: Option<ClientId>,
pub is_main_client: bool, // more accurately: is_focused_for_main_client
@@ -73,6 +91,9 @@ pub struct FrameParams {
pub show_help_text: bool,
pub highlight_tooltip: Option<String>,
pub omit_title: bool,
+ pub frame_geom_override: Option<PaneGeom>,
+ pub stack_list_entry: Option<StackListEntry>,
+ pub blank_title: bool,
}
#[derive(Default, PartialEq)]
@@ -100,6 +121,8 @@ pub struct PaneFrame {
show_help_text: bool,
highlight_tooltip: Option<String>,
omit_title: bool,
+ stack_list_entry: Option<StackListEntry>,
+ color_override: Option<PaletteColor>,
}
impl PaneFrame {
@@ -133,6 +156,8 @@ impl PaneFrame {
show_help_text: frame_params.show_help_text,
highlight_tooltip: frame_params.highlight_tooltip,
omit_title: frame_params.omit_title,
+ stack_list_entry: frame_params.stack_list_entry,
+ color_override: None,
}
}
pub fn is_pinned(mut self, is_pinned: bool) -> Self {
@@ -150,6 +175,7 @@ impl PaneFrame {
}
pub fn override_color(&mut self, color: PaletteColor) {
self.color = Some(color);
+ self.color_override = Some(color);
}
fn client_cursor(&self, client_id: ClientId) -> Vec<TerminalCharacter> {
let color = client_id_to_colors(client_id, self.style.colors.multiplayer_user_colors);
@@ -715,6 +741,86 @@ impl PaneFrame {
.or_else(|| Some(self.title_line_without_middle()))
.with_context(|| format!("failed to render title '{}'", self.title))
}
+ fn render_stack_list_entry(&self, entry: &StackListEntry) -> Vec<TerminalCharacter> {
+ let usable_cols = self.geom.cols.saturating_sub(2);
+ let bracket_overhead = "[ ]".width();
+ let inner_width = entry
+ .width
+ .min(usable_cols.saturating_sub(bracket_overhead));
+ let content = if entry.label.width() <= inner_width {
+ entry.label.clone()
+ } else {
+ let truncation_budget = inner_width.saturating_sub(1);
+ let mut truncated = String::new();
+ for character in entry.label.chars() {
+ if truncated.width() + character.width().unwrap_or(0) > truncation_budget {
+ break;
+ }
+ truncated.push(character);
+ }
+ truncated.push('…');
+ truncated
+ };
+ let padding = " ".repeat(inner_width.saturating_sub(content.width()));
+ let padded_content = format!("{}{}", content, padding);
+ let left_bracket = "[ ";
+ let right_bracket = " ]";
+ let entry_length = left_bracket.width() + padded_content.width() + right_bracket.width();
+ let entry_start = usable_cols.saturating_sub(entry_length) / 2;
+ let selection_sign = "<↓↑> ";
+ let selection_sign_width = if entry.is_selected && entry_start >= selection_sign.width() {
+ selection_sign.width()
+ } else {
+ 0
+ };
+ let left_budget = entry_start.saturating_sub(selection_sign_width);
+ let (mut focus_part, focus_length) = self
+ .bracketed_focus_indicator(left_budget)
+ .unwrap_or((vec![], 0));
+ let mut line = Vec::with_capacity(usable_cols);
+ line.append(&mut focus_part);
+ for _ in focus_length..left_budget {
+ line.push(EMPTY_TERMINAL_CHARACTER);
+ }
+ if selection_sign_width > 0 {
+ line.append(&mut foreground_color(selection_sign, self.color));
+ }
+ let content_color = if entry.is_emphasized {
+ self.color
+ } else {
+ self.color_override
+ };
+ if entry.is_selected {
+ line.append(&mut foreground_color(left_bracket, self.color_override));
+ line.append(&mut foreground_color(&padded_content, self.color));
+ line.append(&mut foreground_color(right_bracket, self.color_override));
+ } else {
+ let unbracketed = format!(" {} ", padded_content);
+ if entry.is_emphasized {
+ line.append(&mut foreground_color(&unbracketed, content_color));
+ } else {
+ line.append(&mut dimmed_foreground_color(&unbracketed, content_color));
+ }
+ }
+ let mut occupied_columns = entry_start + entry_length;
+ let (mut scroll_part, scroll_length) = self
+ .bracketed_scroll_indicator(usable_cols.saturating_sub(occupied_columns))
+ .unwrap_or((vec![], 0));
+ if self.exit_status.is_some() {
+ let (mut exit_part, exit_length) = self.first_exited_held_title_part_full();
+ if occupied_columns + 1 + exit_length + scroll_length <= usable_cols {
+ line.push(EMPTY_TERMINAL_CHARACTER);
+ line.append(&mut exit_part);
+ occupied_columns += 1 + exit_length;
+ }
+ }
+ while occupied_columns < usable_cols.saturating_sub(scroll_length) {
+ line.push(EMPTY_TERMINAL_CHARACTER);
+ occupied_columns += 1;
+ }
+ line.append(&mut scroll_part);
+ line
+ }
fn render_one_line_title(&self) -> Result<Vec<TerminalCharacter>> {
if self.should_draw_pane_frames {
let total_title_length = self.geom.cols.saturating_sub(2);
@@ -1168,6 +1274,14 @@ impl PaneFrame {
if self.omit_title {
return Ok((character_chunks, None));
}
+ if let Some(entry) = &self.stack_list_entry {
+ character_chunks.push(CharacterChunk::new(
+ self.render_stack_list_entry(entry),
+ self.geom.x + 1,
+ self.geom.y,
+ ));
+ return Ok((character_chunks, None));
+ }
if self.geom.rows == 1 || !self.should_draw_pane_frames {
// we do this explicitly when not drawing pane frames because this should only happen
// if this is a stacked pane with pane frames off (and it doesn't necessarily have only
diff --git a/zellij-server/src/ui/pane_contents_and_ui.rs b/zellij-server/src/ui/pane_contents_and_ui.rs
index f2763a9b..fc72bf0f 100644
--- a/zellij-server/src/ui/pane_contents_and_ui.rs
+++ b/zellij-server/src/ui/pane_contents_and_ui.rs
@@ -2,11 +2,12 @@ use crate::output::Output;
use crate::panes::PaneId;
use crate::tab::Pane;
use crate::ui::boundaries::Boundaries;
-use crate::ui::pane_boundaries_frame::FrameParams;
+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::errors::prelude::*;
+use zellij_utils::pane_size::PaneGeom;
pub struct PaneContentsAndUi<'a> {
pane: &'a mut Box<dyn Pane>,
output: &'a mut Output,
@@ -21,6 +22,10 @@ pub struct PaneContentsAndUi<'a> {
current_pane_group: HashMap<ClientId, Vec<PaneId>>,
show_help_text: bool,
omit_title: bool,
+ frame_geom_override: Option<PaneGeom>,
+ stack_list_entry_width: Option<usize>,
+ stack_list_entry_is_selected: bool,
+ blank_title: bool,
}
impl<'a> PaneContentsAndUi<'a> {
@@ -69,8 +74,26 @@ impl<'a> PaneContentsAndUi<'a> {
current_pane_group,
show_help_text,
omit_title,
+ frame_geom_override: None,
+ stack_list_entry_width: None,
+ stack_list_entry_is_selected: false,
+ blank_title: false,
}
}
+ pub fn set_frame_geom_override(&mut self, frame_geom_override: Option<PaneGeom>) {
+ 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<usize>,
+ stack_list_entry_is_selected: bool,
+ ) {
+ self.stack_list_entry_width = stack_list_entry_width;
+ self.stack_list_entry_is_selected = stack_list_entry_is_selected;
+ }
pub fn render_pane_contents_to_multiple_clients(
&mut self,
clients: impl Iterator<Item = ClientId>,
@@ -243,6 +266,21 @@ impl<'a> PaneContentsAndUi<'a> {
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,
+ is_emphasized: pane_is_in_group
+ || (self
+ .mouse_is_hovering_over_pane_for_clients
+ .contains(&client_id)
+ && !pane_focused_for_client_id),
+ });
let frame_params = if session_is_mirrored {
FrameParams {
focused_client,
@@ -264,6 +302,9 @@ impl<'a> PaneContentsAndUi<'a> {
show_help_text: self.show_help_text,
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,
}
} else {
FrameParams {
@@ -286,6 +327,9 @@ impl<'a> PaneContentsAndUi<'a> {
show_help_text: self.show_help_text,
highlight_tooltip,
omit_title: self.omit_title,
+ frame_geom_override: self.frame_geom_override,
+ stack_list_entry,
+ blank_title: self.blank_title,
}
};
diff --git a/zellij-server/src/unit/screen_tests.rs b/zellij-server/src/unit/screen_tests.rs
index ee46dad3..a45ec812 100644
--- a/zellij-server/src/unit/screen_tests.rs
+++ b/zellij-server/src/unit/screen_tests.rs
@@ -306,6 +306,7 @@ fn create_new_screen(
layout_dir,
explicitly_disable_kitty_keyboard_protocol,
stacked_resize,
+ false,
None,
false,
web_sharing,
@@ -695,6 +696,7 @@ impl MockScreen {
let config_options = Options::default();
let mut config = Config::default();
config.options.pane_frame_style = Some(PaneFrameStyle::Full);
+ config.options.stacked_pane_list = Some(false);
let main_client_id = 1;
std::thread::Builder::new()
@@ -5425,6 +5427,7 @@ fn create_new_screen_with_message_capture(
layout_dir,
explicitly_disable_kitty_keyboard_protocol,
stacked_resize,
+ false,
None,
false,
web_sharing,
@@ -8499,6 +8502,7 @@ fn create_new_screen_with_forward_capture(size: Size) -> (Screen, ForwardCapture
layout_dir,
explicitly_disable_kitty_keyboard_protocol,
stacked_resize,
+ false,
None,
false,
web_sharing,
@@ -9082,6 +9086,7 @@ fn create_new_screen_with_theme_capture(size: Size) -> (Screen, ThemeCapture) {
None,
false,
true,
+ false,
None,
false,
web_sharing,
@@ -9561,6 +9566,7 @@ fn create_non_mirrored_screen(size: Size) -> Screen {
None, // layout_dir
false, // explicitly_disable_kitty_keyboard_protocol
true, // stacked_resize
+ false,
None,
false,
WebSharing::Off,