feat: stack pane action (#4255)

* refactor: group placement properties

* add stackpane cli and keybinding

* add test

* refactor: move spawn vertically/horizontally to spawnterminal

* fix tests and cleanups

* some cleanups and minor fixes

* more cleanups

* add stack action to the UI

* style(fmt): rustfmt

* fix serialization

* add to default config

* fix e2e tests

* style(fmt): rustfmt

* fix cli

* fix tests

* docs(changelog): add PR
This commit is contained in:
Aram Drevekenin 2025-07-01 20:17:37 +02:00 committed by GitHub
parent 02a0d055b6
commit ca0048bdcb
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
44 changed files with 1202 additions and 863 deletions

View file

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

View file

@ -31,6 +31,7 @@ keybinds clear-defaults=true {{
bind "n" {{ NewPane; SwitchToMode "Locked"; }}
bind "d" {{ NewPane "Down"; SwitchToMode "Locked"; }}
bind "r" {{ NewPane "Right"; SwitchToMode "Locked"; }}
bind "s" {{ NewPane "stacked"; SwitchToMode "Locked"; }}
bind "x" {{ CloseFocus; SwitchToMode "Locked"; }}
bind "f" {{ ToggleFocusFullscreen; SwitchToMode "Locked"; }}
bind "z" {{ TogglePaneFrames; SwitchToMode "Locked"; }}
@ -244,6 +245,7 @@ keybinds clear-defaults=true {{
bind "n" {{ NewPane; SwitchToMode "Normal"; }}
bind "d" {{ NewPane "Down"; SwitchToMode "Normal"; }}
bind "r" {{ NewPane "Right"; SwitchToMode "Normal"; }}
bind "s" {{ NewPane "stacked"; SwitchToMode "Normal"; }}
bind "x" {{ CloseFocus; SwitchToMode "Normal"; }}
bind "f" {{ ToggleFocusFullscreen; SwitchToMode "Normal"; }}
bind "z" {{ TogglePaneFrames; SwitchToMode "Normal"; }}
@ -468,6 +470,7 @@ keybinds clear-defaults=true {{
bind "n" {{ NewPane; SwitchToMode "Normal"; }}
bind "d" {{ NewPane "Down"; SwitchToMode "Normal"; }}
bind "r" {{ NewPane "Right"; SwitchToMode "Normal"; }}
bind "s" {{ NewPane "stacked"; SwitchToMode "Normal"; }}
bind "x" {{ CloseFocus; SwitchToMode "Normal"; }}
bind "f" {{ ToggleFocusFullscreen; SwitchToMode "Normal"; }}
bind "z" {{ TogglePaneFrames; SwitchToMode "Normal"; }}
@ -663,6 +666,7 @@ keybinds clear-defaults=true {{
bind "n" {{ NewPane; SwitchToMode "Normal"; }}
bind "d" {{ NewPane "Down"; SwitchToMode "Normal"; }}
bind "r" {{ NewPane "Right"; SwitchToMode "Normal"; }}
bind "s" {{ NewPane "stacked"; SwitchToMode "Normal"; }}
bind "x" {{ CloseFocus; SwitchToMode "Normal"; }}
bind "f" {{ ToggleFocusFullscreen; SwitchToMode "Normal"; }}
bind "z" {{ TogglePaneFrames; SwitchToMode "Normal"; }}
@ -865,6 +869,7 @@ keybinds clear-defaults=true {{
bind "n" {{ NewPane; SwitchToMode "Normal"; }}
bind "d" {{ NewPane "Down"; SwitchToMode "Normal"; }}
bind "r" {{ NewPane "Right"; SwitchToMode "Normal"; }}
bind "s" {{ NewPane "stacked"; SwitchToMode "Normal"; }}
bind "x" {{ CloseFocus; SwitchToMode "Normal"; }}
bind "f" {{ ToggleFocusFullscreen; SwitchToMode "Normal"; }}
bind "z" {{ TogglePaneFrames; SwitchToMode "Normal"; }}
@ -1042,6 +1047,7 @@ keybinds clear-defaults=true {{
bind "n" {{ NewPane; SwitchToMode "Normal"; }}
bind "d" {{ NewPane "Down"; SwitchToMode "Normal"; }}
bind "r" {{ NewPane "Right"; SwitchToMode "Normal"; }}
bind "s" {{ NewPane "stacked"; SwitchToMode "Normal"; }}
bind "x" {{ CloseFocus; SwitchToMode "Normal"; }}
bind "f" {{ ToggleFocusFullscreen; SwitchToMode "Normal"; }}
bind "z" {{ TogglePaneFrames; SwitchToMode "Normal"; }}

View file

@ -1276,6 +1276,7 @@ fn get_keys_and_hints(mi: &ModeInfo) -> Vec<(String, String, Vec<KeyWithModifier
(s("Toggle Embed"), s("Embed"), single_action_key(&km, &[A::TogglePaneEmbedOrFloating, TO_NORMAL])),
(s("Split Right"), s("Right"), single_action_key(&km, &[A::NewPane(Some(Direction::Right), None, false), TO_NORMAL])),
(s("Split Down"), s("Down"), single_action_key(&km, &[A::NewPane(Some(Direction::Down), None, false), TO_NORMAL])),
(s("Stack"), s("Stack"), single_action_key(&km, &[A::NewStackedPane(None, None), TO_NORMAL])),
(s("Select pane"), s("Select"), to_basemode_key),
]} else if mi.mode == IM::Tab {
// With the default bindings, "Move focus" for tabs is tricky: It binds all the arrow keys

View file

@ -38,6 +38,7 @@ fn main() {
width,
height,
pinned,
stacked,
})) = opts.command
{
let cwd = cwd.or_else(|| std::env::current_dir().ok());
@ -59,6 +60,7 @@ fn main() {
width,
height,
pinned,
stacked,
};
commands::send_action_to_session(command_cli_action, opts.session, config);
std::process::exit(0);
@ -77,6 +79,7 @@ fn main() {
})) = opts.command
{
let cwd = None;
let stacked = false;
let command_cli_action = CliAction::NewPane {
command: vec![],
plugin: Some(url),
@ -94,6 +97,7 @@ fn main() {
width,
height,
pinned,
stacked,
};
commands::send_action_to_session(command_cli_action, opts.session, config);
std::process::exit(0);

View file

@ -1045,7 +1045,6 @@ pub fn resize_terminal_window() {
name: "wait for terminal to be resized and app to be re-rendered",
instruction: |remote_terminal: RemoteTerminal| -> bool {
let mut step_is_complete = false;
eprintln!("current_snapshot: {}", remote_terminal.current_snapshot());
if remote_terminal.cursor_position_is(53, 2) && remote_terminal.ctrl_plus_appears()
{
// size has been changed

View file

@ -395,6 +395,72 @@ impl TiledPanes {
},
}
}
pub fn add_pane_to_stack_of_active_pane(
&mut self,
pane_id: PaneId,
mut pane: Box<dyn Pane>,
client_id: ClientId,
) {
let mut pane_grid = TiledPaneGrid::new(
&mut self.panes,
&self.panes_to_hide,
*self.display_area.borrow(),
*self.viewport.borrow(),
);
let Some(active_pane_id) = self.active_panes.get(&client_id) else {
log::error!("Could not find active pane id for client_id");
return;
};
let pane_id_is_stacked = pane_grid
.get_pane_geom(active_pane_id)
.map(|p| p.is_stacked())
.unwrap_or(false);
if !pane_id_is_stacked {
let _ = pane_grid.make_pane_stacked(&active_pane_id);
}
match pane_grid.make_room_in_stack_of_pane_id_for_pane(active_pane_id) {
Ok(new_pane_geom) => {
pane.set_geom(new_pane_geom);
self.panes.insert(pane_id, pane);
self.set_force_render(); // TODO: why do we need this?
return;
},
Err(e) => {
log::error!("Failed to add pane to stack: {}", e);
},
}
}
pub fn add_pane_to_stack_of_pane_id(
&mut self,
pane_id: PaneId,
mut pane: Box<dyn Pane>,
root_pane_id: PaneId,
) {
let mut pane_grid = TiledPaneGrid::new(
&mut self.panes,
&self.panes_to_hide,
*self.display_area.borrow(),
*self.viewport.borrow(),
);
let pane_id_is_stacked = pane_grid
.get_pane_geom(&root_pane_id)
.map(|p| p.is_stacked())
.unwrap_or(false);
if !pane_id_is_stacked {
let _ = pane_grid.make_pane_stacked(&root_pane_id);
}
match pane_grid.make_room_in_stack_of_pane_id_for_pane(&root_pane_id) {
Ok(new_pane_geom) => {
pane.set_geom(new_pane_geom);
self.panes.insert(pane_id, pane);
self.set_force_render(); // TODO: why do we need this?
return;
},
Err(e) => {
log::error!("Failed to add pane to stack: {}", e);
},
}
}
pub fn fixed_pane_geoms(&self) -> Vec<Viewport> {
self.panes
.values()

View file

@ -1,7 +1,6 @@
---
source: zellij-server/src/plugins/./unit/plugin_tests.rs
assertion_line: 6878
expression: "format!(\"{:#?}\",\n new_tab_event).replace(&format!(\"{:?}\", temp_folder.path()),\n \"\\\"CWD\\\"\")"
expression: "format!(\"{:#?}\",\nnew_tab_event).replace(&format!(\"{:?}\", temp_folder.path()), \"\\\"CWD\\\"\")"
---
Some(
SpawnTerminal(
@ -29,8 +28,7 @@ Some(
),
),
None,
None,
None,
NoPreference,
true,
ClientId(
1,

View file

@ -1,6 +1,5 @@
---
source: zellij-server/src/plugins/./unit/plugin_tests.rs
assertion_line: 4557
expression: "format!(\"{:#?}\", new_tab_event)"
---
Some(
@ -26,11 +25,10 @@ Some(
},
),
),
Some(
true,
None,
Floating(
None,
),
None,
None,
false,
ClientId(
1,

View file

@ -1,6 +1,5 @@
---
source: zellij-server/src/plugins/./unit/plugin_tests.rs
assertion_line: 4479
expression: "format!(\"{:#?}\", new_tab_event)"
---
Some(
@ -26,11 +25,10 @@ Some(
},
),
),
Some(
false,
None,
Tiled(
None,
),
None,
None,
false,
ClientId(
1,

View file

@ -1,7 +1,6 @@
---
source: zellij-server/src/plugins/./unit/plugin_tests.rs
assertion_line: 3996
expression: "format!(\"{:#?}\",\n new_tab_event).replace(&format!(\"{:?}\", temp_folder.path()),\n \"\\\"CWD\\\"\")"
expression: "format!(\"{:#?}\",\nnew_tab_event).replace(&format!(\"{:?}\", temp_folder.path()), \"\\\"CWD\\\"\")"
---
Some(
SpawnTerminal(
@ -23,13 +22,12 @@ Some(
},
),
),
Some(
true,
),
Some(
"Editing: /path/to/my/file.rs",
),
None,
Floating(
None,
),
false,
ClientId(
1,

View file

@ -1,7 +1,6 @@
---
source: zellij-server/src/plugins/./unit/plugin_tests.rs
assertion_line: 4078
expression: "format!(\"{:#?}\",\n new_tab_event).replace(&format!(\"{:?}\", temp_folder.path()),\n \"\\\"CWD\\\"\")"
expression: "format!(\"{:#?}\",\nnew_tab_event).replace(&format!(\"{:?}\", temp_folder.path()), \"\\\"CWD\\\"\")"
---
Some(
SpawnTerminal(
@ -23,13 +22,12 @@ Some(
},
),
),
Some(
false,
),
Some(
"Editing: /path/to/my/file.rs",
),
None,
Tiled(
None,
),
false,
ClientId(
1,

View file

@ -1,7 +1,6 @@
---
source: zellij-server/src/plugins/./unit/plugin_tests.rs
assertion_line: 4243
expression: "format!(\"{:#?}\",\n new_tab_event).replace(&format!(\"{:?}\", temp_folder.path()),\n \"\\\"CWD\\\"\")"
expression: "format!(\"{:#?}\",\nnew_tab_event).replace(&format!(\"{:?}\", temp_folder.path()), \"\\\"CWD\\\"\")"
---
Some(
SpawnTerminal(
@ -25,13 +24,12 @@ Some(
},
),
),
Some(
true,
),
Some(
"Editing: /path/to/my/file.rs",
),
None,
Floating(
None,
),
false,
ClientId(
1,

View file

@ -1,7 +1,6 @@
---
source: zellij-server/src/plugins/./unit/plugin_tests.rs
assertion_line: 4161
expression: "format!(\"{:#?}\",\n new_tab_event).replace(&format!(\"{:?}\", temp_folder.path()),\n \"\\\"CWD\\\"\")"
expression: "format!(\"{:#?}\",\nnew_tab_event).replace(&format!(\"{:?}\", temp_folder.path()), \"\\\"CWD\\\"\")"
---
Some(
SpawnTerminal(
@ -25,13 +24,12 @@ Some(
},
),
),
Some(
false,
),
Some(
"Editing: /path/to/my/file.rs",
),
None,
Tiled(
None,
),
false,
ClientId(
1,

View file

@ -1,6 +1,5 @@
---
source: zellij-server/src/plugins/./unit/plugin_tests.rs
assertion_line: 4401
expression: "format!(\"{:#?}\", new_tab_event)"
---
Some(
@ -19,11 +18,10 @@ Some(
},
),
),
Some(
true,
None,
Floating(
None,
),
None,
None,
false,
ClientId(
1,

View file

@ -1,6 +1,5 @@
---
source: zellij-server/src/plugins/./unit/plugin_tests.rs
assertion_line: 4323
expression: "format!(\"{:#?}\", new_tab_event)"
---
Some(
@ -19,11 +18,10 @@ Some(
},
),
),
Some(
false,
None,
Tiled(
None,
),
None,
None,
false,
ClientId(
1,

View file

@ -2,7 +2,7 @@ use super::PluginInstruction;
use crate::background_jobs::BackgroundJob;
use crate::plugins::plugin_map::PluginEnv;
use crate::plugins::wasm_bridge::handle_plugin_crash;
use crate::pty::{ClientTabIndexOrPaneId, PtyInstruction};
use crate::pty::{ClientTabIndexOrPaneId, NewPanePlacement, PtyInstruction};
use crate::route::route_action;
use crate::ServerInstruction;
use async_std::task;
@ -744,14 +744,12 @@ fn open_file_near_plugin(
OriginatingPlugin::new(env.plugin_id, env.client_id, context),
);
let title = format!("Editing: {}", open_file_payload.path.display());
let should_float = false;
let start_suppressed = false;
let open_file = TerminalAction::OpenFile(open_file_payload);
let pty_instr = PtyInstruction::SpawnTerminal(
Some(open_file),
Some(should_float),
Some(title),
None,
NewPanePlacement::default(),
start_suppressed,
ClientTabIndexOrPaneId::PaneId(PaneId::Plugin(env.plugin_id)),
);
@ -774,14 +772,12 @@ fn open_file_floating_near_plugin(
OriginatingPlugin::new(env.plugin_id, env.client_id, context),
);
let title = format!("Editing: {}", open_file_payload.path.display());
let should_float = true;
let start_suppressed = false;
let open_file = TerminalAction::OpenFile(open_file_payload);
let pty_instr = PtyInstruction::SpawnTerminal(
Some(open_file),
Some(should_float),
Some(title),
floating_pane_coordinates,
NewPanePlacement::Floating(floating_pane_coordinates),
start_suppressed,
ClientTabIndexOrPaneId::PaneId(PaneId::Plugin(env.plugin_id)),
);
@ -834,7 +830,6 @@ fn open_terminal(env: &PluginEnv, cwd: PathBuf) {
fn open_terminal_near_plugin(env: &PluginEnv, cwd: PathBuf) {
let cwd = env.plugin_cwd.join(cwd);
let should_float = false;
let mut default_shell = env.default_shell.clone().unwrap_or_else(|| {
TerminalAction::RunCommand(RunCommand {
command: env.path_to_default_shell.clone(),
@ -845,9 +840,8 @@ fn open_terminal_near_plugin(env: &PluginEnv, cwd: PathBuf) {
default_shell.change_cwd(cwd);
let _ = env.senders.send_to_pty(PtyInstruction::SpawnTerminal(
Some(default_shell),
Some(should_float),
name,
None,
NewPanePlacement::Tiled(None),
false,
ClientTabIndexOrPaneId::PaneId(PaneId::Plugin(env.plugin_id)),
));
@ -881,7 +875,6 @@ fn open_terminal_floating_near_plugin(
floating_pane_coordinates: Option<FloatingPaneCoordinates>,
) {
let cwd = env.plugin_cwd.join(cwd);
let should_float = true;
let mut default_shell = env.default_shell.clone().unwrap_or_else(|| {
TerminalAction::RunCommand(RunCommand {
command: env.path_to_default_shell.clone(),
@ -892,9 +885,8 @@ fn open_terminal_floating_near_plugin(
let name = None;
let _ = env.senders.send_to_pty(PtyInstruction::SpawnTerminal(
Some(default_shell),
Some(should_float),
name,
floating_pane_coordinates,
NewPanePlacement::Floating(floating_pane_coordinates),
false,
ClientTabIndexOrPaneId::PaneId(PaneId::Plugin(env.plugin_id)),
));
@ -1021,7 +1013,6 @@ fn open_command_pane_near_plugin(
let hold_on_close = true;
let hold_on_start = false;
let name = None;
let should_float = false;
let run_command_action = RunCommandAction {
command,
args,
@ -1038,9 +1029,8 @@ fn open_command_pane_near_plugin(
let run_cmd = TerminalAction::RunCommand(run_command_action.into());
let _ = env.senders.send_to_pty(PtyInstruction::SpawnTerminal(
Some(run_cmd),
Some(should_float),
name,
None,
NewPanePlacement::Tiled(None),
false,
ClientTabIndexOrPaneId::PaneId(PaneId::Plugin(env.plugin_id)),
));
@ -1090,7 +1080,6 @@ fn open_command_pane_floating_near_plugin(
let hold_on_close = true;
let hold_on_start = false;
let name = None;
let should_float = true;
let run_command_action = RunCommandAction {
command,
args,
@ -1107,9 +1096,8 @@ fn open_command_pane_floating_near_plugin(
let run_cmd = TerminalAction::RunCommand(run_command_action.into());
let _ = env.senders.send_to_pty(PtyInstruction::SpawnTerminal(
Some(run_cmd),
Some(should_float),
name,
floating_pane_coordinates,
NewPanePlacement::Floating(floating_pane_coordinates),
false,
ClientTabIndexOrPaneId::PaneId(PaneId::Plugin(env.plugin_id)),
));
@ -1177,9 +1165,8 @@ fn open_command_pane_background(
let run_cmd = TerminalAction::RunCommand(run_command_action.into());
let _ = env.senders.send_to_pty(PtyInstruction::SpawnTerminal(
Some(run_cmd),
None,
name,
None,
NewPanePlacement::default(),
start_suppressed,
ClientTabIndexOrPaneId::ClientId(env.client_id),
));

View file

@ -16,7 +16,7 @@ use nix::unistd::Pid;
use std::sync::Arc;
use std::{collections::HashMap, os::unix::io::RawFd, path::PathBuf};
use zellij_utils::{
data::{Event, FloatingPaneCoordinates, OriginatingPlugin},
data::{Direction, Event, FloatingPaneCoordinates, OriginatingPlugin},
errors::prelude::*,
errors::{ContextType, PtyContext},
input::{
@ -37,26 +37,95 @@ pub enum ClientTabIndexOrPaneId {
PaneId(PaneId),
}
// TODO: move elsewhere
#[derive(Clone, Debug)]
pub enum NewPanePlacement {
NoPreference,
Tiled(Option<Direction>),
Floating(Option<FloatingPaneCoordinates>),
InPlace {
pane_id_to_replace: Option<PaneId>,
close_replaced_pane: bool,
},
Stacked(Option<PaneId>),
}
impl Default for NewPanePlacement {
fn default() -> Self {
NewPanePlacement::NoPreference
}
}
impl NewPanePlacement {
pub fn with_floating_pane_coordinates(
floating_pane_coordinates: Option<FloatingPaneCoordinates>,
) -> Self {
NewPanePlacement::Floating(floating_pane_coordinates)
}
pub fn with_should_be_in_place(
self,
should_be_in_place: bool,
close_replaced_pane: bool,
) -> Self {
if should_be_in_place {
NewPanePlacement::InPlace {
pane_id_to_replace: None,
close_replaced_pane,
}
} else {
self
}
}
pub fn with_pane_id_to_replace(
pane_id_to_replace: Option<PaneId>,
close_replaced_pane: bool,
) -> Self {
NewPanePlacement::InPlace {
pane_id_to_replace,
close_replaced_pane,
}
}
pub fn should_float(&self) -> Option<bool> {
match self {
NewPanePlacement::Floating(_) => Some(true),
NewPanePlacement::Tiled(_) => Some(false),
_ => None,
}
}
pub fn floating_pane_coordinates(&self) -> Option<FloatingPaneCoordinates> {
match self {
NewPanePlacement::Floating(floating_pane_coordinates) => {
floating_pane_coordinates.clone()
},
_ => None,
}
}
pub fn should_stack(&self) -> bool {
match self {
NewPanePlacement::Stacked(_) => true,
_ => false,
}
}
pub fn id_of_stack_root(&self) -> Option<PaneId> {
match self {
NewPanePlacement::Stacked(id) => *id,
_ => None,
}
}
}
/// Instructions related to PTYs (pseudoterminals).
#[derive(Clone, Debug)]
pub enum PtyInstruction {
SpawnTerminal(
Option<TerminalAction>,
Option<bool>,
Option<String>,
Option<FloatingPaneCoordinates>,
NewPanePlacement,
bool, // start suppressed
ClientTabIndexOrPaneId,
), // bool (if Some) is
// should_float, String is an optional pane name
OpenInPlaceEditor(PathBuf, Option<usize>, ClientTabIndexOrPaneId), // Option<usize> is the optional line number
SpawnTerminalVertically(Option<TerminalAction>, Option<String>, ClientId), // String is an
// optional pane
// name
// bool is start_suppressed
SpawnTerminalHorizontally(Option<TerminalAction>, Option<String>, ClientId), // String is an
// optional pane
// name
UpdateActivePane(Option<PaneId>, ClientId),
GoToTab(TabIndex, ClientId),
NewTab(
@ -114,8 +183,6 @@ impl From<&PtyInstruction> for PtyContext {
match *pty_instruction {
PtyInstruction::SpawnTerminal(..) => PtyContext::SpawnTerminal,
PtyInstruction::OpenInPlaceEditor(..) => PtyContext::OpenInPlaceEditor,
PtyInstruction::SpawnTerminalVertically(..) => PtyContext::SpawnTerminalVertically,
PtyInstruction::SpawnTerminalHorizontally(..) => PtyContext::SpawnTerminalHorizontally,
PtyInstruction::UpdateActivePane(..) => PtyContext::UpdateActivePane,
PtyInstruction::GoToTab(..) => PtyContext::GoToTab,
PtyInstruction::ClosePane(_) => PtyContext::ClosePane,
@ -153,9 +220,8 @@ pub(crate) fn pty_thread_main(mut pty: Pty, layout: Box<Layout>) -> Result<()> {
match event {
PtyInstruction::SpawnTerminal(
terminal_action,
should_float,
name,
floating_pane_coordinates,
new_pane_placement,
start_suppressed,
client_or_tab_index,
) => {
@ -235,10 +301,9 @@ pub(crate) fn pty_thread_main(mut pty: Pty, layout: Box<Layout>) -> Result<()> {
.send_to_screen(ScreenInstruction::NewPane(
PaneId::Terminal(pid),
pane_title,
should_float,
hold_for_command,
invoked_with,
floating_pane_coordinates,
new_pane_placement,
start_suppressed,
client_or_tab_index,
))
@ -253,10 +318,9 @@ pub(crate) fn pty_thread_main(mut pty: Pty, layout: Box<Layout>) -> Result<()> {
.send_to_screen(ScreenInstruction::NewPane(
PaneId::Terminal(*terminal_id),
pane_title,
should_float,
hold_for_command,
invoked_with,
floating_pane_coordinates,
new_pane_placement,
start_suppressed,
client_or_tab_index,
))
@ -390,144 +454,6 @@ pub(crate) fn pty_thread_main(mut pty: Pty, layout: Box<Layout>) -> Result<()> {
},
}
},
PtyInstruction::SpawnTerminalVertically(terminal_action, name, client_id) => {
let err_context =
|| format!("failed to spawn terminal vertically for client {client_id}");
let (hold_on_close, run_command, pane_title) = match &terminal_action {
Some(TerminalAction::RunCommand(run_command)) => (
run_command.hold_on_close,
Some(run_command.clone()),
Some(name.unwrap_or_else(|| run_command.to_string())),
),
_ => (false, None, name),
};
match pty
.spawn_terminal(terminal_action, ClientTabIndexOrPaneId::ClientId(client_id))
.with_context(err_context)
{
Ok((pid, starts_held)) => {
let hold_for_command = if starts_held { run_command } else { None };
pty.bus
.senders
.send_to_screen(ScreenInstruction::VerticalSplit(
PaneId::Terminal(pid),
pane_title,
hold_for_command,
client_id,
))
.with_context(err_context)?;
},
Err(err) => match err.downcast_ref::<ZellijError>() {
Some(ZellijError::CommandNotFound { terminal_id, .. }) => {
let hold_for_command = None; // we do not hold an "error" pane
if hold_on_close {
pty.bus
.senders
.send_to_screen(ScreenInstruction::VerticalSplit(
PaneId::Terminal(*terminal_id),
pane_title,
hold_for_command,
client_id,
))
.with_context(err_context)?;
if let Some(run_command) = run_command {
pty.bus
.senders
.send_to_screen(ScreenInstruction::PtyBytes(
*terminal_id,
format!(
"Command not found: {}",
run_command.command.display()
)
.as_bytes()
.to_vec(),
))
.with_context(err_context)?;
pty.bus
.senders
.send_to_screen(ScreenInstruction::HoldPane(
PaneId::Terminal(*terminal_id),
Some(2), // exit status
run_command,
))
.with_context(err_context)?;
}
}
},
_ => Err::<(), _>(err).non_fatal(),
},
}
},
PtyInstruction::SpawnTerminalHorizontally(terminal_action, name, client_id) => {
let err_context =
|| format!("failed to spawn terminal horizontally for client {client_id}");
let (hold_on_close, run_command, pane_title) = match &terminal_action {
Some(TerminalAction::RunCommand(run_command)) => (
run_command.hold_on_close,
Some(run_command.clone()),
Some(name.unwrap_or_else(|| run_command.to_string())),
),
_ => (false, None, name),
};
match pty
.spawn_terminal(terminal_action, ClientTabIndexOrPaneId::ClientId(client_id))
.with_context(err_context)
{
Ok((pid, starts_held)) => {
let hold_for_command = if starts_held { run_command } else { None };
pty.bus
.senders
.send_to_screen(ScreenInstruction::HorizontalSplit(
PaneId::Terminal(pid),
pane_title,
hold_for_command,
client_id,
))
.with_context(err_context)?;
},
Err(err) => match err.downcast_ref::<ZellijError>() {
Some(ZellijError::CommandNotFound { terminal_id, .. }) => {
if hold_on_close {
let hold_for_command = None; // we do not hold an "error" pane
pty.bus
.senders
.send_to_screen(ScreenInstruction::HorizontalSplit(
PaneId::Terminal(*terminal_id),
pane_title,
hold_for_command,
client_id,
))
.with_context(err_context)?;
if let Some(run_command) = run_command {
pty.bus
.senders
.send_to_screen(ScreenInstruction::PtyBytes(
*terminal_id,
format!(
"Command not found: {}",
run_command.command.display()
)
.as_bytes()
.to_vec(),
))
.with_context(err_context)?;
pty.bus
.senders
.send_to_screen(ScreenInstruction::HoldPane(
PaneId::Terminal(*terminal_id),
Some(2), // exit status
run_command,
))
.with_context(err_context)?;
}
}
},
_ => Err::<(), _>(err).non_fatal(),
},
}
},
PtyInstruction::UpdateActivePane(pane_id, client_id) => {
pty.set_active_pane(pane_id, client_id);
},

View file

@ -6,7 +6,7 @@ use crate::{
os_input_output::ServerOsApi,
panes::PaneId,
plugins::PluginInstruction,
pty::{ClientTabIndexOrPaneId, PtyInstruction},
pty::{ClientTabIndexOrPaneId, NewPanePlacement, PtyInstruction},
screen::ScreenInstruction,
ServerInstruction, SessionMetaData, SessionState,
};
@ -257,30 +257,13 @@ pub(crate) fn route_action(
},
Action::NewPane(direction, name, start_suppressed) => {
let shell = default_shell.clone();
let pty_instr = match direction {
Some(Direction::Left) => {
PtyInstruction::SpawnTerminalVertically(shell, name, client_id)
},
Some(Direction::Right) => {
PtyInstruction::SpawnTerminalVertically(shell, name, client_id)
},
Some(Direction::Up) => {
PtyInstruction::SpawnTerminalHorizontally(shell, name, client_id)
},
Some(Direction::Down) => {
PtyInstruction::SpawnTerminalHorizontally(shell, name, client_id)
},
// No direction specified - try to put it in the biggest available spot
None => PtyInstruction::SpawnTerminal(
shell,
None,
name,
None,
start_suppressed,
ClientTabIndexOrPaneId::ClientId(client_id),
),
};
senders.send_to_pty(pty_instr).with_context(err_context)?;
senders.send_to_pty(PtyInstruction::SpawnTerminal(
shell,
name,
NewPanePlacement::Tiled(direction),
start_suppressed,
ClientTabIndexOrPaneId::ClientId(client_id),
));
},
Action::EditFile(
open_file_payload,
@ -292,25 +275,8 @@ pub(crate) fn route_action(
) => {
let title = format!("Editing: {}", open_file_payload.path.display());
let open_file = TerminalAction::OpenFile(open_file_payload);
let pty_instr = match (split_direction, should_float, should_open_in_place) {
(Some(Direction::Left), false, false) => {
PtyInstruction::SpawnTerminalVertically(Some(open_file), Some(title), client_id)
},
(Some(Direction::Right), false, false) => {
PtyInstruction::SpawnTerminalVertically(Some(open_file), Some(title), client_id)
},
(Some(Direction::Up), false, false) => PtyInstruction::SpawnTerminalHorizontally(
Some(open_file),
Some(title),
client_id,
),
(Some(Direction::Down), false, false) => PtyInstruction::SpawnTerminalHorizontally(
Some(open_file),
Some(title),
client_id,
),
// open terminal in place
(_, _, true) => match pane_id {
let pty_instr = if should_open_in_place {
match pane_id {
Some(pane_id) => PtyInstruction::SpawnInPlaceTerminal(
Some(open_file),
Some(title),
@ -323,17 +289,19 @@ pub(crate) fn route_action(
false,
ClientTabIndexOrPaneId::ClientId(client_id),
),
},
// Open either floating terminal if we were asked with should_float or defer
// placement to screen
(None, _, _) | (_, true, _) => PtyInstruction::SpawnTerminal(
}
} else {
PtyInstruction::SpawnTerminal(
Some(open_file),
Some(should_float),
Some(title),
floating_pane_coordinates,
if should_float {
NewPanePlacement::Floating(floating_pane_coordinates)
} else {
NewPanePlacement::Tiled(split_direction)
},
start_suppressed,
ClientTabIndexOrPaneId::ClientId(client_id),
),
)
};
senders.send_to_pty(pty_instr).with_context(err_context)?;
},
@ -368,16 +336,14 @@ pub(crate) fn route_action(
.with_context(err_context)?;
},
Action::NewFloatingPane(run_command, name, floating_pane_coordinates) => {
let should_float = true;
let run_cmd = run_command
.map(|cmd| TerminalAction::RunCommand(cmd.into()))
.or_else(|| default_shell.clone());
senders
.send_to_pty(PtyInstruction::SpawnTerminal(
run_cmd,
Some(should_float),
name,
floating_pane_coordinates,
NewPanePlacement::Floating(floating_pane_coordinates),
false,
ClientTabIndexOrPaneId::ClientId(client_id),
))
@ -410,35 +376,46 @@ pub(crate) fn route_action(
},
}
},
Action::NewTiledPane(direction, run_command, name) => {
let should_float = false;
Action::NewStackedPane(run_command, name) => {
let run_cmd = run_command
.map(|cmd| TerminalAction::RunCommand(cmd.into()))
.or_else(|| default_shell.clone());
let pty_instr = match direction {
Some(Direction::Left) => {
PtyInstruction::SpawnTerminalVertically(run_cmd, name, client_id)
match pane_id {
Some(pane_id) => {
senders
.send_to_pty(PtyInstruction::SpawnTerminal(
run_cmd,
name,
NewPanePlacement::Stacked(Some(pane_id)),
false,
ClientTabIndexOrPaneId::PaneId(pane_id),
))
.with_context(err_context)?;
},
Some(Direction::Right) => {
PtyInstruction::SpawnTerminalVertically(run_cmd, name, client_id)
None => {
senders
.send_to_pty(PtyInstruction::SpawnTerminal(
run_cmd,
name,
NewPanePlacement::Stacked(None),
false,
ClientTabIndexOrPaneId::ClientId(client_id),
))
.with_context(err_context)?;
},
Some(Direction::Up) => {
PtyInstruction::SpawnTerminalHorizontally(run_cmd, name, client_id)
},
Some(Direction::Down) => {
PtyInstruction::SpawnTerminalHorizontally(run_cmd, name, client_id)
},
// No direction specified - try to put it in the biggest available spot
None => PtyInstruction::SpawnTerminal(
run_cmd,
Some(should_float),
name,
None,
false,
ClientTabIndexOrPaneId::ClientId(client_id),
),
};
senders.send_to_pty(pty_instr).with_context(err_context)?;
}
},
Action::NewTiledPane(direction, run_command, name) => {
let run_cmd = run_command
.map(|cmd| TerminalAction::RunCommand(cmd.into()))
.or_else(|| default_shell.clone());
let _ = senders.send_to_pty(PtyInstruction::SpawnTerminal(
run_cmd,
name,
NewPanePlacement::Tiled(direction),
false,
ClientTabIndexOrPaneId::ClientId(client_id),
));
},
Action::TogglePaneEmbedOrFloating => {
senders
@ -465,30 +442,13 @@ pub(crate) fn route_action(
},
Action::Run(command) => {
let run_cmd = Some(TerminalAction::RunCommand(command.clone().into()));
let pty_instr = match command.direction {
Some(Direction::Left) => {
PtyInstruction::SpawnTerminalVertically(run_cmd, None, client_id)
},
Some(Direction::Right) => {
PtyInstruction::SpawnTerminalVertically(run_cmd, None, client_id)
},
Some(Direction::Up) => {
PtyInstruction::SpawnTerminalHorizontally(run_cmd, None, client_id)
},
Some(Direction::Down) => {
PtyInstruction::SpawnTerminalHorizontally(run_cmd, None, client_id)
},
// No direction specified - try to put it in the biggest available spot
None => PtyInstruction::SpawnTerminal(
run_cmd,
None,
None,
None,
false,
ClientTabIndexOrPaneId::ClientId(client_id),
),
};
senders.send_to_pty(pty_instr).with_context(err_context)?;
let _ = senders.send_to_pty(PtyInstruction::SpawnTerminal(
run_cmd,
None,
NewPanePlacement::Tiled(command.direction),
false,
ClientTabIndexOrPaneId::ClientId(client_id),
));
},
Action::CloseFocus => {
senders

View file

@ -43,7 +43,7 @@ use crate::{
panes::sixel::SixelImageStore,
panes::PaneId,
plugins::{PluginId, PluginInstruction, PluginRenderAsset},
pty::{get_default_shell, ClientTabIndexOrPaneId, PtyInstruction, VteBytes},
pty::{get_default_shell, ClientTabIndexOrPaneId, NewPanePlacement, PtyInstruction, VteBytes},
tab::{SuppressedPanes, Tab},
thread_bus::Bus,
ui::{
@ -138,7 +138,6 @@ macro_rules! active_tab_and_connected_client_id {
}
type InitialTitle = String;
type ShouldFloat = bool;
type HoldForCommand = Option<RunCommand>;
/// Instructions that can be sent to the [`Screen`].
@ -151,10 +150,9 @@ pub enum ScreenInstruction {
NewPane(
PaneId,
Option<InitialTitle>,
Option<ShouldFloat>,
HoldForCommand,
Option<Run>, // invoked with
Option<FloatingPaneCoordinates>,
NewPanePlacement,
bool, // start suppressed
ClientTabIndexOrPaneId,
),
@ -3335,10 +3333,9 @@ pub(crate) fn screen_thread_main(
ScreenInstruction::NewPane(
pid,
initial_pane_title,
should_float,
hold_for_command,
invoked_with,
floating_pane_coordinates,
new_pane_placement,
start_suppressed,
client_or_tab_index,
) => {
@ -3347,11 +3344,10 @@ pub(crate) fn screen_thread_main(
active_tab_and_connected_client_id!(screen, client_id, |tab: &mut Tab, client_id: ClientId| {
tab.new_pane(pid,
initial_pane_title,
should_float,
invoked_with,
floating_pane_coordinates,
start_suppressed,
true,
new_pane_placement,
Some(client_id)
)
}, ?);
@ -3374,11 +3370,10 @@ pub(crate) fn screen_thread_main(
active_tab.new_pane(
pid,
initial_pane_title,
should_float,
invoked_with,
floating_pane_coordinates,
start_suppressed,
true,
new_pane_placement,
None,
)?;
if let Some(hold_for_command) = hold_for_command {
@ -3397,11 +3392,10 @@ pub(crate) fn screen_thread_main(
tab.new_pane(
pid,
initial_pane_title,
should_float,
invoked_with,
floating_pane_coordinates,
start_suppressed,
true,
new_pane_placement,
None,
)?;
if let Some(hold_for_command) = hold_for_command {
@ -4659,9 +4653,28 @@ pub(crate) fn screen_thread_main(
should_focus_plugin,
client_id,
) => {
let close_replaced_pane = false; // TODO: support this
let mut new_pane_placement = NewPanePlacement::default();
let maybe_should_float = should_float;
let should_be_tiled = maybe_should_float.map(|f| !f).unwrap_or(false);
let should_float = maybe_should_float.unwrap_or(false);
if floating_pane_coordinates.is_some() || should_float {
new_pane_placement = NewPanePlacement::with_floating_pane_coordinates(
floating_pane_coordinates.clone(),
);
}
if should_be_tiled {
new_pane_placement = NewPanePlacement::Tiled(None);
}
if should_be_in_place {
new_pane_placement = NewPanePlacement::with_pane_id_to_replace(
pane_id_to_replace,
close_replaced_pane,
);
}
if screen.active_tab_indices.is_empty() && tab_index.is_none() {
pending_events_waiting_for_client.push(ScreenInstruction::AddPlugin(
should_float,
maybe_should_float,
should_be_in_place,
run_plugin_or_alias,
pane_title,
@ -4718,11 +4731,10 @@ pub(crate) fn screen_thread_main(
active_tab.new_pane(
PaneId::Plugin(plugin_id),
Some(pane_title),
should_float,
Some(run_plugin),
floating_pane_coordinates,
start_suppressed,
should_focus_plugin.unwrap_or(true),
new_pane_placement,
Some(client_id),
)
}, ?);
@ -4732,11 +4744,10 @@ pub(crate) fn screen_thread_main(
active_tab.new_pane(
PaneId::Plugin(plugin_id),
Some(pane_title),
should_float,
Some(run_plugin),
None,
start_suppressed,
should_focus_plugin.unwrap_or(true),
new_pane_placement,
None,
)?;
} else {

View file

@ -39,7 +39,7 @@ use crate::{
panes::{FloatingPanes, TiledPanes},
panes::{LinkHandler, PaneId, PluginPane, TerminalPane},
plugins::PluginInstruction,
pty::{ClientTabIndexOrPaneId, PtyInstruction, VteBytes},
pty::{ClientTabIndexOrPaneId, NewPanePlacement, PtyInstruction, VteBytes},
thread_bus::ThreadSenders,
ClientId, ServerInstruction,
};
@ -1238,7 +1238,6 @@ impl Tab {
},
None => {
let name = None;
let should_float = true;
let client_id_or_tab_index = match client_id {
Some(client_id) => ClientTabIndexOrPaneId::ClientId(client_id),
None => ClientTabIndexOrPaneId::TabIndex(self.index),
@ -1246,9 +1245,8 @@ impl Tab {
let should_start_suppressed = false;
let instruction = PtyInstruction::SpawnTerminal(
default_shell,
Some(should_float),
name,
None,
NewPanePlacement::Floating(None),
should_start_suppressed,
client_id_or_tab_index,
);
@ -1266,21 +1264,81 @@ impl Tab {
&mut self,
pid: PaneId,
initial_pane_title: Option<String>,
should_float: Option<bool>,
invoked_with: Option<Run>,
floating_pane_coordinates: Option<FloatingPaneCoordinates>,
start_suppressed: bool,
should_focus_pane: bool,
new_pane_placement: NewPanePlacement,
client_id: Option<ClientId>,
) -> Result<()> {
match new_pane_placement {
NewPanePlacement::NoPreference => self.new_no_preference_pane(
pid,
initial_pane_title,
invoked_with,
start_suppressed,
should_focus_pane,
client_id,
),
NewPanePlacement::Tiled(None) => self.new_tiled_pane(
pid,
initial_pane_title,
invoked_with,
start_suppressed,
should_focus_pane,
client_id,
),
NewPanePlacement::Tiled(Some(direction)) => {
if let Some(client_id) = client_id {
if direction == Direction::Left || direction == Direction::Right {
self.vertical_split(pid, initial_pane_title, client_id)?;
} else {
self.horizontal_split(pid, initial_pane_title, client_id)?;
}
}
Ok(())
},
NewPanePlacement::Floating(floating_pane_coordinates) => self.new_floating_pane(
pid,
initial_pane_title,
invoked_with,
start_suppressed,
should_focus_pane,
floating_pane_coordinates,
),
NewPanePlacement::InPlace {
pane_id_to_replace,
close_replaced_pane,
} => self.new_in_place_pane(
pid,
initial_pane_title,
invoked_with,
start_suppressed,
should_focus_pane,
pane_id_to_replace,
close_replaced_pane,
client_id,
),
NewPanePlacement::Stacked(pane_id_to_stack_under) => self.new_stacked_pane(
pid,
initial_pane_title,
invoked_with,
start_suppressed,
should_focus_pane,
pane_id_to_stack_under,
client_id,
),
}
}
pub fn new_no_preference_pane(
&mut self,
pid: PaneId,
initial_pane_title: Option<String>,
invoked_with: Option<Run>,
start_suppressed: bool,
should_focus_pane: bool,
client_id: Option<ClientId>,
) -> Result<()> {
let err_context = || format!("failed to create new pane with id {pid:?}");
if should_focus_pane {
match should_float {
Some(true) => self.show_floating_panes(),
Some(false) => self.hide_floating_panes(),
None => {},
};
}
self.close_down_to_max_terminals()
.with_context(err_context)?;
let mut new_pane = match pid {
@ -1357,23 +1415,340 @@ impl Tab {
Ok(())
} else if should_focus_pane {
if self.floating_panes.panes_are_visible() {
self.add_floating_pane(new_pane, pid, floating_pane_coordinates, true)
self.add_floating_pane(new_pane, pid, None, true)
} else {
self.add_tiled_pane(new_pane, pid, client_id)
}
} else {
match should_float {
Some(true) => {
self.add_floating_pane(new_pane, pid, floating_pane_coordinates, false)
if self.floating_panes.panes_are_visible() {
self.add_floating_pane(new_pane, pid, None, false)
} else {
self.add_tiled_pane(new_pane, pid, client_id)
}
}
}
pub fn new_tiled_pane(
&mut self,
pid: PaneId,
initial_pane_title: Option<String>,
invoked_with: Option<Run>,
start_suppressed: bool,
should_focus_pane: bool,
client_id: Option<ClientId>,
) -> Result<()> {
let err_context = || format!("failed to create new pane with id {pid:?}");
if should_focus_pane {
self.hide_floating_panes();
}
self.close_down_to_max_terminals()
.with_context(err_context)?;
let mut new_pane = match pid {
PaneId::Terminal(term_pid) => {
let next_terminal_position = self.get_next_terminal_position();
Box::new(TerminalPane::new(
term_pid,
PaneGeom::default(), // this will be filled out later
self.style,
next_terminal_position,
String::new(),
self.link_handler.clone(),
self.character_cell_size.clone(),
self.sixel_image_store.clone(),
self.terminal_emulator_colors.clone(),
self.terminal_emulator_color_codes.clone(),
initial_pane_title,
invoked_with,
self.debug,
self.arrow_fonts,
self.styled_underlines,
self.explicitly_disable_kitty_keyboard_protocol,
)) as Box<dyn Pane>
},
PaneId::Plugin(plugin_pid) => {
Box::new(PluginPane::new(
plugin_pid,
PaneGeom::default(), // this will be filled out later
self.senders
.to_plugin
.as_ref()
.with_context(err_context)?
.clone(),
initial_pane_title.unwrap_or("".to_owned()),
String::new(),
self.sixel_image_store.clone(),
self.terminal_emulator_colors.clone(),
self.terminal_emulator_color_codes.clone(),
self.link_handler.clone(),
self.character_cell_size.clone(),
self.connected_clients.borrow().iter().copied().collect(),
self.style,
invoked_with,
self.debug,
self.arrow_fonts,
self.styled_underlines,
)) as Box<dyn Pane>
},
};
if start_suppressed {
// this pane needs to start in the background (suppressed), only accessible if a plugin takes it out
// of there in one way or another
// we need to do some bookkeeping for this pane, namely setting its geom and
// content_offset so that things will appear properly in the terminal - we set it to
// the default geom of the first floating pane - this is just in order to give it some
// reasonable size, when it is shown - if needed - it will be given the proper geom as if it were
// resized
let viewport = { self.viewport.borrow().clone() };
let new_pane_geom = half_size_middle_geom(&viewport, 0);
new_pane.set_active_at(Instant::now());
new_pane.set_geom(new_pane_geom);
new_pane.set_content_offset(Offset::frame(1));
resize_pty!(
new_pane,
self.os_api,
self.senders,
self.character_cell_size
)
.with_context(err_context)?;
let is_scrollback_editor = false;
self.suppressed_panes
.insert(pid, (is_scrollback_editor, new_pane));
Ok(())
} else {
self.add_tiled_pane(new_pane, pid, client_id)
}
}
pub fn new_floating_pane(
&mut self,
pid: PaneId,
initial_pane_title: Option<String>,
invoked_with: Option<Run>,
start_suppressed: bool,
should_focus_pane: bool,
floating_pane_coordinates: Option<FloatingPaneCoordinates>,
) -> Result<()> {
let err_context = || format!("failed to create new pane with id {pid:?}");
if should_focus_pane {
self.show_floating_panes();
}
self.close_down_to_max_terminals()
.with_context(err_context)?;
let mut new_pane = match pid {
PaneId::Terminal(term_pid) => {
let next_terminal_position = self.get_next_terminal_position();
Box::new(TerminalPane::new(
term_pid,
PaneGeom::default(), // this will be filled out later
self.style,
next_terminal_position,
String::new(),
self.link_handler.clone(),
self.character_cell_size.clone(),
self.sixel_image_store.clone(),
self.terminal_emulator_colors.clone(),
self.terminal_emulator_color_codes.clone(),
initial_pane_title,
invoked_with,
self.debug,
self.arrow_fonts,
self.styled_underlines,
self.explicitly_disable_kitty_keyboard_protocol,
)) as Box<dyn Pane>
},
PaneId::Plugin(plugin_pid) => {
Box::new(PluginPane::new(
plugin_pid,
PaneGeom::default(), // this will be filled out later
self.senders
.to_plugin
.as_ref()
.with_context(err_context)?
.clone(),
initial_pane_title.unwrap_or("".to_owned()),
String::new(),
self.sixel_image_store.clone(),
self.terminal_emulator_colors.clone(),
self.terminal_emulator_color_codes.clone(),
self.link_handler.clone(),
self.character_cell_size.clone(),
self.connected_clients.borrow().iter().copied().collect(),
self.style,
invoked_with,
self.debug,
self.arrow_fonts,
self.styled_underlines,
)) as Box<dyn Pane>
},
};
if start_suppressed {
// this pane needs to start in the background (suppressed), only accessible if a plugin takes it out
// of there in one way or another
// we need to do some bookkeeping for this pane, namely setting its geom and
// content_offset so that things will appear properly in the terminal - we set it to
// the default geom of the first floating pane - this is just in order to give it some
// reasonable size, when it is shown - if needed - it will be given the proper geom as if it were
// resized
let viewport = { self.viewport.borrow().clone() };
let new_pane_geom = half_size_middle_geom(&viewport, 0);
new_pane.set_active_at(Instant::now());
new_pane.set_geom(new_pane_geom);
new_pane.set_content_offset(Offset::frame(1));
resize_pty!(
new_pane,
self.os_api,
self.senders,
self.character_cell_size
)
.with_context(err_context)?;
let is_scrollback_editor = false;
self.suppressed_panes
.insert(pid, (is_scrollback_editor, new_pane));
Ok(())
} else {
self.add_floating_pane(new_pane, pid, floating_pane_coordinates, should_focus_pane)
}
}
pub fn new_in_place_pane(
&mut self,
pid: PaneId,
initial_pane_title: Option<String>,
invoked_with: Option<Run>,
start_suppressed: bool,
should_focus_pane: bool,
pane_id_to_replace: Option<PaneId>,
close_replaced_pane: bool,
client_id: Option<ClientId>,
) -> Result<()> {
match (pane_id_to_replace, client_id) {
(Some(pane_id_to_replace), _) => {
self.suppress_pane_and_replace_with_pid(
pane_id_to_replace,
pid,
close_replaced_pane,
invoked_with,
)?;
},
(None, Some(client_id)) => match self.get_active_pane_id(client_id) {
Some(active_pane_id) => {
self.suppress_pane_and_replace_with_pid(
active_pane_id,
pid,
close_replaced_pane,
invoked_with,
)?;
},
Some(false) => self.add_tiled_pane(new_pane, pid, client_id),
None => {
if self.floating_panes.panes_are_visible() {
self.add_floating_pane(new_pane, pid, floating_pane_coordinates, false)
} else {
self.add_tiled_pane(new_pane, pid, client_id)
}
log::error!("Cannot find active pane");
},
},
_ => {
log::error!("Must have pane id to replace or client id to start pane in place>");
},
}
if let Some(initial_pane_title) = initial_pane_title {
let _ = self.rename_pane(initial_pane_title.as_bytes().to_vec(), pid);
}
Ok(())
}
pub fn new_stacked_pane(
&mut self,
pid: PaneId,
initial_pane_title: Option<String>,
invoked_with: Option<Run>,
start_suppressed: bool,
should_focus_pane: bool,
pane_id_to_stack_under: Option<PaneId>,
client_id: Option<ClientId>,
) -> Result<()> {
let err_context = || format!("failed to create new pane with id {pid:?}");
if should_focus_pane {
self.hide_floating_panes();
}
self.close_down_to_max_terminals()
.with_context(err_context)?;
let mut new_pane = match pid {
PaneId::Terminal(term_pid) => {
let next_terminal_position = self.get_next_terminal_position();
Box::new(TerminalPane::new(
term_pid,
PaneGeom::default(), // this will be filled out later
self.style,
next_terminal_position,
String::new(),
self.link_handler.clone(),
self.character_cell_size.clone(),
self.sixel_image_store.clone(),
self.terminal_emulator_colors.clone(),
self.terminal_emulator_color_codes.clone(),
initial_pane_title,
invoked_with,
self.debug,
self.arrow_fonts,
self.styled_underlines,
self.explicitly_disable_kitty_keyboard_protocol,
)) as Box<dyn Pane>
},
PaneId::Plugin(plugin_pid) => {
Box::new(PluginPane::new(
plugin_pid,
PaneGeom::default(), // this will be filled out later
self.senders
.to_plugin
.as_ref()
.with_context(err_context)?
.clone(),
initial_pane_title.unwrap_or("".to_owned()),
String::new(),
self.sixel_image_store.clone(),
self.terminal_emulator_colors.clone(),
self.terminal_emulator_color_codes.clone(),
self.link_handler.clone(),
self.character_cell_size.clone(),
self.connected_clients.borrow().iter().copied().collect(),
self.style,
invoked_with,
self.debug,
self.arrow_fonts,
self.styled_underlines,
)) as Box<dyn Pane>
},
};
if start_suppressed {
// this pane needs to start in the background (suppressed), only accessible if a plugin takes it out
// of there in one way or another
// we need to do some bookkeeping for this pane, namely setting its geom and
// content_offset so that things will appear properly in the terminal - we set it to
// the default geom of the first floating pane - this is just in order to give it some
// reasonable size, when it is shown - if needed - it will be given the proper geom as if it were
// resized
let viewport = { self.viewport.borrow().clone() };
let new_pane_geom = half_size_middle_geom(&viewport, 0);
new_pane.set_active_at(Instant::now());
new_pane.set_geom(new_pane_geom);
new_pane.set_content_offset(Offset::frame(1));
resize_pty!(
new_pane,
self.os_api,
self.senders,
self.character_cell_size
)
.with_context(err_context)?;
let is_scrollback_editor = false;
self.suppressed_panes
.insert(pid, (is_scrollback_editor, new_pane));
Ok(())
} else {
if let Some(pane_id_to_stack_under) = pane_id_to_stack_under {
// TODO: also focus pane if should_focus_pane? in cases where we did this from the CLI in an unfocused
// pane...
self.add_stacked_pane_to_pane_id(new_pane, pid, pane_id_to_stack_under)
} else if let Some(client_id) = client_id {
self.add_stacked_pane_to_active_pane(new_pane, pid, client_id)
} else {
log::error!("Must have client id or pane id to stack pane");
return Ok(());
}
}
}
@ -4653,6 +5028,38 @@ impl Tab {
}
Ok(())
}
pub fn add_stacked_pane_to_pane_id(
&mut self,
pane: Box<dyn Pane>,
pane_id: PaneId,
root_pane_id: PaneId,
) -> Result<()> {
if self.tiled_panes.fullscreen_is_active() {
self.tiled_panes.unset_fullscreen();
}
self.tiled_panes
.add_pane_to_stack_of_pane_id(pane_id, pane, root_pane_id);
self.set_should_clear_display_before_rendering();
self.tiled_panes.expand_pane_in_stack(pane_id); // so that it will get focused by all
// clients
self.swap_layouts.set_is_tiled_damaged();
Ok(())
}
pub fn add_stacked_pane_to_active_pane(
&mut self,
pane: Box<dyn Pane>,
pane_id: PaneId,
client_id: ClientId,
) -> Result<()> {
if self.tiled_panes.fullscreen_is_active() {
self.tiled_panes.unset_fullscreen();
}
self.tiled_panes
.add_pane_to_stack_of_active_pane(pane_id, pane, client_id);
self.tiled_panes.focus_pane(pane_id, client_id);
self.swap_layouts.set_is_tiled_damaged();
Ok(())
}
pub fn request_plugin_permissions(&mut self, pid: u32, permissions: Option<PluginPermission>) {
let mut should_focus_pane = false;
if let Some(plugin_pane) = self

View file

@ -0,0 +1,24 @@
---
source: zellij-server/src/tab/./unit/tab_integration_tests.rs
expression: snapshot
---
00 (C): ┌ Pane #1 ──────────────────────────────────────────────────────────────────────────────────────────────────────────────┐
01 (C): ┌ Pane #2 ──────────────────────────────────────────────────────────────────────────────────────────────────────────────┐
02 (C): │ │
03 (C): │ │
04 (C): │ │
05 (C): │ │
06 (C): │ │
07 (C): │ │
08 (C): │ │
09 (C): │ │
10 (C): │ │
11 (C): │ │
12 (C): │ │
13 (C): │ │
14 (C): │ │
15 (C): │ │
16 (C): │ │
17 (C): │ │
18 (C): │ │
19 (C): └───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘

File diff suppressed because it is too large Load diff

View file

@ -1,6 +1,7 @@
use super::Tab;
use crate::pane_groups::PaneGroups;
use crate::panes::sixel::SixelImageStore;
use crate::pty::NewPanePlacement;
use crate::screen::CopyOptions;
use crate::{
os_input_output::{AsyncReader, Pid, ServerOsApi},
@ -625,8 +626,16 @@ fn split_largest_pane() {
let mut tab = create_new_tab(size, stacked_resize);
for i in 2..5 {
let new_pane_id = PaneId::Terminal(i);
tab.new_pane(new_pane_id, None, None, None, None, false, true, Some(1))
.unwrap();
tab.new_pane(
new_pane_id,
None,
None,
false,
true,
NewPanePlacement::default(),
Some(1),
)
.unwrap();
}
assert_eq!(tab.tiled_panes.panes.len(), 4, "The tab has four panes");
@ -838,10 +847,9 @@ pub fn cannot_split_largest_pane_when_there_is_no_room() {
PaneId::Terminal(2),
None,
None,
None,
None,
false,
true,
NewPanePlacement::default(),
Some(1),
)
.unwrap();
@ -888,8 +896,16 @@ pub fn toggle_focused_pane_fullscreen() {
let mut tab = create_new_tab(size, stacked_resize);
for i in 2..5 {
let new_pane_id = PaneId::Terminal(i);
tab.new_pane(new_pane_id, None, None, None, None, false, true, Some(1))
.unwrap();
tab.new_pane(
new_pane_id,
None,
None,
false,
true,
NewPanePlacement::default(),
Some(1),
)
.unwrap();
}
tab.toggle_active_pane_fullscreen(1);
assert_eq!(
@ -964,8 +980,16 @@ pub fn toggle_focused_pane_fullscreen_with_stacked_resizes() {
let mut tab = create_new_tab(size, stacked_resize);
for i in 2..5 {
let new_pane_id = PaneId::Terminal(i);
tab.new_pane(new_pane_id, None, None, None, None, false, true, Some(1))
.unwrap();
tab.new_pane(
new_pane_id,
None,
None,
false,
true,
NewPanePlacement::default(),
Some(1),
)
.unwrap();
}
tab.toggle_active_pane_fullscreen(1);
assert_eq!(
@ -1044,10 +1068,9 @@ fn switch_to_next_pane_fullscreen() {
PaneId::Terminal(1),
None,
None,
None,
None,
false,
true,
NewPanePlacement::default(),
Some(1),
)
.unwrap();
@ -1056,10 +1079,9 @@ fn switch_to_next_pane_fullscreen() {
PaneId::Terminal(2),
None,
None,
None,
None,
false,
true,
NewPanePlacement::default(),
Some(1),
)
.unwrap();
@ -1068,10 +1090,9 @@ fn switch_to_next_pane_fullscreen() {
PaneId::Terminal(3),
None,
None,
None,
None,
false,
true,
NewPanePlacement::default(),
Some(1),
)
.unwrap();
@ -1080,10 +1101,9 @@ fn switch_to_next_pane_fullscreen() {
PaneId::Terminal(4),
None,
None,
None,
None,
false,
true,
NewPanePlacement::default(),
Some(1),
)
.unwrap();
@ -1121,10 +1141,9 @@ fn switch_to_prev_pane_fullscreen() {
PaneId::Terminal(1),
None,
None,
None,
None,
false,
true,
NewPanePlacement::default(),
Some(1),
)
.unwrap();
@ -1133,10 +1152,9 @@ fn switch_to_prev_pane_fullscreen() {
PaneId::Terminal(2),
None,
None,
None,
None,
false,
true,
NewPanePlacement::default(),
Some(1),
)
.unwrap();
@ -1145,10 +1163,9 @@ fn switch_to_prev_pane_fullscreen() {
PaneId::Terminal(3),
None,
None,
None,
None,
false,
true,
NewPanePlacement::default(),
Some(1),
)
.unwrap();
@ -1157,10 +1174,9 @@ fn switch_to_prev_pane_fullscreen() {
PaneId::Terminal(4),
None,
None,
None,
None,
false,
true,
NewPanePlacement::default(),
Some(1),
)
.unwrap();
@ -14758,10 +14774,9 @@ fn correctly_resize_frameless_panes_on_pane_close() {
PaneId::Terminal(2),
None,
None,
None,
None,
false,
true,
NewPanePlacement::default(),
Some(1),
)
.unwrap();

View file

@ -34,7 +34,7 @@ use std::sync::{Arc, Mutex};
use crate::{
plugins::PluginInstruction,
pty::{ClientTabIndexOrPaneId, PtyInstruction},
pty::{ClientTabIndexOrPaneId, NewPanePlacement, PtyInstruction},
};
use zellij_utils::ipc::PixelDimensions;
@ -1235,10 +1235,9 @@ fn switch_to_tab_with_fullscreen() {
PaneId::Terminal(2),
None,
None,
None,
None,
false,
true,
NewPanePlacement::default(),
Some(1),
)
.unwrap();
@ -1359,10 +1358,9 @@ fn attach_after_first_tab_closed() {
PaneId::Terminal(2),
None,
None,
None,
None,
false,
true,
NewPanePlacement::default(),
Some(1),
)
.unwrap();
@ -1385,22 +1383,20 @@ fn open_new_floating_pane_with_custom_coordinates() {
new_tab(&mut screen, 1, 0);
let active_tab = screen.get_active_tab_mut(1).unwrap();
let should_float = Some(true);
active_tab
.new_pane(
PaneId::Terminal(2),
None,
should_float,
None,
Some(FloatingPaneCoordinates {
false,
true,
NewPanePlacement::Floating(Some(FloatingPaneCoordinates {
x: Some(SplitSize::Percent(10)),
y: Some(SplitSize::Fixed(5)),
width: Some(SplitSize::Percent(1)),
height: Some(SplitSize::Fixed(2)),
pinned: None,
}),
false,
true,
})),
Some(1),
)
.unwrap();
@ -1421,22 +1417,20 @@ fn open_new_floating_pane_with_custom_coordinates_exceeding_viewport() {
new_tab(&mut screen, 1, 0);
let active_tab = screen.get_active_tab_mut(1).unwrap();
let should_float = Some(true);
active_tab
.new_pane(
PaneId::Terminal(2),
None,
should_float,
None,
Some(FloatingPaneCoordinates {
false,
true,
NewPanePlacement::Floating(Some(FloatingPaneCoordinates {
x: Some(SplitSize::Fixed(122)),
y: Some(SplitSize::Fixed(21)),
width: Some(SplitSize::Fixed(10)),
height: Some(SplitSize::Fixed(10)),
pinned: None,
}),
false,
true,
})),
Some(1),
)
.unwrap();
@ -1611,17 +1605,15 @@ fn group_panes_following_focus() {
{
let active_tab = screen.get_active_tab_mut(client_id).unwrap();
let should_float = Some(false);
for i in 2..5 {
active_tab
.new_pane(
PaneId::Terminal(i),
None,
should_float,
None,
None,
false,
true,
NewPanePlacement::Tiled(None),
Some(client_id),
)
.unwrap();
@ -1670,17 +1662,15 @@ fn break_group_with_mouse() {
{
let active_tab = screen.get_active_tab_mut(client_id).unwrap();
let should_float = Some(false);
for i in 2..5 {
active_tab
.new_pane(
PaneId::Terminal(i),
None,
should_float,
None,
None,
false,
true,
NewPanePlacement::Tiled(None),
Some(client_id),
)
.unwrap();
@ -2596,6 +2586,7 @@ pub fn send_cli_new_pane_action_with_default_parameters() {
width: None,
height: None,
pinned: None,
stacked: false,
};
send_cli_action_to_server(&session_metadata, cli_new_pane_action, client_id);
std::thread::sleep(std::time::Duration::from_millis(100)); // give time for actions to be
@ -2640,6 +2631,7 @@ pub fn send_cli_new_pane_action_with_split_direction() {
width: None,
height: None,
pinned: None,
stacked: false,
};
send_cli_action_to_server(&session_metadata, cli_new_pane_action, client_id);
std::thread::sleep(std::time::Duration::from_millis(100)); // give time for actions to be
@ -2684,6 +2676,7 @@ pub fn send_cli_new_pane_action_with_command_and_cwd() {
width: None,
height: None,
pinned: None,
stacked: false,
};
send_cli_action_to_server(&session_metadata, cli_new_pane_action, client_id);
std::thread::sleep(std::time::Duration::from_millis(100)); // give time for actions to be
@ -2694,7 +2687,7 @@ pub fn send_cli_new_pane_action_with_command_and_cwd() {
.unwrap()
.iter()
.find(|instruction| match instruction {
PtyInstruction::SpawnTerminalVertically(..) => true,
PtyInstruction::SpawnTerminal(..) => true,
_ => false,
})
.cloned();
@ -2739,6 +2732,7 @@ pub fn send_cli_new_pane_action_with_floating_pane_and_coordinates() {
width: Some("20%".to_owned()),
height: None,
pinned: None,
stacked: false,
};
send_cli_action_to_server(&session_metadata, cli_new_pane_action, client_id);
std::thread::sleep(std::time::Duration::from_millis(100)); // give time for actions to be

View file

@ -2,4 +2,4 @@
source: zellij-server/src/./unit/screen_tests.rs
expression: "format!(\"{:?}\", *received_pty_instructions.lock().unwrap())"
---
[UpdateActivePane(Some(Terminal(0)), 1), UpdateActivePane(Some(Terminal(0)), 1), SpawnTerminal(Some(OpenFile(OpenFilePayload { path: "/file/to/edit", line_number: None, cwd: Some("."), originating_plugin: None })), Some(false), Some("Editing: /file/to/edit"), None, false, ClientId(10)), Exit]
[UpdateActivePane(Some(Terminal(0)), 1), UpdateActivePane(Some(Terminal(0)), 1), SpawnTerminal(Some(OpenFile(OpenFilePayload { path: "/file/to/edit", line_number: None, cwd: Some("."), originating_plugin: None })), Some("Editing: /file/to/edit"), Tiled(None), false, ClientId(10)), Exit]

View file

@ -2,4 +2,4 @@
source: zellij-server/src/./unit/screen_tests.rs
expression: "format!(\"{:?}\", *received_pty_instructions.lock().unwrap())"
---
[UpdateActivePane(Some(Terminal(0)), 1), UpdateActivePane(Some(Terminal(0)), 1), SpawnTerminal(Some(OpenFile(OpenFilePayload { path: "/file/to/edit", line_number: Some(100), cwd: Some("."), originating_plugin: None })), Some(false), Some("Editing: /file/to/edit"), None, false, ClientId(10)), Exit]
[UpdateActivePane(Some(Terminal(0)), 1), UpdateActivePane(Some(Terminal(0)), 1), SpawnTerminal(Some(OpenFile(OpenFilePayload { path: "/file/to/edit", line_number: Some(100), cwd: Some("."), originating_plugin: None })), Some("Editing: /file/to/edit"), Tiled(None), false, ClientId(10)), Exit]

View file

@ -2,4 +2,4 @@
source: zellij-server/src/./unit/screen_tests.rs
expression: "format!(\"{:?}\", *received_pty_instructions.lock().unwrap())"
---
[UpdateActivePane(Some(Terminal(0)), 1), UpdateActivePane(Some(Terminal(0)), 1), SpawnTerminalHorizontally(Some(OpenFile(OpenFilePayload { path: "/file/to/edit", line_number: None, cwd: Some("."), originating_plugin: None })), Some("Editing: /file/to/edit"), 10), Exit]
[UpdateActivePane(Some(Terminal(0)), 1), UpdateActivePane(Some(Terminal(0)), 1), SpawnTerminal(Some(OpenFile(OpenFilePayload { path: "/file/to/edit", line_number: None, cwd: Some("."), originating_plugin: None })), Some("Editing: /file/to/edit"), Tiled(Some(Down)), false, ClientId(10)), Exit]

View file

@ -1,6 +1,5 @@
---
source: zellij-server/src/./unit/screen_tests.rs
assertion_line: 2339
expression: "format!(\"{:?}\", new_pane_instruction)"
---
Some(SpawnTerminalVertically(Some(RunCommand(RunCommand { command: "htop", args: [], cwd: Some("/some/folder"), hold_on_close: true, hold_on_start: false, originating_plugin: None })), None, 10))
Some(SpawnTerminal(Some(RunCommand(RunCommand { command: "htop", args: [], cwd: Some("/some/folder"), hold_on_close: true, hold_on_start: false, originating_plugin: None })), None, Tiled(Some(Right)), false, ClientId(10)))

View file

@ -2,4 +2,4 @@
source: zellij-server/src/./unit/screen_tests.rs
expression: "format!(\"{:?}\", *received_pty_instructions.lock().unwrap())"
---
[UpdateActivePane(Some(Terminal(0)), 1), UpdateActivePane(Some(Terminal(0)), 1), SpawnTerminal(None, Some(false), None, None, false, ClientId(10)), Exit]
[UpdateActivePane(Some(Terminal(0)), 1), UpdateActivePane(Some(Terminal(0)), 1), SpawnTerminal(None, None, Tiled(None), false, ClientId(10)), Exit]

View file

@ -2,4 +2,4 @@
source: zellij-server/src/./unit/screen_tests.rs
expression: "format!(\"{:?}\", *received_pty_instructions.lock().unwrap())"
---
[UpdateActivePane(Some(Terminal(0)), 1), UpdateActivePane(Some(Terminal(0)), 1), SpawnTerminal(Some(RunCommand(RunCommand { command: "htop", args: [], cwd: Some("/some/folder"), hold_on_close: true, hold_on_start: false, originating_plugin: None })), Some(true), None, Some(FloatingPaneCoordinates { x: Some(Fixed(10)), y: None, width: Some(Percent(20)), height: None, pinned: None }), false, ClientId(10)), Exit]
[UpdateActivePane(Some(Terminal(0)), 1), UpdateActivePane(Some(Terminal(0)), 1), SpawnTerminal(Some(RunCommand(RunCommand { command: "htop", args: [], cwd: Some("/some/folder"), hold_on_close: true, hold_on_start: false, originating_plugin: None })), None, Floating(Some(FloatingPaneCoordinates { x: Some(Fixed(10)), y: None, width: Some(Percent(20)), height: None, pinned: None })), false, ClientId(10)), Exit]

View file

@ -2,4 +2,4 @@
source: zellij-server/src/./unit/screen_tests.rs
expression: "format!(\"{:?}\", *received_pty_instructions.lock().unwrap())"
---
[UpdateActivePane(Some(Terminal(0)), 1), UpdateActivePane(Some(Terminal(0)), 1), SpawnTerminalVertically(None, None, 10), Exit]
[UpdateActivePane(Some(Terminal(0)), 1), UpdateActivePane(Some(Terminal(0)), 1), SpawnTerminal(None, None, Tiled(Some(Right)), false, ClientId(10)), Exit]

View file

@ -30,6 +30,7 @@ keybinds {
bind "n" { NewPane; SwitchToMode "Normal"; }
bind "d" { NewPane "Down"; SwitchToMode "Normal"; }
bind "r" { NewPane "Right"; SwitchToMode "Normal"; }
bind "s" { NewPane "stacked"; SwitchToMode "Normal"; }
bind "x" { CloseFocus; SwitchToMode "Normal"; }
bind "f" { ToggleFocusFullscreen; SwitchToMode "Normal"; }
bind "z" { TogglePaneFrames; SwitchToMode "Normal"; }

View file

@ -475,6 +475,7 @@ pub enum ActionName {
MouseEvent = 86,
TogglePaneInGroup = 87,
ToggleGroupMarking = 88,
NewStackedPane = 89,
}
impl ActionName {
/// String value of the enum field names used in the ProtoBuf definition.
@ -569,6 +570,7 @@ impl ActionName {
ActionName::MouseEvent => "MouseEvent",
ActionName::TogglePaneInGroup => "TogglePaneInGroup",
ActionName::ToggleGroupMarking => "ToggleGroupMarking",
ActionName::NewStackedPane => "NewStackedPane",
}
}
/// Creates an enum from field names used in the ProtoBuf definition.
@ -660,6 +662,7 @@ impl ActionName {
"MouseEvent" => Some(Self::MouseEvent),
"TogglePaneInGroup" => Some(Self::TogglePaneInGroup),
"ToggleGroupMarking" => Some(Self::ToggleGroupMarking),
"NewStackedPane" => Some(Self::NewStackedPane),
_ => None,
}
}

View file

@ -313,6 +313,15 @@ pub enum Sessions {
/// Whether to pin a floating pane so that it is always on top
#[clap(long, requires("floating"))]
pinned: Option<bool>,
#[clap(
long,
conflicts_with("floating"),
conflicts_with("direction"),
value_parser,
default_value("false"),
takes_value(false)
)]
stacked: bool,
},
/// Load a plugin
#[clap(visible_alias = "p")]
@ -602,6 +611,15 @@ pub enum CliAction {
/// Whether to pin a floating pane so that it is always on top
#[clap(long, requires("floating"))]
pinned: Option<bool>,
#[clap(
long,
conflicts_with("floating"),
conflicts_with("direction"),
value_parser,
default_value("false"),
takes_value(false)
)]
stacked: bool,
},
/// Open the specified file in a new zellij pane with your default EDITOR
Edit {

View file

@ -180,6 +180,8 @@ pub enum Action {
/// Open a new pane in place of the focused one, suppressing it instead
NewInPlacePane(Option<RunCommandAction>, Option<String>), // String is an
// optional pane
NewStackedPane(Option<RunCommandAction>, Option<String>), // String is an
// optional pane
// name
/// Embed focused pane in tab if floating or float focused pane if embedded
TogglePaneEmbedOrFloating,
@ -362,6 +364,7 @@ impl Action {
width,
height,
pinned,
stacked,
} => {
let current_dir = get_current_dir();
// cwd should only be specified in a plugin alias if it was explicitly given to us,
@ -443,6 +446,8 @@ impl Action {
)])
} else if in_place {
Ok(vec![Action::NewInPlacePane(Some(run_command_action), name)])
} else if stacked {
Ok(vec![Action::NewStackedPane(Some(run_command_action), name)])
} else {
Ok(vec![Action::NewTiledPane(
direction,
@ -459,6 +464,8 @@ impl Action {
)])
} else if in_place {
Ok(vec![Action::NewInPlacePane(None, name)])
} else if stacked {
Ok(vec![Action::NewStackedPane(None, name)])
} else {
Ok(vec![Action::NewTiledPane(direction, None, name)])
}

View file

@ -534,6 +534,8 @@ impl Action {
"NewPane" => {
if string.is_empty() {
return Ok(Action::NewPane(None, None, false));
} else if string == "stacked" {
return Ok(Action::NewStackedPane(None, None));
} else {
let direction = Direction::from_str(string.as_str()).map_err(|_| {
ConfigError::new_kdl_error(
@ -911,6 +913,48 @@ impl Action {
}
Some(node)
},
Action::NewStackedPane(run_command_action, name) => match run_command_action {
Some(run_command_action) => {
let mut node = KdlNode::new("Run");
let mut node_children = KdlDocument::new();
node.push(run_command_action.command.display().to_string());
for arg in &run_command_action.args {
node.push(arg.clone());
}
let mut stacked_node = KdlNode::new("stacked");
stacked_node.push(KdlValue::Bool(true));
node_children.nodes_mut().push(stacked_node);
if let Some(cwd) = &run_command_action.cwd {
let mut cwd_node = KdlNode::new("cwd");
cwd_node.push(cwd.display().to_string());
node_children.nodes_mut().push(cwd_node);
}
if run_command_action.hold_on_start {
let mut hos_node = KdlNode::new("hold_on_start");
hos_node.push(KdlValue::Bool(true));
node_children.nodes_mut().push(hos_node);
}
if !run_command_action.hold_on_close {
let mut hoc_node = KdlNode::new("hold_on_close");
hoc_node.push(KdlValue::Bool(false));
node_children.nodes_mut().push(hoc_node);
}
if let Some(name) = name {
let mut name_node = KdlNode::new("name");
name_node.push(name.clone());
node_children.nodes_mut().push(name_node);
}
if !node_children.nodes().is_empty() {
node.set_children(node_children);
}
Some(node)
},
None => {
let mut node = KdlNode::new("NewPane");
node.push("stacked");
Some(node)
},
},
Action::Detach => Some(KdlNode::new("Detach")),
Action::LaunchOrFocusPlugin(
run_plugin_or_alias,
@ -1557,6 +1601,9 @@ impl TryFrom<(&KdlNode, &Options)> for Action {
let in_place = command_metadata
.and_then(|c_m| kdl_child_bool_value_for_entry(c_m, "in_place"))
.unwrap_or(false);
let stacked = command_metadata
.and_then(|c_m| kdl_child_bool_value_for_entry(c_m, "stacked"))
.unwrap_or(false);
let run_command_action = RunCommandAction {
command: PathBuf::from(command),
args,
@ -1588,6 +1635,8 @@ impl TryFrom<(&KdlNode, &Options)> for Action {
))
} else if in_place {
Ok(Action::NewInPlacePane(Some(run_command_action), name))
} else if stacked {
Ok(Action::NewStackedPane(Some(run_command_action), name))
} else {
Ok(Action::NewTiledPane(
direction,

View file

@ -24,6 +24,7 @@ keybinds clear-defaults=true {
bind "p" { SwitchFocus; }
bind "Ctrl p" { SwitchToMode "normal"; }
bind "r" { NewPane "right"; SwitchToMode "normal"; }
bind "s" { NewPane "stacked"; SwitchToMode "normal"; }
bind "w" { ToggleFloatingPanes; SwitchToMode "normal"; }
bind "z" { TogglePaneFrames; SwitchToMode "normal"; }
}

View file

@ -24,6 +24,7 @@ keybinds clear-defaults=true {
bind "p" { SwitchFocus; }
bind "Ctrl p" { SwitchToMode "normal"; }
bind "r" { NewPane "right"; SwitchToMode "normal"; }
bind "s" { NewPane "stacked"; SwitchToMode "normal"; }
bind "w" { ToggleFloatingPanes; SwitchToMode "normal"; }
bind "z" { TogglePaneFrames; SwitchToMode "normal"; }
}

View file

@ -243,6 +243,7 @@ enum ActionName {
MouseEvent = 86;
TogglePaneInGroup = 87;
ToggleGroupMarking = 88;
NewStackedPane = 89;
}
message Position {

View file

@ -720,6 +720,10 @@ impl TryFrom<ProtobufAction> for Action {
plugin_id: None,
}),
},
Some(ProtobufActionName::NewStackedPane) => match protobuf_action.optional_payload {
Some(_) => Err("NewStackedPane should not have a payload"),
None => Ok(Action::NewStackedPane(None, None)),
},
_ => Err("Unknown Action"),
}
}
@ -1254,6 +1258,10 @@ impl TryFrom<Action> for ProtobufAction {
name: ProtobufActionName::ToggleGroupMarking as i32,
optional_payload: None,
}),
Action::NewStackedPane(..) => Ok(ProtobufAction {
name: ProtobufActionName::NewStackedPane as i32,
optional_payload: None,
}),
Action::NoOp
| Action::Confirm
| Action::NewInPlacePane(..)

View file

@ -1339,6 +1339,20 @@ Config {
Normal,
),
],
KeyWithModifier {
bare_key: Char(
's',
),
key_modifiers: {},
}: [
NewStackedPane(
None,
None,
),
SwitchToMode(
Normal,
),
],
KeyWithModifier {
bare_key: Char(
's',

View file

@ -1339,6 +1339,20 @@ Config {
Normal,
),
],
KeyWithModifier {
bare_key: Char(
's',
),
key_modifiers: {},
}: [
NewStackedPane(
None,
None,
),
SwitchToMode(
Normal,
),
],
KeyWithModifier {
bare_key: Char(
's',

View file

@ -1339,6 +1339,20 @@ Config {
Normal,
),
],
KeyWithModifier {
bare_key: Char(
's',
),
key_modifiers: {},
}: [
NewStackedPane(
None,
None,
),
SwitchToMode(
Normal,
),
],
KeyWithModifier {
bare_key: Char(
's',

View file

@ -1339,6 +1339,20 @@ Config {
Normal,
),
],
KeyWithModifier {
bare_key: Char(
's',
),
key_modifiers: {},
}: [
NewStackedPane(
None,
None,
),
SwitchToMode(
Normal,
),
],
KeyWithModifier {
bare_key: Char(
's',