mirror of
https://github.com/zellij-org/zellij.git
synced 2026-10-12 02:27:24 +00:00
Revamp the sequence plugin (#4713)
* initial pipe interface * initial open_x_pane_in_new_tab plugin api implementation * fix: send CommandPaneExited for command panes opened as initial_terminals in new tabs from plugins * open panes in place of specific pane id plugin commands - initial implementation * sequence and plugin api ux * focus self when starting * combine ui and pipe paths * highlight selected pane * initial sequence removal of editing capabilities * fix: translate plugin paths in api * properly trim commands when loaded from pipe or pasted text * minor ux fixes * get ctrl c to work properly * behavior fixes * more ui fixes * fix tests * close tab and end sequence properly * default to AND * zellij sequence alias * add alias * consolidate paths * rustfmt * fix ui padding * add pr * fix: reset scrolling when resetting terminal state
This commit is contained in:
+1
-1
@@ -16,7 +16,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/)
|
||||
* infra: migrate wasm runtime from wasmtime to wasmi (https://github.com/zellij-org/zellij/pull/4449)
|
||||
* feat: allow attaching to sessions as "read-only" (https://github.com/zellij-org/zellij/pull/4487)
|
||||
* feat: improve cli interface (add detach, switch-session and more) (https://github.com/zellij-org/zellij/pull/4492)
|
||||
* feat: command sequences, conditionally blocking CLI commands, new plugin APIs (https://github.com/zellij-org/zellij/pull/4546)
|
||||
* feat: command sequences, conditionally blocking CLI commands, new plugin APIs (https://github.com/zellij-org/zellij/pull/4546 and https://github.com/zellij-org/zellij/pull/4713)
|
||||
* feat: read-only web authentication-tokens (https://github.com/zellij-org/zellij/pull/4551)
|
||||
* feat: allow overriding layouts at runtime (https://github.com/zellij-org/zellij/pull/4566)
|
||||
* build: Update Rust toolchain to 1.92.0 (https://github.com/zellij-org/zellij/pull/4579)
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,6 +1,5 @@
|
||||
use super::{ChainType, CommandStatus};
|
||||
use std::path::PathBuf;
|
||||
use std::time::Instant;
|
||||
use zellij_tile::prelude::PaneId;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
@@ -9,7 +8,6 @@ pub struct CommandEntry {
|
||||
cwd: Option<PathBuf>,
|
||||
pub(super) chain_type: ChainType,
|
||||
pub(super) status: CommandStatus,
|
||||
pub(super) start_time: std::time::Instant,
|
||||
}
|
||||
|
||||
impl Default for CommandEntry {
|
||||
@@ -19,7 +17,6 @@ impl Default for CommandEntry {
|
||||
cwd: None,
|
||||
chain_type: ChainType::default(),
|
||||
status: CommandStatus::default(),
|
||||
start_time: Instant::now(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -32,10 +29,6 @@ impl CommandEntry {
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
pub fn with_and(mut self) -> Self {
|
||||
self.chain_type = ChainType::And;
|
||||
self
|
||||
}
|
||||
pub fn get_text(&self) -> String {
|
||||
self.text.clone()
|
||||
}
|
||||
@@ -74,9 +67,6 @@ impl CommandEntry {
|
||||
pub fn clear_status(&mut self) {
|
||||
self.status = CommandStatus::Pending;
|
||||
}
|
||||
pub fn cycle_chain_type(&mut self) {
|
||||
self.chain_type.cycle_next();
|
||||
}
|
||||
pub fn get_cwd(&self) -> Option<PathBuf> {
|
||||
self.cwd.clone()
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use super::ChainType;
|
||||
use super::{ChainType, CommandEntry};
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||
enum ParseState {
|
||||
@@ -104,50 +104,6 @@ pub fn split_by_chain_operators(text: &str) -> Vec<(String, Option<ChainType>)>
|
||||
segments
|
||||
}
|
||||
|
||||
pub fn detect_chain_operator_at_end(text: &str) -> Option<(String, ChainType)> {
|
||||
let segments = split_by_chain_operators(text);
|
||||
|
||||
if segments.is_empty() {
|
||||
return None;
|
||||
}
|
||||
|
||||
if segments.len() == 1 {
|
||||
if let Some((text, Some(chain_type))) = segments.first() {
|
||||
return Some((text.clone(), *chain_type));
|
||||
}
|
||||
return None;
|
||||
}
|
||||
|
||||
if let Some((first_text, Some(chain_type))) = segments.first() {
|
||||
return Some((first_text.clone(), *chain_type));
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
pub fn get_remaining_after_first_segment(text: &str) -> Option<String> {
|
||||
let segments = split_by_chain_operators(text);
|
||||
|
||||
if segments.len() <= 1 {
|
||||
return None;
|
||||
}
|
||||
|
||||
let mut result = String::new();
|
||||
for (i, (segment_text, chain_type_opt)) in segments.iter().enumerate().skip(1) {
|
||||
if i > 1 {
|
||||
if let Some(chain_type) = chain_type_opt {
|
||||
result.push_str(&format!(" {} ", chain_type.as_str()));
|
||||
}
|
||||
}
|
||||
result.push_str(segment_text);
|
||||
if let Some(chain_type) = chain_type_opt {
|
||||
result.push_str(&format!(" {} ", chain_type.as_str()));
|
||||
}
|
||||
}
|
||||
|
||||
Some(result.trim().to_string())
|
||||
}
|
||||
|
||||
pub fn detect_cd_command(text: &str) -> Option<String> {
|
||||
let trimmed = text.trim();
|
||||
|
||||
@@ -163,6 +119,92 @@ pub fn detect_cd_command(text: &str) -> Option<String> {
|
||||
None
|
||||
}
|
||||
|
||||
/// Serialize a sequence of commands to editor format.
|
||||
/// CWD changes are emitted as `cd <path>` lines; chain type as trailing ` &&`, ` ||`, ` ;`.
|
||||
pub fn serialize_sequence_to_editor(commands: &[CommandEntry]) -> String {
|
||||
let mut result = String::new();
|
||||
|
||||
result.push_str("# One cmd per line. Use && || ; to chain. cd <path> sets cwd.\n");
|
||||
result.push_str("\n");
|
||||
|
||||
let mut prev_cwd: Option<std::path::PathBuf> = None;
|
||||
|
||||
for cmd in commands {
|
||||
if cmd.get_text().trim().is_empty() {
|
||||
continue;
|
||||
}
|
||||
|
||||
let cmd_cwd = cmd.get_cwd();
|
||||
if cmd_cwd != prev_cwd && cmd_cwd.is_some() {
|
||||
let path = cmd_cwd.as_ref().unwrap();
|
||||
result.push_str(&format!("cd {}\n", path.display()));
|
||||
}
|
||||
prev_cwd = cmd_cwd;
|
||||
|
||||
let op_suffix = match cmd.get_chain_type() {
|
||||
ChainType::And => " &&",
|
||||
ChainType::Or => " ||",
|
||||
ChainType::Then => " ;",
|
||||
ChainType::None => "",
|
||||
};
|
||||
|
||||
result.push_str(&format!("{}{}\n", cmd.get_text(), op_suffix));
|
||||
}
|
||||
|
||||
result
|
||||
}
|
||||
|
||||
/// Parse command text into a Vec<CommandEntry>.
|
||||
/// `cd <path>` lines set the cwd for subsequent commands.
|
||||
/// Blank lines and `#` comment lines are ignored.
|
||||
pub fn parse_commands(
|
||||
contents: &str,
|
||||
initial_cwd: Option<std::path::PathBuf>,
|
||||
) -> Vec<CommandEntry> {
|
||||
use crate::path_formatting;
|
||||
let mut commands = Vec::new();
|
||||
let mut current_cwd = initial_cwd;
|
||||
|
||||
for line in contents.lines() {
|
||||
let trimmed = line.trim();
|
||||
if trimmed.is_empty() || trimmed.starts_with('#') {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Split by chain operators first so that "cd /path &&" doesn't absorb the operator
|
||||
// into the path. Each resulting segment is then checked for cd independently.
|
||||
let segments = split_by_chain_operators(trimmed);
|
||||
for (text, chain_type_opt) in segments {
|
||||
let text = text.trim().to_string();
|
||||
if text.is_empty() {
|
||||
continue;
|
||||
}
|
||||
if let Some(path) = detect_cd_command(&text) {
|
||||
if let Some(new_cwd) = path_formatting::resolve_path(current_cwd.as_ref(), &path) {
|
||||
current_cwd = Some(new_cwd);
|
||||
}
|
||||
// cd segments are not added as command entries; their chain type is discarded
|
||||
} else {
|
||||
let mut entry = CommandEntry::new(&text, current_cwd.clone());
|
||||
if let Some(chain_type) = chain_type_opt {
|
||||
entry.set_chain_type(chain_type);
|
||||
}
|
||||
commands.push(entry);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Commands with no explicit operator default to &&
|
||||
if !commands.is_empty() {
|
||||
let last_idx = commands.len() - 1;
|
||||
for cmd in &mut commands[..last_idx] {
|
||||
cmd.fill_chain_type_if_empty();
|
||||
}
|
||||
}
|
||||
|
||||
commands
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
@@ -277,36 +319,6 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_detect_operator_just_typed_and() {
|
||||
let result = detect_chain_operator_at_end("ls &&");
|
||||
assert_eq!(result, Some(("ls".to_string(), ChainType::And)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_detect_operator_just_typed_or() {
|
||||
let result = detect_chain_operator_at_end("cmd ||");
|
||||
assert_eq!(result, Some(("cmd".to_string(), ChainType::Or)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_detect_operator_just_typed_semicolon() {
|
||||
let result = detect_chain_operator_at_end("echo ;");
|
||||
assert_eq!(result, Some(("echo".to_string(), ChainType::Then)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_detect_operator_no_operator() {
|
||||
let result = detect_chain_operator_at_end("ls");
|
||||
assert_eq!(result, None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_detect_operator_in_quotes() {
|
||||
let result = detect_chain_operator_at_end("echo '&&'");
|
||||
assert_eq!(result, None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_whitespace_trimming() {
|
||||
let result = split_by_chain_operators(" cmd1 && cmd2 ");
|
||||
@@ -324,4 +336,35 @@ mod tests {
|
||||
let result = split_by_chain_operators("ls && ");
|
||||
assert_eq!(result, vec![("ls".to_string(), Some(ChainType::And))]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_editor_file_no_operator_defaults_to_and() {
|
||||
let contents = "cmd1\ncmd2\ncmd3";
|
||||
let result = parse_commands(contents, None);
|
||||
assert_eq!(result.len(), 3);
|
||||
assert_eq!(result[0].get_chain_type(), ChainType::And);
|
||||
assert_eq!(result[1].get_chain_type(), ChainType::And);
|
||||
assert_eq!(result[2].get_chain_type(), ChainType::None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_editor_file_explicit_operator_preserved() {
|
||||
let contents = "cmd1 ||\ncmd2";
|
||||
let result = parse_commands(contents, None);
|
||||
assert_eq!(result.len(), 2);
|
||||
assert_eq!(result[0].get_chain_type(), ChainType::Or);
|
||||
assert_eq!(result[1].get_chain_type(), ChainType::None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_cd_with_chain_operator_does_not_absorb_operator_into_path() {
|
||||
use super::super::CommandEntry;
|
||||
use std::path::PathBuf;
|
||||
|
||||
let contents = "cd /some/path && cargo build";
|
||||
let result = parse_commands(contents, None);
|
||||
assert_eq!(result.len(), 1);
|
||||
assert_eq!(result[0].get_text(), "cargo build");
|
||||
assert_eq!(result[0].get_cwd(), Some(PathBuf::from("/some/path")));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,80 +0,0 @@
|
||||
use crate::path_formatting;
|
||||
use crate::state::CommandEntry;
|
||||
use crate::ui::text_input::TextInput;
|
||||
use std::path::PathBuf;
|
||||
|
||||
pub struct Editing {
|
||||
pub editing_input: Option<TextInput>,
|
||||
}
|
||||
|
||||
impl Editing {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
editing_input: Some(TextInput::new("".to_owned())),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn start_editing(&mut self, text: String) {
|
||||
self.editing_input = Some(TextInput::new(text));
|
||||
}
|
||||
|
||||
pub fn cancel_editing(&mut self) {
|
||||
self.editing_input = None;
|
||||
}
|
||||
|
||||
pub fn input_text(&self) -> Option<String> {
|
||||
self.editing_input.as_ref().map(|e| e.get_text().to_owned())
|
||||
}
|
||||
|
||||
pub fn set_input_text(&mut self, text: String) {
|
||||
self.editing_input.as_mut().map(|e| e.set_text(text));
|
||||
}
|
||||
|
||||
pub fn handle_submit(
|
||||
&mut self,
|
||||
selection_index: Option<usize>,
|
||||
all_commands: &mut Vec<CommandEntry>,
|
||||
current_cwd: &Option<PathBuf>,
|
||||
) -> (bool, Option<usize>) {
|
||||
let mut handled_internally = false;
|
||||
let mut new_selection_index = None;
|
||||
|
||||
if let Some(current_text) = self.editing_input.as_ref().map(|i| i.get_text()) {
|
||||
if let Some(index) = selection_index {
|
||||
if let Some(command) = all_commands.get_mut(index) {
|
||||
if current_text.starts_with("cd ") || current_text == "cd" {
|
||||
let path = if current_text == "cd" {
|
||||
"~"
|
||||
} else {
|
||||
current_text[3..].trim()
|
||||
};
|
||||
|
||||
if let Some(new_cwd) =
|
||||
path_formatting::resolve_path(current_cwd.as_ref(), path)
|
||||
{
|
||||
command.set_cwd(Some(new_cwd));
|
||||
}
|
||||
|
||||
command.set_text("".to_owned());
|
||||
self.editing_input.as_mut().map(|c| c.clear());
|
||||
new_selection_index = selection_index; // remain editing after a cd command
|
||||
handled_internally = true;
|
||||
return (handled_internally, new_selection_index); // avoid setting edit_input to None below, we
|
||||
// still want to be in editing mode after
|
||||
// changing directory with cd
|
||||
} else {
|
||||
command.set_text(current_text.to_owned());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
self.editing_input = None;
|
||||
(handled_internally, new_selection_index)
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for Editing {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
@@ -1,16 +1,13 @@
|
||||
use crate::path_formatting::format_cwd;
|
||||
use crate::state::{ChainType, CommandEntry, CommandStatus};
|
||||
use std::collections::BTreeMap;
|
||||
use crate::state::{CommandEntry, CommandStatus};
|
||||
use std::path::PathBuf;
|
||||
use std::time::Duration;
|
||||
use zellij_tile::prelude::*;
|
||||
|
||||
pub struct Execution {
|
||||
pub all_commands: Vec<CommandEntry>,
|
||||
pub current_running_command_index: usize,
|
||||
pub is_running: bool,
|
||||
pub sequence_id: u64,
|
||||
pub primary_pane_id_before_sequence: Option<PaneId>,
|
||||
pub displayed_pane_id: Option<PaneId>,
|
||||
}
|
||||
|
||||
impl Execution {
|
||||
@@ -19,8 +16,7 @@ impl Execution {
|
||||
all_commands: vec![CommandEntry::default()],
|
||||
current_running_command_index: 0,
|
||||
is_running: false,
|
||||
sequence_id: 0,
|
||||
primary_pane_id_before_sequence: None,
|
||||
displayed_pane_id: None,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -82,181 +78,6 @@ impl Execution {
|
||||
copy_to_clipboard(text_to_copy);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn update_pane_id_for_command(&mut self, pane_id: PaneId, command_text: &str) {
|
||||
for command in self.all_commands.iter_mut() {
|
||||
if let CommandStatus::Pending | CommandStatus::Running(None) = command.get_status() {
|
||||
let cmd_text = command.get_text();
|
||||
if cmd_text == command_text {
|
||||
command.set_status(CommandStatus::Running(Some(pane_id)));
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn update_exited_command_statuses(&mut self, pane_manifest: &PaneManifest) -> bool {
|
||||
let mut updated = false;
|
||||
for command in self.all_commands.iter_mut() {
|
||||
let status = command.get_status();
|
||||
let pane_id_opt = match &status {
|
||||
CommandStatus::Running(pid) => *pid,
|
||||
CommandStatus::Exited(_, pid) => *pid,
|
||||
CommandStatus::Interrupted(pid) => *pid,
|
||||
_ => None,
|
||||
};
|
||||
|
||||
if let Some(pane_id) = pane_id_opt {
|
||||
let mut pane_found = false;
|
||||
for (_tab_index, panes) in &pane_manifest.panes {
|
||||
for pane_info in panes {
|
||||
let pane_matches = match pane_id {
|
||||
PaneId::Terminal(id) => !pane_info.is_plugin && pane_info.id == id,
|
||||
PaneId::Plugin(id) => pane_info.is_plugin && pane_info.id == id,
|
||||
};
|
||||
|
||||
if pane_matches {
|
||||
pane_found = true;
|
||||
if pane_info.exited {
|
||||
command.set_status(CommandStatus::Exited(
|
||||
pane_info.exit_status,
|
||||
Some(pane_id),
|
||||
));
|
||||
updated = true;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
if pane_found {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if !pane_found && command.start_time.elapsed() > Duration::from_millis(400) {
|
||||
match command.get_status() {
|
||||
CommandStatus::Running(_) => {
|
||||
eprintln!(
|
||||
"Pane {:?} was closed while running, setting pane_id to None",
|
||||
pane_id
|
||||
);
|
||||
command.set_status(CommandStatus::Running(None));
|
||||
updated = true;
|
||||
},
|
||||
CommandStatus::Exited(exit_code, _) => {
|
||||
eprintln!(
|
||||
"Pane {:?} was closed after exiting, setting pane_id to None",
|
||||
pane_id
|
||||
);
|
||||
command.set_status(CommandStatus::Exited(exit_code, None));
|
||||
updated = true;
|
||||
},
|
||||
CommandStatus::Interrupted(_) => {
|
||||
eprintln!("Pane {:?} was closed after being interrupted, setting pane_id to None", pane_id);
|
||||
command.set_status(CommandStatus::Interrupted(None));
|
||||
updated = true;
|
||||
},
|
||||
_ => {},
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
updated
|
||||
}
|
||||
|
||||
pub fn update_sequence_stopped_state(&mut self) -> bool {
|
||||
let mut needs_rerender = false;
|
||||
if self.is_running {
|
||||
let current_idx = self.current_running_command_index;
|
||||
if let Some(command) = self.all_commands.get_mut(current_idx) {
|
||||
if let CommandStatus::Exited(exit_code, _) = command.get_status() {
|
||||
let should_stop;
|
||||
if current_idx >= self.all_commands.len().saturating_sub(1) {
|
||||
should_stop = true;
|
||||
} else {
|
||||
if let Some(chain_type) = &self
|
||||
.all_commands
|
||||
.get(current_idx)
|
||||
.map(|c| c.get_chain_type())
|
||||
{
|
||||
match chain_type {
|
||||
ChainType::And => {
|
||||
should_stop = exit_code.unwrap_or(0) != 0;
|
||||
},
|
||||
ChainType::Or => {
|
||||
should_stop = exit_code.unwrap_or(0) == 0;
|
||||
},
|
||||
ChainType::Then => {
|
||||
should_stop = false;
|
||||
},
|
||||
ChainType::None => {
|
||||
should_stop = true;
|
||||
},
|
||||
}
|
||||
} else {
|
||||
should_stop = true;
|
||||
}
|
||||
};
|
||||
if should_stop {
|
||||
self.is_running = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
needs_rerender = true;
|
||||
}
|
||||
needs_rerender
|
||||
}
|
||||
|
||||
pub fn execute_command_sequence(
|
||||
&mut self,
|
||||
shell: &Option<PathBuf>,
|
||||
global_cwd: &Option<PathBuf>,
|
||||
primary_pane_id: Option<PaneId>,
|
||||
) {
|
||||
use zellij_tile::prelude::actions::{Action, RunCommandAction};
|
||||
|
||||
self.all_commands.retain(|c| !c.is_empty());
|
||||
|
||||
let Some(first_active_sequence_command) = self.get_first_command() else {
|
||||
return;
|
||||
};
|
||||
|
||||
let shell = shell.clone().unwrap_or_else(|| PathBuf::from("/bin/bash"));
|
||||
|
||||
let first_command = first_active_sequence_command.get_text();
|
||||
let first_chain_type = first_active_sequence_command.get_chain_type();
|
||||
let command_cwd = first_active_sequence_command
|
||||
.get_cwd()
|
||||
.or_else(|| global_cwd.clone());
|
||||
|
||||
let command = RunCommandAction {
|
||||
command: shell.clone(),
|
||||
args: vec!["-ic".to_string(), first_command.trim().to_string()],
|
||||
cwd: command_cwd,
|
||||
hold_on_close: true,
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let placement = NewPanePlacement::InPlace {
|
||||
pane_id_to_replace: primary_pane_id,
|
||||
close_replaced_pane: false,
|
||||
borderless: Some(false),
|
||||
};
|
||||
|
||||
let action = Action::NewBlockingPane {
|
||||
placement,
|
||||
command: Some(command),
|
||||
pane_name: Some(first_command.trim().to_string()),
|
||||
unblock_condition: first_chain_type.to_unblock_condition(),
|
||||
near_current_pane: true,
|
||||
};
|
||||
|
||||
self.sequence_id += 1;
|
||||
|
||||
let mut context = BTreeMap::new();
|
||||
context.insert("sequence_id".to_string(), self.sequence_id.to_string());
|
||||
run_action(action, context);
|
||||
self.set_command_status(0, CommandStatus::Running(None));
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for Execution {
|
||||
|
||||
@@ -1,10 +1,4 @@
|
||||
use crate::state::positioning;
|
||||
use std::path::PathBuf;
|
||||
use zellij_tile::prelude::*;
|
||||
|
||||
pub struct Layout {
|
||||
pub needs_reposition: bool,
|
||||
pub cached_cursor_position: Option<(usize, usize)>,
|
||||
pub spinner_frame: usize,
|
||||
pub spinner_timer_scheduled: bool,
|
||||
}
|
||||
@@ -12,58 +6,10 @@ pub struct Layout {
|
||||
impl Layout {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
needs_reposition: false,
|
||||
cached_cursor_position: None,
|
||||
spinner_frame: 0,
|
||||
spinner_timer_scheduled: false,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn reposition_plugin_based_on_sequence_state(
|
||||
&mut self,
|
||||
is_running: bool,
|
||||
first_command_pane_id: Option<PaneId>,
|
||||
pane_manifest: &PaneManifest,
|
||||
plugin_id: Option<u32>,
|
||||
total_viewport_columns: Option<usize>,
|
||||
total_viewport_rows: Option<usize>,
|
||||
total_commands: usize,
|
||||
_selected_index: Option<usize>,
|
||||
_running_index: usize,
|
||||
_cwd: &Option<PathBuf>,
|
||||
) {
|
||||
let Some(plugin_id) = plugin_id else {
|
||||
return;
|
||||
};
|
||||
let (Some(total_cols), Some(total_rows)) = (total_viewport_columns, total_viewport_rows)
|
||||
else {
|
||||
return;
|
||||
};
|
||||
|
||||
if !self.needs_reposition || !is_running {
|
||||
return;
|
||||
}
|
||||
|
||||
let Some(first_pane_id) = first_command_pane_id else {
|
||||
return;
|
||||
};
|
||||
|
||||
for (_tab_index, panes) in &pane_manifest.panes {
|
||||
for pane in panes {
|
||||
if !pane.is_plugin && PaneId::Terminal(pane.id) == first_pane_id {
|
||||
positioning::reposition_plugin_for_sequence(
|
||||
plugin_id,
|
||||
total_cols,
|
||||
total_rows,
|
||||
total_commands,
|
||||
);
|
||||
|
||||
self.needs_reposition = false;
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for Layout {
|
||||
|
||||
@@ -2,28 +2,25 @@ mod chain_type;
|
||||
mod command_entry;
|
||||
mod command_parser;
|
||||
mod command_status;
|
||||
mod editing;
|
||||
mod execution;
|
||||
mod layout;
|
||||
pub mod positioning;
|
||||
mod selection;
|
||||
mod sequence_mode;
|
||||
|
||||
pub use chain_type::ChainType;
|
||||
pub use command_entry::CommandEntry;
|
||||
pub use command_parser::{
|
||||
detect_cd_command, detect_chain_operator_at_end, get_remaining_after_first_segment,
|
||||
split_by_chain_operators,
|
||||
};
|
||||
pub use command_parser::{parse_commands, serialize_sequence_to_editor};
|
||||
pub use command_status::CommandStatus;
|
||||
pub use editing::Editing;
|
||||
pub use execution::Execution;
|
||||
pub use layout::Layout;
|
||||
pub use selection::Selection;
|
||||
pub use sequence_mode::SequenceMode;
|
||||
|
||||
use crate::ui::text_input::TextInput;
|
||||
use std::collections::BTreeMap;
|
||||
use std::path::PathBuf;
|
||||
use zellij_tile::prelude::*;
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct State {
|
||||
pub shell: Option<PathBuf>,
|
||||
pub plugin_id: Option<u32>,
|
||||
@@ -31,291 +28,85 @@ pub struct State {
|
||||
pub own_rows: Option<usize>,
|
||||
pub own_columns: Option<usize>,
|
||||
pub cwd: Option<PathBuf>,
|
||||
pub original_pane_id: Option<PaneId>,
|
||||
pub primary_pane_id: Option<PaneId>,
|
||||
pub is_first_run: bool,
|
||||
pub pane_manifest: Option<PaneManifest>,
|
||||
pub total_viewport_columns: Option<usize>,
|
||||
pub total_viewport_rows: Option<usize>,
|
||||
pub selection: Selection,
|
||||
pub editing: Editing,
|
||||
pub execution: Execution,
|
||||
pub layout: Layout,
|
||||
pub current_position: Option<FloatingPaneCoordinates>,
|
||||
pub mode: SequenceMode,
|
||||
pub editor_pane_id: Option<PaneId>,
|
||||
pub editor_temp_file: Option<PathBuf>,
|
||||
pub sequence_tab_id: Option<usize>,
|
||||
pub blocking_pipe_id: Option<String>,
|
||||
}
|
||||
|
||||
impl State {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
shell: None,
|
||||
plugin_id: None,
|
||||
client_id: None,
|
||||
own_rows: None,
|
||||
own_columns: None,
|
||||
cwd: None,
|
||||
original_pane_id: None,
|
||||
primary_pane_id: None,
|
||||
is_first_run: true,
|
||||
pane_manifest: None,
|
||||
total_viewport_columns: None,
|
||||
total_viewport_rows: None,
|
||||
selection: Selection::new(),
|
||||
editing: Editing::new(),
|
||||
execution: Execution::new(),
|
||||
layout: Layout::new(),
|
||||
current_position: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_plugin_id(&mut self, plugin_id: u32) {
|
||||
self.plugin_id = Some(plugin_id);
|
||||
}
|
||||
|
||||
/// Check if the sequence has finished executing (all commands are done)
|
||||
pub fn has_finished(&self) -> bool {
|
||||
self.execution
|
||||
.all_commands
|
||||
.iter()
|
||||
.all(|command| matches!(command.status, CommandStatus::Exited(_, _)))
|
||||
}
|
||||
pub fn add_empty_command_after_current_selected(&mut self) {
|
||||
self.selection.add_empty_command_after_current_selected(
|
||||
&mut self.execution.all_commands,
|
||||
&mut self.editing,
|
||||
&self.cwd,
|
||||
);
|
||||
}
|
||||
|
||||
pub fn current_selected_command_is_empty(&self) -> bool {
|
||||
self.selection
|
||||
.current_selected_command_is_empty(&self.execution.all_commands, &self.editing)
|
||||
}
|
||||
|
||||
pub fn remove_current_selected_command(&mut self) {
|
||||
let removed_pane_id = self
|
||||
.selection
|
||||
.remove_current_selected_command(&mut self.execution.all_commands, &mut self.editing);
|
||||
if let Some(removed_pane_id) = removed_pane_id {
|
||||
close_pane_with_id(removed_pane_id);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn clear_current_selected_command(&mut self) {
|
||||
self.selection
|
||||
.clear_current_selected_command(&mut self.execution.all_commands, &mut self.editing);
|
||||
}
|
||||
|
||||
pub fn move_selection_up(&mut self) {
|
||||
self.selection
|
||||
.move_up(&mut self.execution.all_commands, &mut self.editing);
|
||||
if let Some(pane_id) = self
|
||||
.current_selected_command()
|
||||
.and_then(|c| c.get_pane_id())
|
||||
{
|
||||
show_pane_with_id(pane_id, true, false);
|
||||
}
|
||||
self.selection.move_up(&self.execution.all_commands);
|
||||
self.show_selected_pane();
|
||||
}
|
||||
|
||||
pub fn move_selection_down(&mut self) {
|
||||
self.selection
|
||||
.move_down(&mut self.execution.all_commands, &mut self.editing);
|
||||
if let Some(pane_id) = self
|
||||
.current_selected_command()
|
||||
.and_then(|c| c.get_pane_id())
|
||||
{
|
||||
show_pane_with_id(pane_id, true, false);
|
||||
}
|
||||
}
|
||||
pub fn start_editing_selected(&mut self) {
|
||||
if let Some(current_command_text) = self.current_selected_command().map(|c| c.get_text()) {
|
||||
self.editing.start_editing(current_command_text);
|
||||
}
|
||||
self.selection.move_down(&self.execution.all_commands);
|
||||
self.show_selected_pane();
|
||||
}
|
||||
|
||||
pub fn cancel_editing_selected(&mut self) {
|
||||
self.editing.cancel_editing();
|
||||
}
|
||||
|
||||
pub fn editing_input_text(&self) -> Option<String> {
|
||||
self.editing.input_text()
|
||||
}
|
||||
|
||||
pub fn set_editing_input_text(&mut self, text: String) {
|
||||
self.editing.set_input_text(text);
|
||||
}
|
||||
fn handle_first_pasted_segment(
|
||||
&mut self,
|
||||
current_text: &str,
|
||||
segment_text: &str,
|
||||
chain_type_opt: &Option<ChainType>,
|
||||
) {
|
||||
let new_text = if current_text.trim().is_empty() {
|
||||
segment_text.to_string()
|
||||
} else {
|
||||
format!("{}{}", current_text, segment_text)
|
||||
};
|
||||
|
||||
if let Some(path) = detect_cd_command(&new_text) {
|
||||
use crate::path_formatting;
|
||||
if let Some(new_cwd) = path_formatting::resolve_path(self.cwd.as_ref(), &path) {
|
||||
self.current_selected_command_mut().map(|c| {
|
||||
c.set_cwd(Some(new_cwd));
|
||||
c.set_text("".to_owned());
|
||||
if let Some(chain_type) = chain_type_opt {
|
||||
c.set_chain_type(*chain_type);
|
||||
}
|
||||
});
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
self.current_selected_command_mut().map(|c| {
|
||||
c.set_text(new_text);
|
||||
if let Some(chain_type) = chain_type_opt {
|
||||
c.set_chain_type(*chain_type);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
fn insert_new_pasted_segment(
|
||||
&mut self,
|
||||
segment_text: &str,
|
||||
chain_type_opt: &Option<ChainType>,
|
||||
is_last_line: bool,
|
||||
) {
|
||||
use crate::path_formatting;
|
||||
|
||||
let cd_path = detect_cd_command(segment_text);
|
||||
|
||||
if let Some(path) = cd_path {
|
||||
if let Some(new_cwd) = path_formatting::resolve_path(self.cwd.as_ref(), &path) {
|
||||
self.current_selected_command_mut().map(|c| {
|
||||
c.set_cwd(Some(new_cwd));
|
||||
if let Some(chain_type) = chain_type_opt {
|
||||
c.set_chain_type(*chain_type);
|
||||
}
|
||||
});
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
let Some(new_selected_index) = self.selection.current_selected_command_index.map(|i| i + 1)
|
||||
else {
|
||||
return;
|
||||
};
|
||||
|
||||
let mut new_command = CommandEntry::new(segment_text, self.cwd.clone());
|
||||
if let Some(chain_type) = chain_type_opt {
|
||||
new_command.set_chain_type(*chain_type);
|
||||
} else if !is_last_line {
|
||||
new_command.set_chain_type(ChainType::And);
|
||||
}
|
||||
|
||||
self.execution
|
||||
.all_commands
|
||||
.insert(new_selected_index, new_command);
|
||||
self.selection.current_selected_command_index = Some(new_selected_index);
|
||||
}
|
||||
|
||||
fn ensure_line_end_chain_type(&mut self) {
|
||||
if let Some(last_cmd_index) = self.selection.current_selected_command_index {
|
||||
self.execution
|
||||
pub fn show_selected_pane(&mut self) {
|
||||
if self.mode == SequenceMode::Spread {
|
||||
let all_pane_ids: Vec<PaneId> = self
|
||||
.execution
|
||||
.all_commands
|
||||
.get_mut(last_cmd_index)
|
||||
.map(|c| {
|
||||
if matches!(c.get_chain_type(), ChainType::None) {
|
||||
c.set_chain_type(ChainType::And);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
pub fn pasted_lines(&mut self, lines: Vec<&str>) {
|
||||
let Some(current_text) = self.editing_input_text() else {
|
||||
.iter()
|
||||
.filter_map(|c| c.get_pane_id())
|
||||
.collect();
|
||||
let selected_pane_id = self
|
||||
.current_selected_command()
|
||||
.and_then(|c| c.get_pane_id());
|
||||
if let Some(selected) = selected_pane_id {
|
||||
let to_unhighlight: Vec<PaneId> = all_pane_ids
|
||||
.into_iter()
|
||||
.filter(|&p| p != selected)
|
||||
.collect();
|
||||
highlight_and_unhighlight_panes(vec![selected], to_unhighlight);
|
||||
}
|
||||
return;
|
||||
};
|
||||
|
||||
for (line_index, line) in lines.iter().enumerate() {
|
||||
let segments = split_by_chain_operators(line);
|
||||
let is_last_line = line_index == lines.len().saturating_sub(1);
|
||||
|
||||
for (seg_index, (segment_text, chain_type_opt)) in segments.iter().enumerate() {
|
||||
if line_index == 0 && seg_index == 0 {
|
||||
self.handle_first_pasted_segment(¤t_text, segment_text, chain_type_opt);
|
||||
} else {
|
||||
self.insert_new_pasted_segment(segment_text, chain_type_opt, is_last_line);
|
||||
}
|
||||
}
|
||||
|
||||
if !is_last_line {
|
||||
self.ensure_line_end_chain_type();
|
||||
}
|
||||
let target_pane_id = self
|
||||
.current_selected_command()
|
||||
.and_then(|c| c.get_pane_id());
|
||||
if let (Some(displayed), Some(target)) = (self.execution.displayed_pane_id, target_pane_id)
|
||||
{
|
||||
if displayed != target {
|
||||
replace_pane_with_existing_pane(displayed, target, true);
|
||||
self.execution.displayed_pane_id = Some(target);
|
||||
}
|
||||
}
|
||||
|
||||
self.start_editing_selected();
|
||||
}
|
||||
|
||||
pub fn remove_empty_commands(&mut self) {
|
||||
self.execution.remove_empty_commands();
|
||||
}
|
||||
|
||||
pub fn get_first_command(&self) -> Option<CommandEntry> {
|
||||
self.execution.get_first_command()
|
||||
}
|
||||
|
||||
pub fn set_command_status(&mut self, command_index: usize, status: CommandStatus) {
|
||||
self.execution.set_command_status(command_index, status);
|
||||
}
|
||||
|
||||
pub fn set_current_running_command_status(&mut self, status: CommandStatus) {
|
||||
self.execution.set_current_running_command_status(status);
|
||||
}
|
||||
|
||||
pub fn get_current_running_command_status(&mut self) -> Option<CommandStatus> {
|
||||
self.execution.get_current_running_command_status()
|
||||
}
|
||||
|
||||
pub fn handle_editing_submit(&mut self, current_cwd: &Option<PathBuf>) -> bool {
|
||||
let (handled_internally, new_selection_index) = self.editing.handle_submit(
|
||||
self.selection.current_selected_command_index,
|
||||
&mut self.execution.all_commands,
|
||||
current_cwd,
|
||||
);
|
||||
self.selection.current_selected_command_index = new_selection_index;
|
||||
handled_internally
|
||||
}
|
||||
|
||||
pub fn update_pane_id_for_command(&mut self, pane_id: PaneId, command_text: &str) {
|
||||
self.execution
|
||||
.update_pane_id_for_command(pane_id, command_text);
|
||||
}
|
||||
pub fn update_exited_command_statuses(&mut self, pane_manifest: &PaneManifest) -> bool {
|
||||
self.execution.update_exited_command_statuses(pane_manifest)
|
||||
}
|
||||
pub fn update_sequence_stopped_state(&mut self) -> bool {
|
||||
self.execution.update_sequence_stopped_state()
|
||||
}
|
||||
|
||||
pub fn cycle_chain_type(&mut self) {
|
||||
self.current_selected_command_mut()
|
||||
.map(|c| c.cycle_chain_type());
|
||||
}
|
||||
|
||||
pub fn set_primary_pane_id_before_sequence(&mut self, pane_id: Option<PaneId>) {
|
||||
if pane_id.is_some() {
|
||||
self.execution.primary_pane_id_before_sequence = pane_id;
|
||||
}
|
||||
}
|
||||
pub fn clear_all_commands(&mut self) {
|
||||
self.execution.all_commands = vec![CommandEntry::new("", self.cwd.clone())];
|
||||
self.editing.editing_input = Some(TextInput::new("".to_owned()));
|
||||
self.selection.current_selected_command_index = Some(0);
|
||||
self.selection.current_selected_command_index = None;
|
||||
self.execution.current_running_command_index = 0;
|
||||
}
|
||||
pub fn update_running_state(&mut self, primary_pane_id: Option<PaneId>) {
|
||||
self.remove_empty_commands();
|
||||
self.set_primary_pane_id_before_sequence(primary_pane_id);
|
||||
self.layout.needs_reposition = true;
|
||||
self.execution.is_running = true;
|
||||
|
||||
pub fn load_commands(&mut self, command_string: &str, cwd_override: Option<PathBuf>) {
|
||||
let effective_cwd = cwd_override.or_else(|| self.cwd.clone());
|
||||
let commands = parse_commands(command_string, effective_cwd);
|
||||
if commands.is_empty() {
|
||||
return;
|
||||
}
|
||||
self.execution.all_commands = commands;
|
||||
self.execution.current_running_command_index = 0;
|
||||
self.selection.current_selected_command_index = None;
|
||||
}
|
||||
|
||||
pub fn can_run_sequence(&self) -> bool {
|
||||
@@ -332,6 +123,31 @@ impl State {
|
||||
};
|
||||
self.execution.all_commands.get_mut(i)
|
||||
}
|
||||
|
||||
/// Serialize current sequence to a temp file and open it in the user's $EDITOR.
|
||||
/// Stores the editor pane id and temp file path for later retrieval.
|
||||
pub fn open_editor(&mut self) {
|
||||
if self.execution.is_running {
|
||||
return;
|
||||
}
|
||||
let Some(plugin_id) = self.plugin_id else {
|
||||
return;
|
||||
};
|
||||
|
||||
let serialized = serialize_sequence_to_editor(&self.execution.all_commands);
|
||||
let temp_path = PathBuf::from(format!("/tmp/zellij-sequence-{}.sh", plugin_id));
|
||||
|
||||
if let Err(e) = std::fs::write(&temp_path, &serialized) {
|
||||
eprintln!("Failed to write sequence to temp file: {}", e);
|
||||
return;
|
||||
}
|
||||
|
||||
let file_to_open = FileToOpen::new(temp_path.clone());
|
||||
let editor_pane_id = open_file_in_place_of_plugin(file_to_open, false, BTreeMap::new());
|
||||
self.editor_pane_id = editor_pane_id;
|
||||
self.editor_temp_file = Some(temp_path);
|
||||
}
|
||||
|
||||
pub fn reposition_plugin(&mut self) -> bool {
|
||||
let mut repositioned = false;
|
||||
let Some(plugin_id) = self.plugin_id else {
|
||||
@@ -343,82 +159,62 @@ impl State {
|
||||
let Some(total_viewport_columns) = self.total_viewport_columns else {
|
||||
return repositioned;
|
||||
};
|
||||
if self.all_commands_are_pending() {
|
||||
return false;
|
||||
}
|
||||
|
||||
let total_commands = std::cmp::max(self.execution.all_commands.iter().len(), 1);
|
||||
let height_padding = 7;
|
||||
if self.all_commands_are_pending() {
|
||||
let initial_height = total_commands + height_padding;
|
||||
let y = total_viewport_rows.saturating_sub(initial_height) / 2;
|
||||
let coordinates = FloatingPaneCoordinates::new(
|
||||
Some(format!("25%")),
|
||||
Some(format!("{}", y)),
|
||||
Some(format!("50%")),
|
||||
Some(format!("{}", initial_height)),
|
||||
Some(false), // should not be pinned when running sequence
|
||||
Some(false),
|
||||
);
|
||||
coordinates.map(|coordinates| {
|
||||
if Some(&coordinates) != self.current_position.as_ref() {
|
||||
repositioned = true;
|
||||
self.current_position = Some(coordinates.clone());
|
||||
change_floating_panes_coordinates(vec![(
|
||||
PaneId::Plugin(plugin_id),
|
||||
coordinates,
|
||||
)]);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
// Calculate longest table row width
|
||||
let longest_cwd_display = self.execution.longest_cwd_display(&self.cwd);
|
||||
let longest_command = self
|
||||
.execution
|
||||
.all_commands
|
||||
.iter()
|
||||
.map(|cmd| cmd.get_text().chars().count())
|
||||
.max()
|
||||
.unwrap_or(1);
|
||||
|
||||
let (max_chain_width, max_status_width) = crate::ui::components::calculate_max_widths(
|
||||
&self.execution.all_commands,
|
||||
self.layout.spinner_frame,
|
||||
);
|
||||
// Calculate longest table row width
|
||||
let longest_cwd_display = self.execution.longest_cwd_display(&self.cwd);
|
||||
let longest_command = self
|
||||
.execution
|
||||
.all_commands
|
||||
.iter()
|
||||
.map(|cmd| cmd.get_text().chars().count())
|
||||
.max()
|
||||
.unwrap_or(1);
|
||||
|
||||
let longest_line = crate::ui::components::calculate_longest_line(
|
||||
&longest_cwd_display,
|
||||
longest_command,
|
||||
max_chain_width,
|
||||
max_status_width,
|
||||
);
|
||||
let (max_chain_width, max_status_width) = crate::ui::components::calculate_max_widths(
|
||||
&self.execution.all_commands,
|
||||
self.layout.spinner_frame,
|
||||
);
|
||||
|
||||
let ui_width = std::cmp::max(longest_line, 50) + 4; // 2 for ui-padding, 2 for pane frame
|
||||
let width = std::cmp::min(ui_width, total_viewport_columns / 2);
|
||||
let longest_line = crate::ui::components::calculate_longest_line(
|
||||
&longest_cwd_display,
|
||||
longest_command,
|
||||
max_chain_width,
|
||||
max_status_width,
|
||||
);
|
||||
|
||||
let height = (height_padding + total_commands).min((total_viewport_rows * 25) / 100);
|
||||
let ui_width = std::cmp::max(longest_line, 50) + 4; // 2 for ui-padding, 2 for pane frame
|
||||
let width = std::cmp::min(ui_width, total_viewport_columns.saturating_sub(2));
|
||||
|
||||
// Position: top-right with 1-space margins
|
||||
let x = total_viewport_columns.saturating_sub(width);
|
||||
let y = 1;
|
||||
let height = (height_padding + total_commands).min((total_viewport_rows * 50) / 100);
|
||||
|
||||
let coordinates = FloatingPaneCoordinates::new(
|
||||
Some(x.to_string()),
|
||||
Some(y.to_string()),
|
||||
Some(width.to_string()),
|
||||
Some(height.to_string()),
|
||||
Some(true), // should be pinned for sequence
|
||||
Some(false),
|
||||
);
|
||||
coordinates.map(|coordinates| {
|
||||
if Some(&coordinates) != self.current_position.as_ref() {
|
||||
self.current_position = Some(coordinates.clone());
|
||||
repositioned = true;
|
||||
change_floating_panes_coordinates(vec![(
|
||||
PaneId::Plugin(plugin_id),
|
||||
coordinates,
|
||||
)]);
|
||||
}
|
||||
});
|
||||
}
|
||||
// Position: top-right with 1-space margins
|
||||
let x = total_viewport_columns.saturating_sub(width);
|
||||
let y = 1;
|
||||
|
||||
let coordinates = FloatingPaneCoordinates::new(
|
||||
Some(x.to_string()),
|
||||
Some(y.to_string()),
|
||||
Some(width.to_string()),
|
||||
Some(height.to_string()),
|
||||
Some(true), // should be pinned for sequence
|
||||
Some(false),
|
||||
);
|
||||
coordinates.map(|coordinates| {
|
||||
if Some(&coordinates) != self.current_position.as_ref() {
|
||||
self.current_position = Some(coordinates.clone());
|
||||
repositioned = true;
|
||||
change_floating_panes_coordinates(vec![(PaneId::Plugin(plugin_id), coordinates)]);
|
||||
}
|
||||
});
|
||||
repositioned
|
||||
}
|
||||
|
||||
pub fn all_commands_are_pending(&self) -> bool {
|
||||
self.execution
|
||||
.all_commands
|
||||
@@ -432,17 +228,4 @@ impl State {
|
||||
};
|
||||
self.execution.all_commands.get(i)
|
||||
}
|
||||
|
||||
pub fn execute_command_sequence(&mut self) {
|
||||
self.execution
|
||||
.execute_command_sequence(&self.shell, &self.cwd, self.primary_pane_id);
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for State {
|
||||
fn default() -> Self {
|
||||
let mut state = Self::new();
|
||||
state.is_first_run = true;
|
||||
state
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,49 +0,0 @@
|
||||
use zellij_tile::prelude::*;
|
||||
|
||||
pub fn reposition_plugin_for_sequence(
|
||||
plugin_id: u32,
|
||||
total_cols: usize,
|
||||
total_rows: usize,
|
||||
total_commands: usize,
|
||||
) {
|
||||
// Width: 30% of viewport
|
||||
let width = std::cmp::max((total_cols * 30) / 100, 50);
|
||||
|
||||
// Height: UI overhead + commands, capped at 25% of viewport
|
||||
let overhead_rows = 6;
|
||||
let height = (overhead_rows + total_commands).min((total_rows * 25) / 100);
|
||||
|
||||
// Position: top-right with 1-space margins
|
||||
let x = total_cols.saturating_sub(width);
|
||||
let y = 1;
|
||||
|
||||
change_floating_pane_coordinates_absolute(
|
||||
plugin_id,
|
||||
Some(x),
|
||||
Some(y),
|
||||
Some(width),
|
||||
Some(height),
|
||||
true,
|
||||
);
|
||||
}
|
||||
|
||||
pub fn change_floating_pane_coordinates_absolute(
|
||||
own_plugin_id: u32,
|
||||
x: Option<usize>,
|
||||
y: Option<usize>,
|
||||
width: Option<usize>,
|
||||
height: Option<usize>,
|
||||
should_be_pinned: bool,
|
||||
) {
|
||||
let coordinates = FloatingPaneCoordinates::new(
|
||||
x.map(|x| x.to_string()),
|
||||
y.map(|y| y.to_string()),
|
||||
width.map(|width| width.to_string()),
|
||||
height.map(|height| height.to_string()),
|
||||
Some(should_be_pinned),
|
||||
Some(false),
|
||||
);
|
||||
if let Some(coordinates) = coordinates {
|
||||
change_floating_panes_coordinates(vec![(PaneId::Plugin(own_plugin_id), coordinates)]);
|
||||
}
|
||||
}
|
||||
@@ -1,115 +1,17 @@
|
||||
use crate::state::{CommandEntry, Editing};
|
||||
use std::path::PathBuf;
|
||||
use zellij_tile::prelude::*;
|
||||
use super::CommandEntry;
|
||||
|
||||
pub struct Selection {
|
||||
pub current_selected_command_index: Option<usize>,
|
||||
pub scroll_offset: usize,
|
||||
}
|
||||
|
||||
impl Selection {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
current_selected_command_index: Some(0),
|
||||
scroll_offset: 0,
|
||||
current_selected_command_index: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn add_empty_command_after_current_selected(
|
||||
&mut self,
|
||||
all_commands: &mut Vec<CommandEntry>,
|
||||
editing: &mut Editing,
|
||||
cwd: &Option<PathBuf>,
|
||||
) {
|
||||
self.save_editing_buffer_to_current_selected(all_commands, editing);
|
||||
editing.editing_input.as_mut().map(|i| i.clear());
|
||||
if !self.current_selected_command_is_empty(all_commands, editing) {
|
||||
self.current_selected_command_mut(all_commands)
|
||||
.map(|c| c.fill_chain_type_if_empty());
|
||||
|
||||
if let Some(current_selected_command_index) = self.current_selected_command_index.take()
|
||||
{
|
||||
let new_command = CommandEntry::new("", cwd.clone());
|
||||
if current_selected_command_index == all_commands.len().saturating_sub(1) {
|
||||
all_commands.push(new_command);
|
||||
} else {
|
||||
all_commands.insert(current_selected_command_index + 1, new_command);
|
||||
}
|
||||
|
||||
self.current_selected_command_index = Some(current_selected_command_index + 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn current_selected_command_is_empty(
|
||||
&self,
|
||||
all_commands: &[CommandEntry],
|
||||
editing: &Editing,
|
||||
) -> bool {
|
||||
let text_input_is_empty = editing
|
||||
.editing_input
|
||||
.as_ref()
|
||||
.map(|i| i.is_empty())
|
||||
.unwrap_or(true);
|
||||
let current_selected_command_is_empty = self
|
||||
.current_selected_command_index
|
||||
.and_then(|i| all_commands.get(i).map(|c| c.is_empty()))
|
||||
.unwrap_or(true);
|
||||
text_input_is_empty && current_selected_command_is_empty
|
||||
}
|
||||
|
||||
pub fn remove_current_selected_command(
|
||||
&mut self,
|
||||
all_commands: &mut Vec<CommandEntry>,
|
||||
editing: &mut Editing,
|
||||
) -> Option<PaneId> {
|
||||
// returns the pane_id of the removed command, if any
|
||||
let mut removed_pane_id = None;
|
||||
let Some(mut current_selected_command_index) = self.current_selected_command_index else {
|
||||
return removed_pane_id;
|
||||
};
|
||||
if all_commands.len() > 1 && all_commands.len() > current_selected_command_index {
|
||||
let command = all_commands.remove(current_selected_command_index);
|
||||
removed_pane_id = command.get_pane_id();
|
||||
} else {
|
||||
self.clear_current_selected_command(all_commands, editing);
|
||||
}
|
||||
if current_selected_command_index >= all_commands.len()
|
||||
&& current_selected_command_index > 0
|
||||
{
|
||||
current_selected_command_index -= 1;
|
||||
}
|
||||
self.update_current_selected_command_index(
|
||||
current_selected_command_index,
|
||||
all_commands,
|
||||
editing,
|
||||
);
|
||||
if current_selected_command_index == all_commands.len().saturating_sub(1) {
|
||||
self.current_selected_command_mut(all_commands)
|
||||
.map(|c| c.clear_chain_type());
|
||||
}
|
||||
removed_pane_id
|
||||
}
|
||||
|
||||
pub fn clear_current_selected_command(
|
||||
&mut self,
|
||||
all_commands: &mut [CommandEntry],
|
||||
editing: &mut Editing,
|
||||
) {
|
||||
let Some(current_selected_command_index) = self.current_selected_command_index else {
|
||||
return;
|
||||
};
|
||||
self.current_selected_command_mut(all_commands)
|
||||
.map(|c| c.clear_text());
|
||||
self.clear_editing_buffer(editing);
|
||||
if current_selected_command_index == all_commands.len().saturating_sub(1) {
|
||||
self.current_selected_command_mut(all_commands)
|
||||
.map(|c| c.clear_chain_type());
|
||||
}
|
||||
}
|
||||
|
||||
pub fn move_up(&mut self, all_commands: &mut [CommandEntry], editing: &mut Editing) {
|
||||
self.save_editing_buffer_to_current_selected(all_commands, editing);
|
||||
pub fn move_up(&mut self, all_commands: &[CommandEntry]) {
|
||||
match self.current_selected_command_index.as_mut() {
|
||||
Some(i) if *i > 0 => {
|
||||
*i -= 1;
|
||||
@@ -121,11 +23,9 @@ impl Selection {
|
||||
self.current_selected_command_index = None;
|
||||
},
|
||||
}
|
||||
self.update_editing_buffer_to_current_selected(all_commands, editing);
|
||||
}
|
||||
|
||||
pub fn move_down(&mut self, all_commands: &mut [CommandEntry], editing: &mut Editing) {
|
||||
self.save_editing_buffer_to_current_selected(all_commands, editing);
|
||||
pub fn move_down(&mut self, all_commands: &[CommandEntry]) {
|
||||
match self.current_selected_command_index.as_mut() {
|
||||
Some(i) if *i < all_commands.len().saturating_sub(1) => {
|
||||
*i += 1;
|
||||
@@ -137,73 +37,6 @@ impl Selection {
|
||||
self.current_selected_command_index = None;
|
||||
},
|
||||
}
|
||||
self.update_editing_buffer_to_current_selected(all_commands, editing);
|
||||
}
|
||||
|
||||
fn current_selected_command_mut<'a>(
|
||||
&self,
|
||||
all_commands: &'a mut [CommandEntry],
|
||||
) -> Option<&'a mut CommandEntry> {
|
||||
let Some(i) = self.current_selected_command_index else {
|
||||
return None;
|
||||
};
|
||||
all_commands.get_mut(i)
|
||||
}
|
||||
|
||||
fn current_selected_command<'a>(
|
||||
&self,
|
||||
all_commands: &'a [CommandEntry],
|
||||
) -> Option<&'a CommandEntry> {
|
||||
let Some(i) = self.current_selected_command_index else {
|
||||
return None;
|
||||
};
|
||||
all_commands.get(i)
|
||||
}
|
||||
|
||||
fn clear_editing_buffer(&self, editing: &mut Editing) {
|
||||
editing.editing_input.as_mut().map(|c| c.clear());
|
||||
}
|
||||
|
||||
fn update_current_selected_command_index(
|
||||
&mut self,
|
||||
new_index: usize,
|
||||
all_commands: &[CommandEntry],
|
||||
editing: &mut Editing,
|
||||
) {
|
||||
self.current_selected_command_index = Some(new_index);
|
||||
self.update_editing_buffer_to_current_selected(all_commands, editing);
|
||||
}
|
||||
|
||||
fn get_text_of_current_selected_command(
|
||||
&self,
|
||||
all_commands: &[CommandEntry],
|
||||
) -> Option<String> {
|
||||
self.current_selected_command(all_commands)
|
||||
.map(|c| c.get_text())
|
||||
}
|
||||
|
||||
fn save_editing_buffer_to_current_selected(
|
||||
&self,
|
||||
all_commands: &mut [CommandEntry],
|
||||
editing: &mut Editing,
|
||||
) {
|
||||
if let Some(text_input) = editing.editing_input.as_ref().map(|i| i.get_text()) {
|
||||
self.current_selected_command_mut(all_commands)
|
||||
.map(|c| c.set_text(text_input.to_owned()));
|
||||
}
|
||||
}
|
||||
|
||||
fn update_editing_buffer_to_current_selected(
|
||||
&self,
|
||||
all_commands: &[CommandEntry],
|
||||
editing: &mut Editing,
|
||||
) {
|
||||
let new_text_of_current_selected_command = self
|
||||
.get_text_of_current_selected_command(all_commands)
|
||||
.unwrap_or_else(|| String::new());
|
||||
if let Some(editing_input) = editing.editing_input.as_mut() {
|
||||
editing_input.set_text(new_text_of_current_selected_command);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
#[derive(Default, Debug, Clone, PartialEq)]
|
||||
pub enum SequenceMode {
|
||||
#[default]
|
||||
SinglePane,
|
||||
Spread,
|
||||
}
|
||||
@@ -6,6 +6,11 @@ use zellij_tile::prelude::*;
|
||||
|
||||
const SPINNER_FRAMES: &[&str] = &["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧"];
|
||||
|
||||
const INTRO_PART1: &str = "Paste a series of commands to run in sequence";
|
||||
const INTRO_PART2: &str = "or <e> to edit in your editor";
|
||||
const INTRO_FULL: &str =
|
||||
"Paste a series of commands to run in sequence or <e> to edit in your editor";
|
||||
|
||||
const HELP_RUNNING_WITH_SELECTION: &str = "<Ctrl c> - interrupt, <Enter> - run from selected";
|
||||
const HELP_RUNNING_NO_SELECTION: &str = "<Ctrl c> - interrupt, <↓↑> - navigate";
|
||||
const HELP_STOPPED_WITH_SELECTION: &str =
|
||||
@@ -13,15 +18,7 @@ const HELP_STOPPED_WITH_SELECTION: &str =
|
||||
const HELP_STOPPED_NO_SELECTION: &str =
|
||||
"<Ctrl w> - close all, <↓↑> - navigate, <Enter> - run from first";
|
||||
|
||||
const HELP_ONE_PENDING_COMMAND: &str = "<Enter> - run, <Ctrl Enter> - add command";
|
||||
|
||||
const HELP_ALL_COMMANDS_PENDING: &str =
|
||||
"<Enter> - run, <Ctrl Enter> - add command, <↓↑> - navigate";
|
||||
const HELP_ALL_COMMANDS_PENDING_WITH_SELECTION: &str =
|
||||
"<Enter> - run, <Ctrl Enter> - add command, <↓↑> - navigate, <e> - edit selected";
|
||||
|
||||
const HELP_EDITING_FIRST_LINE: &str =
|
||||
"<Enter> - accept, <Ctrl Enter> - add command, <↓↑> - navigate";
|
||||
const HELP_PENDING: &str = "<e> - edit, <Enter> - run";
|
||||
|
||||
fn select_help_text(sequence: &State) -> &'static str {
|
||||
let is_running = sequence
|
||||
@@ -35,18 +32,9 @@ fn select_help_text(sequence: &State) -> &'static str {
|
||||
.iter()
|
||||
.all(|command| matches!(command.get_status(), CommandStatus::Pending));
|
||||
let has_selection = sequence.selection.current_selected_command_index.is_some();
|
||||
let is_editing = sequence.editing.editing_input.is_some();
|
||||
|
||||
if all_pending && !sequence.execution.is_running {
|
||||
if sequence.execution.all_commands.len() == 1 {
|
||||
HELP_ONE_PENDING_COMMAND
|
||||
} else if has_selection && !is_editing {
|
||||
HELP_ALL_COMMANDS_PENDING_WITH_SELECTION
|
||||
} else if is_editing {
|
||||
HELP_EDITING_FIRST_LINE
|
||||
} else {
|
||||
HELP_ALL_COMMANDS_PENDING
|
||||
}
|
||||
HELP_PENDING
|
||||
} else if is_running {
|
||||
if has_selection {
|
||||
HELP_RUNNING_WITH_SELECTION
|
||||
@@ -54,10 +42,8 @@ fn select_help_text(sequence: &State) -> &'static str {
|
||||
HELP_RUNNING_NO_SELECTION
|
||||
}
|
||||
} else {
|
||||
if has_selection && !is_editing {
|
||||
if has_selection {
|
||||
HELP_STOPPED_WITH_SELECTION
|
||||
} else if is_editing {
|
||||
HELP_EDITING_FIRST_LINE
|
||||
} else {
|
||||
HELP_STOPPED_NO_SELECTION
|
||||
}
|
||||
@@ -71,7 +57,6 @@ fn style_help_text(text: &str) -> Text {
|
||||
.color_substring(3, "<↓↑>")
|
||||
.color_substring(3, "<Esc>")
|
||||
.color_substring(3, "<Enter>")
|
||||
.color_substring(3, "<Ctrl Enter>")
|
||||
.color_substring(3, "<e>")
|
||||
}
|
||||
|
||||
@@ -79,33 +64,6 @@ pub fn render_help_lines(
|
||||
sequence: &State,
|
||||
max_width: Option<usize>,
|
||||
) -> (Text, usize, Option<(Text, usize)>) {
|
||||
let is_editing = sequence.editing.editing_input.is_some();
|
||||
|
||||
if is_editing {
|
||||
let help_text = select_help_text(sequence);
|
||||
let (truncated_text, help_len) = if let Some(width) = max_width {
|
||||
truncate_help_line(help_text, width)
|
||||
} else {
|
||||
(help_text.to_string(), help_text.chars().count())
|
||||
};
|
||||
let first_line = (style_help_text(&truncated_text).unbold_all(), help_len);
|
||||
|
||||
let editing_help_text = "Navigate with cd, chain with ||, &&, ; ";
|
||||
let editing_help = Text::new(editing_help_text)
|
||||
.color_substring(3, "cd")
|
||||
.color_substring(3, "||")
|
||||
.color_substring(3, "&&")
|
||||
.color_substring(3, ";")
|
||||
.unbold_all();
|
||||
let editing_len = editing_help_text.len();
|
||||
|
||||
return (
|
||||
first_line.0,
|
||||
first_line.1,
|
||||
Some((editing_help, editing_len)),
|
||||
);
|
||||
}
|
||||
|
||||
let help_text = select_help_text(sequence);
|
||||
|
||||
if let Some(width) = max_width {
|
||||
@@ -139,6 +97,38 @@ pub fn render_help_lines(
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns `(text, x, y)` tuples for the main screen intro hint.
|
||||
/// The text is centered horizontally; if it does not fit on one line, two lines
|
||||
/// are returned separated by one blank line (1-line padding).
|
||||
pub fn render_intro_hint(rows: usize, cols: usize) -> Vec<(Text, usize, usize)> {
|
||||
let full_len = INTRO_FULL.chars().count();
|
||||
|
||||
if full_len <= cols {
|
||||
let x = cols.saturating_sub(full_len) / 2;
|
||||
let y = rows / 2;
|
||||
vec![(Text::new(INTRO_FULL).color_substring(3, "<e>"), x, y)]
|
||||
} else {
|
||||
let p1_len = INTRO_PART1.chars().count();
|
||||
let p2_len = INTRO_PART2.chars().count();
|
||||
let x1 = cols.saturating_sub(p1_len) / 2;
|
||||
let x2 = cols.saturating_sub(p2_len) / 2;
|
||||
// 3 display rows: line1, padding, line2 — center the group vertically
|
||||
let start_y = rows.saturating_sub(3) / 2;
|
||||
vec![
|
||||
(
|
||||
Text::new(INTRO_PART1).color_substring(3, "<e>"),
|
||||
x1,
|
||||
start_y,
|
||||
),
|
||||
(
|
||||
Text::new(INTRO_PART2).color_substring(3, "<e>"),
|
||||
x2,
|
||||
start_y + 2,
|
||||
),
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
fn split_help_line(help_text: &str, max_width: usize) -> Option<(String, String)> {
|
||||
let text_width = help_text.chars().count();
|
||||
|
||||
@@ -200,7 +190,7 @@ fn chain_cell(chain_type: &crate::state::ChainType) -> Text {
|
||||
}
|
||||
}
|
||||
|
||||
fn get_spinner_frame(frame: usize) -> &'static str {
|
||||
pub fn get_spinner_frame(frame: usize) -> &'static str {
|
||||
SPINNER_FRAMES[frame % SPINNER_FRAMES.len()]
|
||||
}
|
||||
|
||||
@@ -371,7 +361,8 @@ pub fn add_command_row(
|
||||
None => return table,
|
||||
};
|
||||
|
||||
let cmd_text = &command.get_text();
|
||||
let cmd_text = command.get_text();
|
||||
let cmd_text = cmd_text.trim();
|
||||
let chain_type = &command.get_chain_type();
|
||||
let status = &command.get_status();
|
||||
let command_cwd = command.get_cwd().or_else(|| state.cwd.clone());
|
||||
@@ -398,26 +389,6 @@ pub fn add_command_row(
|
||||
return table;
|
||||
};
|
||||
|
||||
if let Some(text_input) = &state.editing.editing_input {
|
||||
if state.selection.current_selected_command_index == Some(index) {
|
||||
let (truncated_editing_text, _) = truncate_middle(
|
||||
text_input.get_text(),
|
||||
available_cmd_width,
|
||||
Some(text_input.cursor_position()),
|
||||
);
|
||||
let (row, _) = command_sequence_row(
|
||||
state,
|
||||
&command_cwd,
|
||||
&truncated_editing_text,
|
||||
chain_type,
|
||||
&CommandStatus::Pending,
|
||||
false,
|
||||
overflow_indicator,
|
||||
);
|
||||
return table.add_styled_row(row);
|
||||
}
|
||||
}
|
||||
|
||||
let truncated_cmd_text = truncate_middle(cmd_text, available_cmd_width, None).0;
|
||||
|
||||
let (row, _) = command_sequence_row(
|
||||
|
||||
@@ -1,321 +0,0 @@
|
||||
use fuzzy_matcher::skim::SkimMatcherV2;
|
||||
use fuzzy_matcher::FuzzyMatcher;
|
||||
use std::collections::BTreeMap;
|
||||
use std::fs;
|
||||
use std::path::PathBuf;
|
||||
|
||||
/// Result of a fuzzy completion operation
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct CompletionResult {
|
||||
pub completed_text: String,
|
||||
pub score: i64,
|
||||
pub is_directory: bool,
|
||||
pub is_prefix_completion: bool, // True if this is the shortest of multiple matches with same prefix
|
||||
}
|
||||
|
||||
/// Type of completion
|
||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||
pub enum CompletionType {
|
||||
Command,
|
||||
Path,
|
||||
}
|
||||
|
||||
/// Fuzzy match executables (commands) from the PATH
|
||||
pub fn fuzzy_complete_command(
|
||||
query: &str,
|
||||
executables: &BTreeMap<String, PathBuf>,
|
||||
) -> Option<CompletionResult> {
|
||||
if query.is_empty() {
|
||||
return None;
|
||||
}
|
||||
|
||||
let matcher = SkimMatcherV2::default().ignore_case();
|
||||
|
||||
// Collect all matches with their scores
|
||||
let mut all_matches: Vec<(String, i64)> = Vec::new();
|
||||
|
||||
for executable_name in executables.keys() {
|
||||
if let Some((score, _indices)) = matcher.fuzzy_indices(executable_name, query) {
|
||||
all_matches.push((executable_name.clone(), score));
|
||||
}
|
||||
}
|
||||
|
||||
if all_matches.is_empty() {
|
||||
return None;
|
||||
}
|
||||
|
||||
// Sort by score (highest first), then by length (shortest first)
|
||||
all_matches.sort_by(|a, b| b.1.cmp(&a.1).then_with(|| a.0.len().cmp(&b.0.len())));
|
||||
|
||||
// Get the best match
|
||||
let (best_name, best_score) = &all_matches[0];
|
||||
|
||||
// Check if there are multiple matches that share the same prefix
|
||||
let is_prefix_completion = check_for_prefix_matches(&all_matches, best_name);
|
||||
|
||||
Some(CompletionResult {
|
||||
completed_text: best_name.clone(),
|
||||
score: *best_score,
|
||||
is_directory: false, // Commands are not directories
|
||||
is_prefix_completion,
|
||||
})
|
||||
}
|
||||
|
||||
/// Fuzzy match paths (directories and files) relative to the current working directory
|
||||
pub fn fuzzy_complete_path(query: &str, cwd: Option<&PathBuf>) -> Option<CompletionResult> {
|
||||
if query.is_empty() {
|
||||
return None;
|
||||
}
|
||||
|
||||
let matcher = SkimMatcherV2::default().ignore_case();
|
||||
|
||||
// Determine the base directory to search in
|
||||
let (base_dir, search_prefix) = if query.starts_with('/') {
|
||||
// Absolute path
|
||||
let parts: Vec<&str> = query.rsplitn(2, '/').collect();
|
||||
if parts.len() == 2 {
|
||||
// Has a directory component
|
||||
let dir = parts[1];
|
||||
let prefix = parts[0];
|
||||
(PathBuf::from(dir), prefix.to_string())
|
||||
} else {
|
||||
// Just "/"
|
||||
(PathBuf::from("/"), String::new())
|
||||
}
|
||||
} else if query.starts_with("~/") {
|
||||
// Home directory
|
||||
let home_dir = std::env::var("HOME").ok()?;
|
||||
let query_without_tilde = &query[2..];
|
||||
let parts: Vec<&str> = query_without_tilde.rsplitn(2, '/').collect();
|
||||
if parts.len() == 2 {
|
||||
let dir = PathBuf::from(&home_dir).join(parts[1]);
|
||||
let prefix = parts[0];
|
||||
(dir, prefix.to_string())
|
||||
} else {
|
||||
// Just "~/" or "~/something"
|
||||
(PathBuf::from(home_dir), query_without_tilde.to_string())
|
||||
}
|
||||
} else {
|
||||
// Relative path
|
||||
let base = cwd?;
|
||||
let parts: Vec<&str> = query.rsplitn(2, '/').collect();
|
||||
if parts.len() == 2 {
|
||||
// Has a directory component
|
||||
let dir = base.join(parts[1]);
|
||||
let prefix = parts[0];
|
||||
(dir, prefix.to_string())
|
||||
} else {
|
||||
// Just a name in the current directory
|
||||
(base.clone(), query.to_string())
|
||||
}
|
||||
};
|
||||
|
||||
// Convert to host path
|
||||
let host_base_dir =
|
||||
PathBuf::from("/host").join(base_dir.strip_prefix("/").unwrap_or(&base_dir));
|
||||
|
||||
// Read directory entries
|
||||
let entries = fs::read_dir(&host_base_dir).ok()?;
|
||||
|
||||
// Collect all matches with their scores and directory status
|
||||
let mut all_matches: Vec<(String, i64, bool)> = Vec::new();
|
||||
|
||||
for entry in entries.flatten() {
|
||||
if let Some(name) = entry.file_name().to_str() {
|
||||
// Skip hidden files unless the query starts with a dot
|
||||
if name.starts_with('.') && !search_prefix.starts_with('.') {
|
||||
continue;
|
||||
}
|
||||
|
||||
if let Some((score, _indices)) = matcher.fuzzy_indices(name, &search_prefix) {
|
||||
// Check if this entry is a directory
|
||||
let is_dir = entry.metadata().ok().map(|m| m.is_dir()).unwrap_or(false);
|
||||
all_matches.push((name.to_string(), score, is_dir));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if all_matches.is_empty() {
|
||||
return None;
|
||||
}
|
||||
|
||||
// Sort by score (highest first), then by length (shortest first)
|
||||
all_matches.sort_by(|a, b| b.1.cmp(&a.1).then_with(|| a.0.len().cmp(&b.0.len())));
|
||||
|
||||
// Get the best match
|
||||
let (name, score, is_directory) = all_matches[0].clone();
|
||||
|
||||
// Check if there are multiple matches that share the same prefix
|
||||
let name_matches: Vec<(String, i64)> = all_matches
|
||||
.iter()
|
||||
.map(|(n, s, _)| (n.clone(), *s))
|
||||
.collect();
|
||||
let is_prefix_completion = check_for_prefix_matches(&name_matches, &name);
|
||||
|
||||
// Reconstruct the full path
|
||||
let completed_text = if query.starts_with('/') {
|
||||
// Absolute path
|
||||
let dir_part = query.rsplitn(2, '/').nth(1).unwrap_or("");
|
||||
if dir_part.is_empty() {
|
||||
format!("/{}", name)
|
||||
} else {
|
||||
format!("{}/{}", dir_part, name)
|
||||
}
|
||||
} else if query.starts_with("~/") {
|
||||
// Home directory
|
||||
let query_without_tilde = &query[2..];
|
||||
let dir_part = query_without_tilde.rsplitn(2, '/').nth(1).unwrap_or("");
|
||||
if dir_part.is_empty() {
|
||||
format!("~/{}", name)
|
||||
} else {
|
||||
format!("~/{}/{}", dir_part, name)
|
||||
}
|
||||
} else {
|
||||
// Relative path
|
||||
let dir_part = query.rsplitn(2, '/').nth(1).unwrap_or("");
|
||||
if dir_part.is_empty() {
|
||||
name.clone()
|
||||
} else {
|
||||
format!("{}/{}", dir_part, name)
|
||||
}
|
||||
};
|
||||
|
||||
Some(CompletionResult {
|
||||
completed_text,
|
||||
score,
|
||||
is_directory,
|
||||
is_prefix_completion,
|
||||
})
|
||||
}
|
||||
|
||||
/// Check if there are multiple matches that start with the best match's name
|
||||
/// Returns true if the best match is a prefix of other matches
|
||||
fn check_for_prefix_matches(matches: &[(String, i64)], best_match: &str) -> bool {
|
||||
if matches.len() <= 1 {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Count how many matches start with the best_match text
|
||||
let prefix_count = matches
|
||||
.iter()
|
||||
.filter(|(name, _)| name.starts_with(best_match) && name != best_match)
|
||||
.count();
|
||||
|
||||
prefix_count > 0
|
||||
}
|
||||
|
||||
/// Perform fuzzy completion on the current input
|
||||
/// Returns the best completion (command or path) with appropriate suffix (space or slash)
|
||||
pub fn fuzzy_complete(
|
||||
input: &str,
|
||||
executables: &BTreeMap<String, PathBuf>,
|
||||
cwd: Option<&PathBuf>,
|
||||
) -> Option<String> {
|
||||
// If input is empty, nothing to complete
|
||||
if input.is_empty() {
|
||||
return None;
|
||||
}
|
||||
|
||||
// Extract the last word/token to complete
|
||||
// For simplicity, we'll complete the entire input if it's a single word,
|
||||
// or the last space-separated token if there are multiple words
|
||||
let tokens: Vec<&str> = input.split_whitespace().collect();
|
||||
let to_complete = if tokens.is_empty() {
|
||||
input
|
||||
} else {
|
||||
tokens.last().unwrap()
|
||||
};
|
||||
|
||||
// Get both command and path completions
|
||||
let command_result = fuzzy_complete_command(to_complete, executables);
|
||||
let path_result = fuzzy_complete_path(to_complete, cwd);
|
||||
|
||||
// Choose the better match based on score
|
||||
match (command_result, path_result) {
|
||||
(Some(cmd), Some(path)) => {
|
||||
// Both matched, use the higher score
|
||||
let (result, completion_type, is_dir, is_prefix) = if cmd.score >= path.score {
|
||||
(
|
||||
cmd.completed_text,
|
||||
CompletionType::Command,
|
||||
false,
|
||||
cmd.is_prefix_completion,
|
||||
)
|
||||
} else {
|
||||
(
|
||||
path.completed_text,
|
||||
CompletionType::Path,
|
||||
path.is_directory,
|
||||
path.is_prefix_completion,
|
||||
)
|
||||
};
|
||||
|
||||
// Add appropriate suffix (skip slash if this is a prefix completion)
|
||||
let suffix = get_completion_suffix(completion_type, is_dir, is_prefix);
|
||||
let completed = format!("{}{}", result, suffix);
|
||||
|
||||
// Replace the last token with the completion
|
||||
Some(replace_last_token(input, &completed))
|
||||
},
|
||||
(Some(cmd), None) => {
|
||||
// Command completion - add space (unless it's a prefix completion)
|
||||
let suffix = if cmd.is_prefix_completion { "" } else { " " };
|
||||
let completed = format!("{}{}", cmd.completed_text, suffix);
|
||||
Some(replace_last_token(input, &completed))
|
||||
},
|
||||
(None, Some(path)) => {
|
||||
// Path completion - add slash if directory (unless it's a prefix completion)
|
||||
let suffix = if path.is_prefix_completion {
|
||||
""
|
||||
} else if path.is_directory {
|
||||
"/"
|
||||
} else {
|
||||
""
|
||||
};
|
||||
let completed = format!("{}{}", path.completed_text, suffix);
|
||||
Some(replace_last_token(input, &completed))
|
||||
},
|
||||
(None, None) => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the appropriate suffix for a completion
|
||||
fn get_completion_suffix(
|
||||
completion_type: CompletionType,
|
||||
is_directory: bool,
|
||||
is_prefix_completion: bool,
|
||||
) -> &'static str {
|
||||
// No suffix for prefix completions
|
||||
if is_prefix_completion {
|
||||
return "";
|
||||
}
|
||||
|
||||
match completion_type {
|
||||
CompletionType::Command => " ",
|
||||
CompletionType::Path => {
|
||||
if is_directory {
|
||||
"/"
|
||||
} else {
|
||||
""
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
/// Replace the last whitespace-separated token in the input with the completion
|
||||
fn replace_last_token(input: &str, completion: &str) -> String {
|
||||
let tokens: Vec<&str> = input.split_whitespace().collect();
|
||||
if tokens.is_empty() {
|
||||
return completion.to_string();
|
||||
}
|
||||
|
||||
if tokens.len() == 1 {
|
||||
completion.to_string()
|
||||
} else {
|
||||
// Join all tokens except the last one, then add the completion
|
||||
let mut result = tokens[..tokens.len() - 1].join(" ");
|
||||
result.push(' ');
|
||||
result.push_str(completion);
|
||||
result
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,3 @@
|
||||
pub mod components;
|
||||
pub mod fuzzy_complete;
|
||||
pub mod layout_calculations;
|
||||
pub mod text_input;
|
||||
pub mod truncation;
|
||||
|
||||
@@ -1,997 +0,0 @@
|
||||
use zellij_tile::prelude::*;
|
||||
|
||||
const MAX_UNDO_STACK_SIZE: usize = 100;
|
||||
|
||||
/// Action returned by TextInput after handling a key event
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
#[cfg_attr(test, derive(Eq))]
|
||||
pub enum InputAction {
|
||||
/// Continue editing
|
||||
Continue,
|
||||
/// User pressed Enter to submit
|
||||
Submit,
|
||||
/// User pressed Esc to cancel
|
||||
Cancel,
|
||||
/// User pressed Tab to request completion
|
||||
Complete,
|
||||
/// Key was not handled by the input
|
||||
NoAction,
|
||||
}
|
||||
|
||||
/// A reusable text input component with cursor support and standard editing keybindings
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct TextInput {
|
||||
buffer: String,
|
||||
cursor_position: usize, // Character position (0-based), NOT byte position
|
||||
undo_stack: Vec<(String, usize)>, // (buffer, cursor) snapshots
|
||||
redo_stack: Vec<(String, usize)>,
|
||||
last_edit_was_insert: bool, // For coalescing consecutive inserts
|
||||
}
|
||||
|
||||
impl TextInput {
|
||||
/// Create a new TextInput with the given initial text
|
||||
/// Cursor is positioned at the end of the text
|
||||
pub fn new(initial_text: String) -> Self {
|
||||
let cursor_position = initial_text.chars().count();
|
||||
Self {
|
||||
buffer: initial_text,
|
||||
cursor_position,
|
||||
undo_stack: Vec::new(),
|
||||
redo_stack: Vec::new(),
|
||||
last_edit_was_insert: false,
|
||||
}
|
||||
}
|
||||
|
||||
/// Create an empty TextInput
|
||||
pub fn empty() -> Self {
|
||||
Self::new(String::new())
|
||||
}
|
||||
|
||||
/// Get the current text
|
||||
pub fn get_text(&self) -> &str {
|
||||
&self.buffer
|
||||
}
|
||||
|
||||
/// Get the cursor position (in characters, not bytes)
|
||||
pub fn get_cursor_position(&self) -> usize {
|
||||
self.cursor_position
|
||||
}
|
||||
|
||||
/// Check if the input is empty
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self.buffer.is_empty()
|
||||
}
|
||||
|
||||
/// Get a shorthand for cursor_position
|
||||
pub fn cursor_position(&self) -> usize {
|
||||
self.cursor_position
|
||||
}
|
||||
|
||||
/// Get mutable access to the underlying buffer for direct manipulation
|
||||
pub fn get_text_mut(&mut self) -> &mut String {
|
||||
&mut self.buffer
|
||||
}
|
||||
|
||||
/// Set the text and move cursor to the end
|
||||
pub fn set_text(&mut self, text: String) {
|
||||
self.break_coalescing();
|
||||
self.save_undo_state();
|
||||
self.cursor_position = text.chars().count();
|
||||
self.buffer = text;
|
||||
}
|
||||
|
||||
/// Set cursor position (clamped to text length)
|
||||
pub fn set_cursor_position(&mut self, pos: usize) {
|
||||
let text_len = self.buffer.chars().count();
|
||||
self.cursor_position = pos.min(text_len);
|
||||
}
|
||||
|
||||
/// Clear all text and reset cursor
|
||||
pub fn clear(&mut self) {
|
||||
self.break_coalescing();
|
||||
self.save_undo_state();
|
||||
self.buffer.clear();
|
||||
self.cursor_position = 0;
|
||||
}
|
||||
|
||||
/// Insert a character at the current cursor position
|
||||
pub fn insert_char(&mut self, c: char) {
|
||||
self.save_undo_state_unless_coalescing();
|
||||
// Convert cursor position (char index) to byte index
|
||||
let byte_index = self.char_index_to_byte_index(self.cursor_position);
|
||||
self.buffer.insert(byte_index, c);
|
||||
self.cursor_position += 1;
|
||||
}
|
||||
|
||||
/// Delete the character before the cursor (backspace)
|
||||
pub fn backspace(&mut self) {
|
||||
if self.cursor_position > 0 {
|
||||
self.break_coalescing();
|
||||
self.save_undo_state();
|
||||
self.cursor_position -= 1;
|
||||
let byte_index = self.char_index_to_byte_index(self.cursor_position);
|
||||
self.buffer.remove(byte_index);
|
||||
}
|
||||
}
|
||||
|
||||
/// Delete the character at the cursor position (delete key)
|
||||
pub fn delete(&mut self) {
|
||||
let len = self.buffer.chars().count();
|
||||
if self.cursor_position < len {
|
||||
self.break_coalescing();
|
||||
self.save_undo_state();
|
||||
let byte_index = self.char_index_to_byte_index(self.cursor_position);
|
||||
self.buffer.remove(byte_index);
|
||||
}
|
||||
}
|
||||
|
||||
/// Delete the word before the cursor (Ctrl/Alt + Backspace)
|
||||
pub fn delete_word_backward(&mut self) {
|
||||
if self.cursor_position == 0 {
|
||||
return;
|
||||
}
|
||||
|
||||
self.break_coalescing();
|
||||
self.save_undo_state();
|
||||
|
||||
let old_position = self.cursor_position;
|
||||
self.move_word_left();
|
||||
let new_position = self.cursor_position;
|
||||
|
||||
// Delete from new position to old position
|
||||
let start_byte = self.char_index_to_byte_index(new_position);
|
||||
let end_byte = self.char_index_to_byte_index(old_position);
|
||||
self.buffer.drain(start_byte..end_byte);
|
||||
}
|
||||
|
||||
/// Delete the word after the cursor (Ctrl/Alt + Delete)
|
||||
pub fn delete_word_forward(&mut self) {
|
||||
let chars: Vec<char> = self.buffer.chars().collect();
|
||||
let len = chars.len();
|
||||
|
||||
if self.cursor_position >= len {
|
||||
return;
|
||||
}
|
||||
|
||||
self.break_coalescing();
|
||||
self.save_undo_state();
|
||||
|
||||
let start_position = self.cursor_position;
|
||||
let mut end_position = start_position;
|
||||
|
||||
// Skip the current word
|
||||
while end_position < len && !chars[end_position].is_whitespace() {
|
||||
end_position += 1;
|
||||
}
|
||||
|
||||
// Skip any whitespace after the word
|
||||
while end_position < len && chars[end_position].is_whitespace() {
|
||||
end_position += 1;
|
||||
}
|
||||
|
||||
// Delete from start to end position
|
||||
let start_byte = self.char_index_to_byte_index(start_position);
|
||||
let end_byte = self.char_index_to_byte_index(end_position);
|
||||
self.buffer.drain(start_byte..end_byte);
|
||||
}
|
||||
|
||||
/// Move cursor one position to the left
|
||||
pub fn move_left(&mut self) {
|
||||
if self.cursor_position > 0 {
|
||||
self.break_coalescing();
|
||||
self.cursor_position -= 1;
|
||||
}
|
||||
}
|
||||
|
||||
/// Move cursor one position to the right
|
||||
pub fn move_right(&mut self) {
|
||||
let len = self.buffer.chars().count();
|
||||
if self.cursor_position < len {
|
||||
self.break_coalescing();
|
||||
self.cursor_position += 1;
|
||||
}
|
||||
}
|
||||
|
||||
/// Move cursor to the start of the text (Ctrl-A / Home)
|
||||
pub fn move_to_start(&mut self) {
|
||||
self.break_coalescing();
|
||||
self.cursor_position = 0;
|
||||
}
|
||||
|
||||
/// Move cursor to the end of the text (Ctrl-E / End)
|
||||
pub fn move_to_end(&mut self) {
|
||||
self.break_coalescing();
|
||||
self.cursor_position = self.buffer.chars().count();
|
||||
}
|
||||
|
||||
/// Move cursor to the start of the previous word (Ctrl/Alt + Left)
|
||||
pub fn move_word_left(&mut self) {
|
||||
if self.cursor_position == 0 {
|
||||
return;
|
||||
}
|
||||
|
||||
self.break_coalescing();
|
||||
|
||||
let chars: Vec<char> = self.buffer.chars().collect();
|
||||
let mut pos = self.cursor_position;
|
||||
|
||||
// Skip any whitespace immediately to the left
|
||||
while pos > 0 && chars[pos - 1].is_whitespace() {
|
||||
pos -= 1;
|
||||
}
|
||||
|
||||
// Skip the word characters
|
||||
while pos > 0 && !chars[pos - 1].is_whitespace() {
|
||||
pos -= 1;
|
||||
}
|
||||
|
||||
self.cursor_position = pos;
|
||||
}
|
||||
|
||||
/// Move cursor to the start of the next word (Ctrl/Alt + Right)
|
||||
pub fn move_word_right(&mut self) {
|
||||
let chars: Vec<char> = self.buffer.chars().collect();
|
||||
let len = chars.len();
|
||||
|
||||
if self.cursor_position >= len {
|
||||
return;
|
||||
}
|
||||
|
||||
self.break_coalescing();
|
||||
|
||||
let mut pos = self.cursor_position;
|
||||
|
||||
// Skip the current word
|
||||
while pos < len && !chars[pos].is_whitespace() {
|
||||
pos += 1;
|
||||
}
|
||||
|
||||
// Skip any whitespace
|
||||
while pos < len && chars[pos].is_whitespace() {
|
||||
pos += 1;
|
||||
}
|
||||
|
||||
self.cursor_position = pos;
|
||||
}
|
||||
|
||||
/// Handle a key event and return the appropriate action
|
||||
/// This is the main entry point for key handling
|
||||
pub fn handle_key(&mut self, key: KeyWithModifier) -> InputAction {
|
||||
// Check for Ctrl modifiers
|
||||
if key.has_modifiers(&[KeyModifier::Ctrl]) {
|
||||
match key.bare_key {
|
||||
BareKey::Char('a') => {
|
||||
self.move_to_start();
|
||||
return InputAction::Continue;
|
||||
},
|
||||
BareKey::Char('e') => {
|
||||
self.move_to_end();
|
||||
return InputAction::Continue;
|
||||
},
|
||||
BareKey::Char('c') => {
|
||||
// Ctrl-C clears the prompt
|
||||
return InputAction::Cancel;
|
||||
},
|
||||
BareKey::Char('z') => {
|
||||
// Ctrl-Z: Undo
|
||||
self.undo();
|
||||
return InputAction::Continue;
|
||||
},
|
||||
BareKey::Char('y') => {
|
||||
// Ctrl-Y: Redo
|
||||
self.redo();
|
||||
return InputAction::Continue;
|
||||
},
|
||||
BareKey::Left => {
|
||||
self.move_word_left();
|
||||
return InputAction::Continue;
|
||||
},
|
||||
BareKey::Right => {
|
||||
self.move_word_right();
|
||||
return InputAction::Continue;
|
||||
},
|
||||
BareKey::Backspace => {
|
||||
self.delete_word_backward();
|
||||
return InputAction::Continue;
|
||||
},
|
||||
BareKey::Delete => {
|
||||
self.delete_word_forward();
|
||||
return InputAction::Continue;
|
||||
},
|
||||
_ => {},
|
||||
}
|
||||
}
|
||||
|
||||
// Check for Ctrl+Shift modifiers (alternative redo: Ctrl+Shift+Z)
|
||||
if key.has_modifiers(&[KeyModifier::Ctrl, KeyModifier::Shift]) {
|
||||
match key.bare_key {
|
||||
BareKey::Char('Z') => {
|
||||
// Ctrl-Shift-Z: Redo (alternative)
|
||||
self.redo();
|
||||
return InputAction::Continue;
|
||||
},
|
||||
_ => {},
|
||||
}
|
||||
}
|
||||
|
||||
// Check for Alt modifiers
|
||||
if key.has_modifiers(&[KeyModifier::Alt]) {
|
||||
match key.bare_key {
|
||||
BareKey::Left => {
|
||||
self.move_word_left();
|
||||
return InputAction::Continue;
|
||||
},
|
||||
BareKey::Right => {
|
||||
self.move_word_right();
|
||||
return InputAction::Continue;
|
||||
},
|
||||
BareKey::Backspace => {
|
||||
self.delete_word_backward();
|
||||
return InputAction::Continue;
|
||||
},
|
||||
BareKey::Delete => {
|
||||
self.delete_word_forward();
|
||||
return InputAction::Continue;
|
||||
},
|
||||
_ => {},
|
||||
}
|
||||
}
|
||||
|
||||
// Handle bare keys (no modifiers)
|
||||
match key.bare_key {
|
||||
BareKey::Enter => InputAction::Submit,
|
||||
BareKey::Esc => InputAction::Cancel,
|
||||
BareKey::Tab => InputAction::Complete,
|
||||
BareKey::Backspace => {
|
||||
self.backspace();
|
||||
InputAction::Continue
|
||||
},
|
||||
BareKey::Delete => {
|
||||
self.delete();
|
||||
InputAction::Continue
|
||||
},
|
||||
BareKey::Left => {
|
||||
self.move_left();
|
||||
InputAction::Continue
|
||||
},
|
||||
BareKey::Right => {
|
||||
self.move_right();
|
||||
InputAction::Continue
|
||||
},
|
||||
BareKey::Home => {
|
||||
self.move_to_start();
|
||||
InputAction::Continue
|
||||
},
|
||||
BareKey::End => {
|
||||
self.move_to_end();
|
||||
InputAction::Continue
|
||||
},
|
||||
BareKey::Char(c) => {
|
||||
self.insert_char(c);
|
||||
InputAction::Continue
|
||||
},
|
||||
_ => InputAction::NoAction,
|
||||
}
|
||||
}
|
||||
|
||||
/// Helper: Convert character index to byte index
|
||||
fn char_index_to_byte_index(&self, char_index: usize) -> usize {
|
||||
self.buffer
|
||||
.char_indices()
|
||||
.nth(char_index)
|
||||
.map(|(byte_idx, _)| byte_idx)
|
||||
.unwrap_or(self.buffer.len())
|
||||
}
|
||||
|
||||
/// Save current state to undo stack before making changes
|
||||
fn save_undo_state(&mut self) {
|
||||
if self.undo_stack.len() >= MAX_UNDO_STACK_SIZE {
|
||||
self.undo_stack.remove(0);
|
||||
}
|
||||
self.undo_stack
|
||||
.push((self.buffer.clone(), self.cursor_position));
|
||||
self.redo_stack.clear();
|
||||
}
|
||||
|
||||
/// Save state only if not coalescing with previous insert
|
||||
fn save_undo_state_unless_coalescing(&mut self) {
|
||||
if !self.last_edit_was_insert {
|
||||
self.save_undo_state();
|
||||
}
|
||||
self.last_edit_was_insert = true;
|
||||
}
|
||||
|
||||
/// Mark that a non-insert edit occurred (breaks coalescing)
|
||||
fn break_coalescing(&mut self) {
|
||||
self.last_edit_was_insert = false;
|
||||
}
|
||||
|
||||
/// Undo last change
|
||||
pub fn undo(&mut self) -> bool {
|
||||
if let Some((buffer, cursor)) = self.undo_stack.pop() {
|
||||
self.redo_stack
|
||||
.push((self.buffer.clone(), self.cursor_position));
|
||||
self.buffer = buffer;
|
||||
self.cursor_position = cursor;
|
||||
self.break_coalescing();
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
/// Redo last undone change
|
||||
pub fn redo(&mut self) -> bool {
|
||||
if let Some((buffer, cursor)) = self.redo_stack.pop() {
|
||||
self.undo_stack
|
||||
.push((self.buffer.clone(), self.cursor_position));
|
||||
self.buffer = buffer;
|
||||
self.cursor_position = cursor;
|
||||
self.break_coalescing();
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
/// Check if undo is available
|
||||
pub fn can_undo(&self) -> bool {
|
||||
!self.undo_stack.is_empty()
|
||||
}
|
||||
|
||||
/// Check if redo is available
|
||||
pub fn can_redo(&self) -> bool {
|
||||
!self.redo_stack.is_empty()
|
||||
}
|
||||
|
||||
pub fn drain_text(&mut self) -> String {
|
||||
self.cursor_position = 0;
|
||||
self.buffer.drain(..).collect()
|
||||
}
|
||||
}
|
||||
|
||||
// run with:
|
||||
// cargo test --lib --target x86_64-unknown-linux-gnu
|
||||
//
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_new_and_empty() {
|
||||
let input = TextInput::new("hello".to_string());
|
||||
assert_eq!(input.get_text(), "hello");
|
||||
assert_eq!(input.get_cursor_position(), 5);
|
||||
|
||||
let empty = TextInput::empty();
|
||||
assert_eq!(empty.get_text(), "");
|
||||
assert_eq!(empty.get_cursor_position(), 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_insert_char() {
|
||||
let mut input = TextInput::new("helo".to_string());
|
||||
input.cursor_position = 3; // Position after "hel"
|
||||
input.insert_char('l');
|
||||
assert_eq!(input.get_text(), "hello");
|
||||
assert_eq!(input.get_cursor_position(), 4);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_backspace() {
|
||||
let mut input = TextInput::new("hello".to_string());
|
||||
input.backspace();
|
||||
assert_eq!(input.get_text(), "hell");
|
||||
assert_eq!(input.get_cursor_position(), 4);
|
||||
|
||||
// Backspace at start does nothing
|
||||
input.cursor_position = 0;
|
||||
input.backspace();
|
||||
assert_eq!(input.get_text(), "hell");
|
||||
assert_eq!(input.get_cursor_position(), 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_delete() {
|
||||
let mut input = TextInput::new("hello".to_string());
|
||||
input.cursor_position = 0;
|
||||
input.delete();
|
||||
assert_eq!(input.get_text(), "ello");
|
||||
assert_eq!(input.get_cursor_position(), 0);
|
||||
|
||||
// Delete at end does nothing
|
||||
input.move_to_end();
|
||||
input.delete();
|
||||
assert_eq!(input.get_text(), "ello");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_cursor_movement() {
|
||||
let mut input = TextInput::new("hello".to_string());
|
||||
assert_eq!(input.get_cursor_position(), 5);
|
||||
|
||||
input.move_left();
|
||||
assert_eq!(input.get_cursor_position(), 4);
|
||||
|
||||
input.move_right();
|
||||
assert_eq!(input.get_cursor_position(), 5);
|
||||
|
||||
input.move_to_start();
|
||||
assert_eq!(input.get_cursor_position(), 0);
|
||||
|
||||
input.move_to_end();
|
||||
assert_eq!(input.get_cursor_position(), 5);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_unicode_support() {
|
||||
let mut input = TextInput::new("hello 🦀 world".to_string());
|
||||
assert_eq!(input.get_cursor_position(), 13); // 13 characters
|
||||
|
||||
input.cursor_position = 6; // After "hello "
|
||||
input.insert_char('🐱');
|
||||
assert_eq!(input.get_text(), "hello 🐱🦀 world");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_word_jump_right() {
|
||||
let mut input = TextInput::new("hello world foo bar".to_string());
|
||||
input.cursor_position = 0;
|
||||
|
||||
// Jump from start to "world"
|
||||
input.move_word_right();
|
||||
assert_eq!(input.get_cursor_position(), 6); // After "hello "
|
||||
|
||||
// Jump to "foo"
|
||||
input.move_word_right();
|
||||
assert_eq!(input.get_cursor_position(), 12); // After "world "
|
||||
|
||||
// Jump to "bar"
|
||||
input.move_word_right();
|
||||
assert_eq!(input.get_cursor_position(), 16); // After "foo "
|
||||
|
||||
// Jump to end
|
||||
input.move_word_right();
|
||||
assert_eq!(input.get_cursor_position(), 19); // At end
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_word_jump_left() {
|
||||
let mut input = TextInput::new("hello world foo bar".to_string());
|
||||
input.move_to_end();
|
||||
assert_eq!(input.get_cursor_position(), 19);
|
||||
|
||||
// Jump back to "bar"
|
||||
input.move_word_left();
|
||||
assert_eq!(input.get_cursor_position(), 16); // Start of "bar"
|
||||
|
||||
// Jump back to "foo"
|
||||
input.move_word_left();
|
||||
assert_eq!(input.get_cursor_position(), 12); // Start of "foo"
|
||||
|
||||
// Jump back to "world"
|
||||
input.move_word_left();
|
||||
assert_eq!(input.get_cursor_position(), 6); // Start of "world"
|
||||
|
||||
// Jump back to "hello"
|
||||
input.move_word_left();
|
||||
assert_eq!(input.get_cursor_position(), 0); // Start of "hello"
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_word_jump_with_multiple_spaces() {
|
||||
let mut input = TextInput::new("hello world".to_string());
|
||||
input.cursor_position = 0;
|
||||
|
||||
// Jump over multiple spaces
|
||||
input.move_word_right();
|
||||
assert_eq!(input.get_cursor_position(), 8); // After "hello ", at start of "world"
|
||||
|
||||
// Jump back should skip spaces
|
||||
input.move_word_left();
|
||||
assert_eq!(input.get_cursor_position(), 0); // Back to start of "hello"
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_word_jump_boundaries() {
|
||||
let mut input = TextInput::new("test".to_string());
|
||||
|
||||
// At start - word left does nothing
|
||||
input.cursor_position = 0;
|
||||
input.move_word_left();
|
||||
assert_eq!(input.get_cursor_position(), 0);
|
||||
|
||||
// At end - word right does nothing
|
||||
input.move_to_end();
|
||||
let end_pos = input.get_cursor_position();
|
||||
input.move_word_right();
|
||||
assert_eq!(input.get_cursor_position(), end_pos);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_up_down_arrows() {
|
||||
let mut input = TextInput::new("hello world".to_string());
|
||||
|
||||
// Start in the middle
|
||||
input.cursor_position = 5;
|
||||
assert_eq!(input.get_cursor_position(), 5);
|
||||
|
||||
// Up arrow should go to start
|
||||
input.move_to_start();
|
||||
assert_eq!(input.get_cursor_position(), 0);
|
||||
|
||||
// Move back to middle
|
||||
input.cursor_position = 5;
|
||||
|
||||
// Down arrow should go to end
|
||||
input.move_to_end();
|
||||
assert_eq!(input.get_cursor_position(), 11);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_delete_word_backward() {
|
||||
let mut input = TextInput::new("hello world foo".to_string());
|
||||
|
||||
// Delete "foo" from end
|
||||
input.move_to_end();
|
||||
input.delete_word_backward();
|
||||
assert_eq!(input.get_text(), "hello world ");
|
||||
assert_eq!(input.get_cursor_position(), 12);
|
||||
|
||||
// Delete "world "
|
||||
input.delete_word_backward();
|
||||
assert_eq!(input.get_text(), "hello ");
|
||||
assert_eq!(input.get_cursor_position(), 6);
|
||||
|
||||
// Delete "hello "
|
||||
input.delete_word_backward();
|
||||
assert_eq!(input.get_text(), "");
|
||||
assert_eq!(input.get_cursor_position(), 0);
|
||||
|
||||
// Delete on empty buffer does nothing
|
||||
input.delete_word_backward();
|
||||
assert_eq!(input.get_text(), "");
|
||||
assert_eq!(input.get_cursor_position(), 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_delete_word_backward_middle() {
|
||||
let mut input = TextInput::new("hello world foo".to_string());
|
||||
|
||||
// Position in middle of "world"
|
||||
input.cursor_position = 8; // After "hello wo"
|
||||
input.delete_word_backward();
|
||||
assert_eq!(input.get_text(), "hello rld foo");
|
||||
assert_eq!(input.get_cursor_position(), 6); // After "hello "
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_delete_word_forward() {
|
||||
let mut input = TextInput::new("hello world foo".to_string());
|
||||
|
||||
// Delete "hello " from start
|
||||
input.cursor_position = 0;
|
||||
input.delete_word_forward();
|
||||
assert_eq!(input.get_text(), "world foo");
|
||||
assert_eq!(input.get_cursor_position(), 0);
|
||||
|
||||
// Delete "world "
|
||||
input.delete_word_forward();
|
||||
assert_eq!(input.get_text(), "foo");
|
||||
assert_eq!(input.get_cursor_position(), 0);
|
||||
|
||||
// Delete "foo"
|
||||
input.delete_word_forward();
|
||||
assert_eq!(input.get_text(), "");
|
||||
assert_eq!(input.get_cursor_position(), 0);
|
||||
|
||||
// Delete on empty buffer does nothing
|
||||
input.delete_word_forward();
|
||||
assert_eq!(input.get_text(), "");
|
||||
assert_eq!(input.get_cursor_position(), 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_delete_word_forward_middle() {
|
||||
let mut input = TextInput::new("hello world foo".to_string());
|
||||
|
||||
// Position in middle of "world"
|
||||
input.cursor_position = 8; // After "hello wo"
|
||||
input.delete_word_forward();
|
||||
assert_eq!(input.get_text(), "hello wofoo");
|
||||
assert_eq!(input.get_cursor_position(), 8); // Same position, text deleted forward
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_delete_word_with_multiple_spaces() {
|
||||
let mut input = TextInput::new("hello world".to_string());
|
||||
|
||||
// Delete forward includes trailing spaces
|
||||
input.cursor_position = 0;
|
||||
input.delete_word_forward();
|
||||
assert_eq!(input.get_text(), "world");
|
||||
assert_eq!(input.get_cursor_position(), 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_undo_redo_basic() {
|
||||
let mut input = TextInput::empty();
|
||||
|
||||
// Type "hello"
|
||||
input.insert_char('h');
|
||||
input.insert_char('e');
|
||||
input.insert_char('l');
|
||||
input.insert_char('l');
|
||||
input.insert_char('o');
|
||||
assert_eq!(input.get_text(), "hello");
|
||||
|
||||
// Undo should remove all characters (coalesced into one undo entry)
|
||||
assert!(input.can_undo());
|
||||
assert!(input.undo());
|
||||
assert_eq!(input.get_text(), "");
|
||||
assert_eq!(input.get_cursor_position(), 0);
|
||||
|
||||
// Redo should restore "hello"
|
||||
assert!(input.can_redo());
|
||||
assert!(input.redo());
|
||||
assert_eq!(input.get_text(), "hello");
|
||||
assert_eq!(input.get_cursor_position(), 5);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_undo_coalescing_breaks_on_cursor_move() {
|
||||
let mut input = TextInput::empty();
|
||||
|
||||
// Type "he"
|
||||
input.insert_char('h');
|
||||
input.insert_char('e');
|
||||
|
||||
// Move cursor to start (breaks coalescing)
|
||||
input.move_to_start();
|
||||
|
||||
// Type "llo"
|
||||
input.insert_char('l');
|
||||
input.insert_char('l');
|
||||
input.insert_char('o');
|
||||
|
||||
assert_eq!(input.get_text(), "llohe");
|
||||
|
||||
// First undo removes "llo" (second coalesced group)
|
||||
input.undo();
|
||||
assert_eq!(input.get_text(), "he");
|
||||
|
||||
// Second undo removes "he" (first coalesced group)
|
||||
input.undo();
|
||||
assert_eq!(input.get_text(), "");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_undo_backspace() {
|
||||
let mut input = TextInput::new("hello".to_string());
|
||||
|
||||
// Backspace once
|
||||
input.backspace();
|
||||
assert_eq!(input.get_text(), "hell");
|
||||
|
||||
// Undo should restore "hello"
|
||||
input.undo();
|
||||
assert_eq!(input.get_text(), "hello");
|
||||
assert_eq!(input.get_cursor_position(), 5);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_undo_delete() {
|
||||
let mut input = TextInput::new("hello".to_string());
|
||||
input.cursor_position = 0;
|
||||
|
||||
// Delete first character
|
||||
input.delete();
|
||||
assert_eq!(input.get_text(), "ello");
|
||||
|
||||
// Undo should restore "hello"
|
||||
input.undo();
|
||||
assert_eq!(input.get_text(), "hello");
|
||||
assert_eq!(input.get_cursor_position(), 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_undo_word_delete() {
|
||||
let mut input = TextInput::new("hello world".to_string());
|
||||
|
||||
// Delete "world" backward
|
||||
input.delete_word_backward();
|
||||
assert_eq!(input.get_text(), "hello ");
|
||||
|
||||
// Undo should restore "hello world"
|
||||
input.undo();
|
||||
assert_eq!(input.get_text(), "hello world");
|
||||
assert_eq!(input.get_cursor_position(), 11);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_undo_clear() {
|
||||
let mut input = TextInput::new("hello world".to_string());
|
||||
|
||||
// Clear the buffer
|
||||
input.clear();
|
||||
assert_eq!(input.get_text(), "");
|
||||
|
||||
// Undo should restore the text
|
||||
input.undo();
|
||||
assert_eq!(input.get_text(), "hello world");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_undo_set_text() {
|
||||
let mut input = TextInput::new("hello".to_string());
|
||||
|
||||
// Replace with new text
|
||||
input.set_text("goodbye".to_string());
|
||||
assert_eq!(input.get_text(), "goodbye");
|
||||
|
||||
// Undo should restore "hello"
|
||||
input.undo();
|
||||
assert_eq!(input.get_text(), "hello");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_redo_clears_on_new_edit() {
|
||||
let mut input = TextInput::empty();
|
||||
|
||||
// Type "hello"
|
||||
input.insert_char('h');
|
||||
input.insert_char('e');
|
||||
input.insert_char('l');
|
||||
input.insert_char('l');
|
||||
input.insert_char('o');
|
||||
|
||||
// Undo
|
||||
input.undo();
|
||||
assert_eq!(input.get_text(), "");
|
||||
assert!(input.can_redo());
|
||||
|
||||
// Make a new edit (should clear redo stack)
|
||||
input.insert_char('x');
|
||||
assert!(!input.can_redo());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_multiple_undo_redo() {
|
||||
let mut input = TextInput::empty();
|
||||
|
||||
// First edit: type "hello"
|
||||
for c in "hello".chars() {
|
||||
input.insert_char(c);
|
||||
}
|
||||
|
||||
// Break coalescing
|
||||
input.move_left();
|
||||
|
||||
// Second edit: type "world"
|
||||
for c in "world".chars() {
|
||||
input.insert_char(c);
|
||||
}
|
||||
|
||||
assert_eq!(input.get_text(), "hellworldo");
|
||||
|
||||
// Undo "world"
|
||||
input.undo();
|
||||
assert_eq!(input.get_text(), "hello");
|
||||
|
||||
// Undo "hello"
|
||||
input.undo();
|
||||
assert_eq!(input.get_text(), "");
|
||||
|
||||
// Redo "hello"
|
||||
input.redo();
|
||||
assert_eq!(input.get_text(), "hello");
|
||||
|
||||
// Redo "world"
|
||||
input.redo();
|
||||
assert_eq!(input.get_text(), "hellworldo");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_undo_stack_limit() {
|
||||
let mut input = TextInput::empty();
|
||||
|
||||
// Perform 102 separate edits (breaking coalescing each time)
|
||||
for _i in 0..102 {
|
||||
input.backspace(); // Break coalescing
|
||||
input.insert_char('x');
|
||||
}
|
||||
|
||||
// Should have at most 100 undo entries
|
||||
let mut undo_count = 0;
|
||||
while input.undo() {
|
||||
undo_count += 1;
|
||||
}
|
||||
|
||||
// We should have 100 undo entries (the stack limit)
|
||||
// Plus the final state change from the last coalescing break
|
||||
assert!(
|
||||
undo_count <= 100,
|
||||
"Undo count should be at most 100, got {}",
|
||||
undo_count
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_undo_redo_empty_stack() {
|
||||
let mut input = TextInput::empty();
|
||||
|
||||
// Undo on empty stack should return false
|
||||
assert!(!input.can_undo());
|
||||
assert!(!input.undo());
|
||||
|
||||
// Redo on empty stack should return false
|
||||
assert!(!input.can_redo());
|
||||
assert!(!input.redo());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_undo_restores_cursor_position() {
|
||||
let mut input = TextInput::new("hello world".to_string());
|
||||
|
||||
// Move cursor to position 5 (before "world")
|
||||
input.cursor_position = 5;
|
||||
|
||||
// Insert a space
|
||||
input.insert_char(' ');
|
||||
assert_eq!(input.get_text(), "hello world");
|
||||
assert_eq!(input.get_cursor_position(), 6);
|
||||
|
||||
// Undo should restore both text and cursor position
|
||||
input.undo();
|
||||
assert_eq!(input.get_text(), "hello world");
|
||||
assert_eq!(input.get_cursor_position(), 5);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_coalescing_consecutive_inserts() {
|
||||
let mut input = TextInput::empty();
|
||||
|
||||
// Type several characters
|
||||
input.insert_char('a');
|
||||
input.insert_char('b');
|
||||
input.insert_char('c');
|
||||
|
||||
assert_eq!(input.get_text(), "abc");
|
||||
|
||||
// Single undo should remove all three (they were coalesced)
|
||||
input.undo();
|
||||
assert_eq!(input.get_text(), "");
|
||||
|
||||
// No more undo available
|
||||
assert!(!input.can_undo());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_backspace_breaks_coalescing() {
|
||||
let mut input = TextInput::empty();
|
||||
|
||||
// Type "ab"
|
||||
input.insert_char('a');
|
||||
input.insert_char('b');
|
||||
|
||||
// Backspace
|
||||
input.backspace();
|
||||
assert_eq!(input.get_text(), "a");
|
||||
|
||||
// Type "c"
|
||||
input.insert_char('c');
|
||||
assert_eq!(input.get_text(), "ac");
|
||||
|
||||
// Undo should remove just "c"
|
||||
input.undo();
|
||||
assert_eq!(input.get_text(), "a");
|
||||
|
||||
// Undo should remove backspace operation
|
||||
input.undo();
|
||||
assert_eq!(input.get_text(), "ab");
|
||||
|
||||
// Undo should remove "ab"
|
||||
input.undo();
|
||||
assert_eq!(input.get_text(), "");
|
||||
}
|
||||
}
|
||||
+27
@@ -210,6 +210,33 @@ fn main() {
|
||||
commands::send_action_to_session(command_cli_action, opts.session, config);
|
||||
std::process::exit(0);
|
||||
}
|
||||
if let Some(Command::Sessions(Sessions::Sequence { payload, blocking })) = opts.command {
|
||||
let args = if blocking {
|
||||
let mut args: std::collections::BTreeMap<String, String> =
|
||||
std::collections::BTreeMap::new();
|
||||
args.insert("blocking".to_string(), "true".to_string());
|
||||
Some(zellij_utils::input::layout::PluginUserConfiguration::new(
|
||||
args,
|
||||
))
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let command_cli_action = CliAction::Pipe {
|
||||
name: None,
|
||||
payload,
|
||||
args,
|
||||
plugin: Some("zellij:sequence".to_string()),
|
||||
plugin_configuration: None,
|
||||
force_launch_plugin: true,
|
||||
skip_plugin_cache: false,
|
||||
floating_plugin: None,
|
||||
in_place_plugin: None,
|
||||
plugin_cwd: None,
|
||||
plugin_title: None,
|
||||
};
|
||||
commands::send_action_to_session(command_cli_action, opts.session, config);
|
||||
std::process::exit(0);
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(Command::Sessions(Sessions::ListSessions {
|
||||
|
||||
@@ -1746,6 +1746,7 @@ impl Grid {
|
||||
pub fn reset_terminal_state(&mut self) {
|
||||
self.lines_above = VecDeque::new();
|
||||
self.lines_below = vec![];
|
||||
self.is_scrolled = false;
|
||||
self.viewport = vec![Row::new().canonical()];
|
||||
self.alternate_screen_state = None;
|
||||
self.cursor_key_mode = false;
|
||||
|
||||
@@ -28,13 +28,15 @@ use zellij_utils::data::{
|
||||
KeyWithModifier, LayoutInfo, LayoutMetadata, LayoutParsingError, MessageToPlugin,
|
||||
NewPanePlacement, NewTabResponse, NewTabsResponse, OpenCommandPaneBackgroundResponse,
|
||||
OpenCommandPaneFloatingNearPluginResponse, OpenCommandPaneFloatingResponse,
|
||||
OpenCommandPaneInPlaceOfPluginResponse, OpenCommandPaneInPlaceResponse,
|
||||
OpenCommandPaneNearPluginResponse, OpenCommandPaneResponse, OpenFileFloatingNearPluginResponse,
|
||||
OpenCommandPaneInPlaceOfPaneIdResponse, OpenCommandPaneInPlaceOfPluginResponse,
|
||||
OpenCommandPaneInPlaceResponse, OpenCommandPaneNearPluginResponse, OpenCommandPaneResponse,
|
||||
OpenEditPaneInPlaceOfPaneIdResponse, OpenFileFloatingNearPluginResponse,
|
||||
OpenFileFloatingResponse, OpenFileInPlaceOfPluginResponse, OpenFileInPlaceResponse,
|
||||
OpenFileNearPluginResponse, OpenFileResponse, OpenTerminalFloatingNearPluginResponse,
|
||||
OpenTerminalFloatingResponse, OpenTerminalInPlaceOfPluginResponse, OpenTerminalInPlaceResponse,
|
||||
OpenTerminalNearPluginResponse, OpenTerminalResponse, OriginatingPlugin,
|
||||
PaneScrollbackResponse, PermissionStatus, PermissionType, PluginPermission,
|
||||
OpenFileNearPluginResponse, OpenFileResponse, OpenPaneInNewTabResponse,
|
||||
OpenTerminalFloatingNearPluginResponse, OpenTerminalFloatingResponse,
|
||||
OpenTerminalInPlaceOfPluginResponse, OpenTerminalInPlaceResponse,
|
||||
OpenTerminalNearPluginResponse, OpenTerminalPaneInPlaceOfPaneIdResponse, OpenTerminalResponse,
|
||||
OriginatingPlugin, PaneScrollbackResponse, PermissionStatus, PermissionType, PluginPermission,
|
||||
RenameLayoutResponse, SaveLayoutResponse, TabMetadata,
|
||||
};
|
||||
use zellij_utils::home::default_layout_dir;
|
||||
@@ -53,10 +55,10 @@ use kdl::KdlDocument;
|
||||
|
||||
use prost::Message;
|
||||
use zellij_utils::{
|
||||
consts::{VERSION, ZELLIJ_SESSION_INFO_CACHE_DIR, ZELLIJ_SOCK_DIR},
|
||||
consts::{VERSION, ZELLIJ_SESSION_INFO_CACHE_DIR, ZELLIJ_SOCK_DIR, ZELLIJ_TMP_DIR},
|
||||
data::{
|
||||
CommandToRun, Direction, EventType, FileToOpen, InputMode, PluginCommand, PluginIds,
|
||||
PluginMessage, Resize, ResizeStrategy,
|
||||
CommandOrPlugin, CommandToRun, Direction, EventType, FileToOpen, InputMode, PluginCommand,
|
||||
PluginIds, PluginMessage, Resize, ResizeStrategy,
|
||||
},
|
||||
errors::prelude::*,
|
||||
input::{
|
||||
@@ -84,14 +86,17 @@ use zellij_utils::{
|
||||
ProtobufOpenCommandPaneBackgroundResponse,
|
||||
ProtobufOpenCommandPaneFloatingNearPluginResponse,
|
||||
ProtobufOpenCommandPaneFloatingResponse,
|
||||
ProtobufOpenCommandPaneInPlaceOfPaneIdResponse,
|
||||
ProtobufOpenCommandPaneInPlaceOfPluginResponse, ProtobufOpenCommandPaneInPlaceResponse,
|
||||
ProtobufOpenCommandPaneNearPluginResponse, ProtobufOpenCommandPaneResponse,
|
||||
ProtobufOpenEditPaneInPlaceOfPaneIdResponse,
|
||||
ProtobufOpenFileFloatingNearPluginResponse, ProtobufOpenFileFloatingResponse,
|
||||
ProtobufOpenFileInPlaceOfPluginResponse, ProtobufOpenFileInPlaceResponse,
|
||||
ProtobufOpenFileNearPluginResponse, ProtobufOpenFileResponse,
|
||||
ProtobufOpenTerminalFloatingNearPluginResponse, ProtobufOpenTerminalFloatingResponse,
|
||||
ProtobufOpenTerminalInPlaceOfPluginResponse, ProtobufOpenTerminalInPlaceResponse,
|
||||
ProtobufOpenTerminalNearPluginResponse, ProtobufOpenTerminalResponse,
|
||||
ProtobufOpenPaneInNewTabResponse, ProtobufOpenTerminalFloatingNearPluginResponse,
|
||||
ProtobufOpenTerminalFloatingResponse, ProtobufOpenTerminalInPlaceOfPluginResponse,
|
||||
ProtobufOpenTerminalInPlaceResponse, ProtobufOpenTerminalNearPluginResponse,
|
||||
ProtobufOpenTerminalPaneInPlaceOfPaneIdResponse, ProtobufOpenTerminalResponse,
|
||||
ProtobufParseLayoutResponse, ProtobufPluginCommand, ProtobufRenameLayoutResponse,
|
||||
ProtobufSaveLayoutResponse, ProtobufSaveSessionResponse,
|
||||
},
|
||||
@@ -131,6 +136,23 @@ macro_rules! apply_action {
|
||||
};
|
||||
}
|
||||
|
||||
fn translate_plugin_path(env: &PluginEnv, path: PathBuf) -> PathBuf {
|
||||
if let Ok(stripped) = path.strip_prefix("/host") {
|
||||
env.plugin_cwd.join(stripped)
|
||||
} else if let Ok(stripped) = path.strip_prefix("/data") {
|
||||
env.plugin_own_data_dir.join(stripped)
|
||||
} else if let Ok(stripped) = path.strip_prefix("/cache") {
|
||||
env.plugin_own_cache_dir.join(stripped)
|
||||
} else if let Ok(stripped) = path.strip_prefix("/tmp") {
|
||||
ZELLIJ_TMP_DIR.join(stripped)
|
||||
} else if path.is_relative() {
|
||||
env.plugin_cwd.join(path)
|
||||
} else {
|
||||
// Absolute path not in any plugin special folder — pass through
|
||||
path
|
||||
}
|
||||
}
|
||||
|
||||
pub fn zellij_exports(linker: &mut Linker<PluginEnv>) {
|
||||
linker
|
||||
.func_wrap("zellij", "host_run_plugin_command", host_run_plugin_command)
|
||||
@@ -663,6 +685,53 @@ fn host_run_plugin_command(mut caller: Caller<'_, PluginEnv>) {
|
||||
PluginCommand::GetSessionEnvironmentVariables => {
|
||||
get_session_environment_variables(env);
|
||||
},
|
||||
PluginCommand::OpenCommandPaneInNewTab(command_to_run, context) => {
|
||||
open_command_pane_in_new_tab(env, command_to_run, context);
|
||||
},
|
||||
PluginCommand::OpenPluginPaneInNewTab {
|
||||
plugin_url,
|
||||
configuration,
|
||||
context,
|
||||
} => {
|
||||
open_plugin_pane_in_new_tab(env, plugin_url, configuration, context);
|
||||
},
|
||||
PluginCommand::OpenEditorPaneInNewTab(file_to_open, context) => {
|
||||
open_editor_pane_in_new_tab(env, file_to_open, context);
|
||||
},
|
||||
PluginCommand::OpenCommandPaneInPlaceOfPaneId(
|
||||
pane_id,
|
||||
command_to_run,
|
||||
close_replaced_pane,
|
||||
context,
|
||||
) => open_command_pane_in_place_of_pane_id(
|
||||
env,
|
||||
pane_id,
|
||||
command_to_run,
|
||||
close_replaced_pane,
|
||||
context,
|
||||
),
|
||||
PluginCommand::OpenTerminalPaneInPlaceOfPaneId(
|
||||
pane_id,
|
||||
cwd,
|
||||
close_replaced_pane,
|
||||
) => open_terminal_pane_in_place_of_pane_id(
|
||||
env,
|
||||
pane_id,
|
||||
cwd,
|
||||
close_replaced_pane,
|
||||
),
|
||||
PluginCommand::OpenEditPaneInPlaceOfPaneId(
|
||||
pane_id,
|
||||
file_to_open,
|
||||
close_replaced_pane,
|
||||
context,
|
||||
) => open_edit_pane_in_place_of_pane_id(
|
||||
env,
|
||||
pane_id,
|
||||
file_to_open,
|
||||
close_replaced_pane,
|
||||
context,
|
||||
),
|
||||
},
|
||||
(PermissionStatus::Denied, permission) => {
|
||||
log::error!(
|
||||
@@ -914,6 +983,147 @@ fn get_session_environment_variables(env: &PluginEnv) {
|
||||
.non_fatal();
|
||||
}
|
||||
|
||||
fn open_command_pane_in_new_tab(
|
||||
env: &PluginEnv,
|
||||
command_to_run: CommandToRun,
|
||||
context: BTreeMap<String, String>,
|
||||
) {
|
||||
let command = command_to_run.path;
|
||||
let cwd = command_to_run
|
||||
.cwd
|
||||
.map(|cwd| translate_plugin_path(env, cwd))
|
||||
.or_else(|| Some(env.plugin_cwd.clone()));
|
||||
let args = command_to_run.args;
|
||||
let hold_on_close = true;
|
||||
let hold_on_start = false;
|
||||
let run_command_action = RunCommandAction {
|
||||
command,
|
||||
args,
|
||||
cwd,
|
||||
direction: None,
|
||||
hold_on_close,
|
||||
hold_on_start,
|
||||
originating_plugin: Some(OriginatingPlugin::new(
|
||||
env.plugin_id,
|
||||
env.client_id,
|
||||
context,
|
||||
)),
|
||||
use_terminal_title: false,
|
||||
};
|
||||
let initial_panes = Some(vec![CommandOrPlugin::Command(run_command_action)]);
|
||||
let action = Action::NewTab {
|
||||
tiled_layout: None,
|
||||
floating_layouts: vec![],
|
||||
swap_tiled_layouts: None,
|
||||
swap_floating_layouts: None,
|
||||
tab_name: None,
|
||||
should_change_focus_to_new_tab: true,
|
||||
cwd: None,
|
||||
initial_panes,
|
||||
first_pane_unblock_condition: None,
|
||||
};
|
||||
let error_msg = || format!("Failed to open command pane in new tab");
|
||||
let result = apply_action!(action, error_msg, env);
|
||||
|
||||
let tab_id = result.as_ref().and_then(|r| r.affected_tab_id);
|
||||
let pane_id = result
|
||||
.as_ref()
|
||||
.and_then(|r| r.affected_pane_id)
|
||||
.map(|p| p.into());
|
||||
let response =
|
||||
ProtobufOpenPaneInNewTabResponse::from(OpenPaneInNewTabResponse { tab_id, pane_id });
|
||||
wasi_write_object(env, &response.encode_to_vec())
|
||||
.with_context(|| format!("failed to write open_command_pane_in_new_tab response"))
|
||||
.non_fatal();
|
||||
}
|
||||
|
||||
fn open_plugin_pane_in_new_tab(
|
||||
env: &PluginEnv,
|
||||
plugin_url: String,
|
||||
configuration: BTreeMap<String, String>,
|
||||
context: BTreeMap<String, String>,
|
||||
) {
|
||||
let run_plugin_or_alias = RunPluginOrAlias::from_url(
|
||||
&plugin_url,
|
||||
&Some(configuration),
|
||||
None,
|
||||
Some(env.plugin_cwd.clone()),
|
||||
);
|
||||
let run_plugin_or_alias = match run_plugin_or_alias {
|
||||
Ok(r) => r,
|
||||
Err(e) => {
|
||||
log::error!("Failed to parse plugin url '{}': {}", plugin_url, e);
|
||||
let response =
|
||||
ProtobufOpenPaneInNewTabResponse::from(OpenPaneInNewTabResponse::default());
|
||||
wasi_write_object(env, &response.encode_to_vec())
|
||||
.with_context(|| {
|
||||
format!("failed to write open_plugin_pane_in_new_tab error response")
|
||||
})
|
||||
.non_fatal();
|
||||
return;
|
||||
},
|
||||
};
|
||||
let _ = context; // context is not currently used for plugin panes
|
||||
let initial_panes = Some(vec![CommandOrPlugin::Plugin(run_plugin_or_alias)]);
|
||||
let action = Action::NewTab {
|
||||
tiled_layout: None,
|
||||
floating_layouts: vec![],
|
||||
swap_tiled_layouts: None,
|
||||
swap_floating_layouts: None,
|
||||
tab_name: None,
|
||||
should_change_focus_to_new_tab: true,
|
||||
cwd: None,
|
||||
initial_panes,
|
||||
first_pane_unblock_condition: None,
|
||||
};
|
||||
let error_msg = || format!("Failed to open plugin pane in new tab");
|
||||
let result = apply_action!(action, error_msg, env);
|
||||
|
||||
let tab_id = result.as_ref().and_then(|r| r.affected_tab_id);
|
||||
let pane_id = result
|
||||
.as_ref()
|
||||
.and_then(|r| r.affected_pane_id)
|
||||
.map(|p| p.into());
|
||||
let response =
|
||||
ProtobufOpenPaneInNewTabResponse::from(OpenPaneInNewTabResponse { tab_id, pane_id });
|
||||
wasi_write_object(env, &response.encode_to_vec())
|
||||
.with_context(|| format!("failed to write open_plugin_pane_in_new_tab response"))
|
||||
.non_fatal();
|
||||
}
|
||||
|
||||
fn open_editor_pane_in_new_tab(
|
||||
env: &PluginEnv,
|
||||
file_to_open: FileToOpen,
|
||||
context: BTreeMap<String, String>,
|
||||
) {
|
||||
let _ = context; // context is not currently used for editor panes in new tab
|
||||
let initial_panes = Some(vec![CommandOrPlugin::File(file_to_open)]);
|
||||
let action = Action::NewTab {
|
||||
tiled_layout: None,
|
||||
floating_layouts: vec![],
|
||||
swap_tiled_layouts: None,
|
||||
swap_floating_layouts: None,
|
||||
tab_name: None,
|
||||
should_change_focus_to_new_tab: true,
|
||||
cwd: None,
|
||||
initial_panes,
|
||||
first_pane_unblock_condition: None,
|
||||
};
|
||||
let error_msg = || format!("Failed to open editor pane in new tab");
|
||||
let result = apply_action!(action, error_msg, env);
|
||||
|
||||
let tab_id = result.as_ref().and_then(|r| r.affected_tab_id);
|
||||
let pane_id = result
|
||||
.as_ref()
|
||||
.and_then(|r| r.affected_pane_id)
|
||||
.map(|p| p.into());
|
||||
let response =
|
||||
ProtobufOpenPaneInNewTabResponse::from(OpenPaneInNewTabResponse { tab_id, pane_id });
|
||||
wasi_write_object(env, &response.encode_to_vec())
|
||||
.with_context(|| format!("failed to write open_editor_pane_in_new_tab response"))
|
||||
.non_fatal();
|
||||
}
|
||||
|
||||
fn get_focused_pane_info(env: &PluginEnv) {
|
||||
use crossbeam::channel::RecvTimeoutError;
|
||||
use std::time::Duration;
|
||||
@@ -1082,10 +1292,10 @@ fn open_file(env: &PluginEnv, file_to_open: FileToOpen, context: BTreeMap<String
|
||||
let floating = false;
|
||||
let in_place = false;
|
||||
let start_suppressed = false;
|
||||
let path = env.plugin_cwd.join(file_to_open.path);
|
||||
let path = translate_plugin_path(env, file_to_open.path);
|
||||
let cwd = file_to_open
|
||||
.cwd
|
||||
.map(|cwd| env.plugin_cwd.join(cwd))
|
||||
.map(|cwd| translate_plugin_path(env, cwd))
|
||||
.or_else(|| Some(env.plugin_cwd.clone()));
|
||||
let action = Action::EditFile {
|
||||
payload: OpenFilePayload::new(path, file_to_open.line_number, cwd).with_originating_plugin(
|
||||
@@ -1183,10 +1393,10 @@ fn open_file_floating(
|
||||
let floating = true;
|
||||
let in_place = false;
|
||||
let start_suppressed = false;
|
||||
let path = env.plugin_cwd.join(file_to_open.path);
|
||||
let path = translate_plugin_path(env, file_to_open.path);
|
||||
let cwd = file_to_open
|
||||
.cwd
|
||||
.map(|cwd| env.plugin_cwd.join(cwd))
|
||||
.map(|cwd| translate_plugin_path(env, cwd))
|
||||
.or_else(|| Some(env.plugin_cwd.clone()));
|
||||
let action = Action::EditFile {
|
||||
payload: OpenFilePayload::new(path, file_to_open.line_number, cwd).with_originating_plugin(
|
||||
@@ -1221,10 +1431,10 @@ fn open_file_in_place(
|
||||
let floating = false;
|
||||
let in_place = true;
|
||||
let start_suppressed = false;
|
||||
let path = env.plugin_cwd.join(file_to_open.path);
|
||||
let path = translate_plugin_path(env, file_to_open.path);
|
||||
let cwd = file_to_open
|
||||
.cwd
|
||||
.map(|cwd| env.plugin_cwd.join(cwd))
|
||||
.map(|cwd| translate_plugin_path(env, cwd))
|
||||
.or_else(|| Some(env.plugin_cwd.clone()));
|
||||
|
||||
let action = Action::EditFile {
|
||||
@@ -1258,9 +1468,9 @@ fn open_file_near_plugin(
|
||||
) {
|
||||
let cwd = file_to_open
|
||||
.cwd
|
||||
.map(|cwd| env.plugin_cwd.join(cwd))
|
||||
.map(|cwd| translate_plugin_path(env, cwd))
|
||||
.or_else(|| Some(env.plugin_cwd.clone()));
|
||||
let path = env.plugin_cwd.join(file_to_open.path);
|
||||
let path = translate_plugin_path(env, file_to_open.path);
|
||||
let open_file_payload =
|
||||
OpenFilePayload::new(path, file_to_open.line_number, cwd).with_originating_plugin(
|
||||
OriginatingPlugin::new(env.plugin_id, env.client_id, context),
|
||||
@@ -1301,9 +1511,9 @@ fn open_file_floating_near_plugin(
|
||||
) {
|
||||
let cwd = file_to_open
|
||||
.cwd
|
||||
.map(|cwd| env.plugin_cwd.join(cwd))
|
||||
.map(|cwd| translate_plugin_path(env, cwd))
|
||||
.or_else(|| Some(env.plugin_cwd.clone()));
|
||||
let path = env.plugin_cwd.join(file_to_open.path);
|
||||
let path = translate_plugin_path(env, file_to_open.path);
|
||||
let open_file_payload =
|
||||
OpenFilePayload::new(path, file_to_open.line_number, cwd).with_originating_plugin(
|
||||
OriginatingPlugin::new(env.plugin_id, env.client_id, context),
|
||||
@@ -1344,9 +1554,9 @@ fn open_file_in_place_of_plugin(
|
||||
) {
|
||||
let cwd = file_to_open
|
||||
.cwd
|
||||
.map(|cwd| env.plugin_cwd.join(cwd))
|
||||
.map(|cwd| translate_plugin_path(env, cwd))
|
||||
.or_else(|| Some(env.plugin_cwd.clone()));
|
||||
let path = env.plugin_cwd.join(file_to_open.path);
|
||||
let path = translate_plugin_path(env, file_to_open.path);
|
||||
let open_file_payload =
|
||||
OpenFilePayload::new(path, file_to_open.line_number, cwd).with_originating_plugin(
|
||||
OriginatingPlugin::new(env.plugin_id, env.client_id, context),
|
||||
@@ -1378,7 +1588,7 @@ fn open_file_in_place_of_plugin(
|
||||
|
||||
fn open_terminal(env: &PluginEnv, cwd: PathBuf) {
|
||||
let error_msg = || format!("failed to open file in plugin {}", env.name());
|
||||
let cwd = env.plugin_cwd.join(cwd);
|
||||
let cwd = translate_plugin_path(env, cwd);
|
||||
let mut default_shell = env.default_shell.clone().unwrap_or_else(|| {
|
||||
TerminalAction::RunCommand(RunCommand {
|
||||
command: env.path_to_default_shell.clone(),
|
||||
@@ -1411,7 +1621,7 @@ fn open_terminal(env: &PluginEnv, cwd: PathBuf) {
|
||||
}
|
||||
|
||||
fn open_terminal_near_plugin(env: &PluginEnv, cwd: PathBuf) {
|
||||
let cwd = env.plugin_cwd.join(cwd);
|
||||
let cwd = translate_plugin_path(env, cwd);
|
||||
let mut default_shell = env.default_shell.clone().unwrap_or_else(|| {
|
||||
TerminalAction::RunCommand(RunCommand {
|
||||
command: env.path_to_default_shell.clone(),
|
||||
@@ -1454,7 +1664,7 @@ fn open_terminal_floating(
|
||||
floating_pane_coordinates: Option<FloatingPaneCoordinates>,
|
||||
) {
|
||||
let error_msg = || format!("failed to open file in plugin {}", env.name());
|
||||
let cwd = env.plugin_cwd.join(cwd);
|
||||
let cwd = translate_plugin_path(env, cwd);
|
||||
let mut default_shell = env.default_shell.clone().unwrap_or_else(|| {
|
||||
TerminalAction::RunCommand(RunCommand {
|
||||
command: env.path_to_default_shell.clone(),
|
||||
@@ -1491,7 +1701,7 @@ fn open_terminal_floating_near_plugin(
|
||||
cwd: PathBuf,
|
||||
floating_pane_coordinates: Option<FloatingPaneCoordinates>,
|
||||
) {
|
||||
let cwd = env.plugin_cwd.join(cwd);
|
||||
let cwd = translate_plugin_path(env, cwd);
|
||||
let mut default_shell = env.default_shell.clone().unwrap_or_else(|| {
|
||||
TerminalAction::RunCommand(RunCommand {
|
||||
command: env.path_to_default_shell.clone(),
|
||||
@@ -1528,7 +1738,7 @@ fn open_terminal_floating_near_plugin(
|
||||
|
||||
fn open_terminal_in_place(env: &PluginEnv, cwd: PathBuf) {
|
||||
let error_msg = || format!("failed to open file in plugin {}", env.name());
|
||||
let cwd = env.plugin_cwd.join(cwd);
|
||||
let cwd = translate_plugin_path(env, cwd);
|
||||
let mut default_shell = env.default_shell.clone().unwrap_or_else(|| {
|
||||
TerminalAction::RunCommand(RunCommand {
|
||||
command: env.path_to_default_shell.clone(),
|
||||
@@ -1566,7 +1776,7 @@ fn open_terminal_in_place_of_plugin(
|
||||
cwd: PathBuf,
|
||||
close_plugin_after_replace: bool,
|
||||
) {
|
||||
let cwd = env.plugin_cwd.join(cwd);
|
||||
let cwd = translate_plugin_path(env, cwd);
|
||||
let mut default_shell = env.default_shell.clone().unwrap_or_else(|| {
|
||||
TerminalAction::RunCommand(RunCommand {
|
||||
command: env.path_to_default_shell.clone(),
|
||||
@@ -1608,7 +1818,9 @@ fn open_command_pane_in_place_of_plugin(
|
||||
context: BTreeMap<String, String>,
|
||||
) {
|
||||
let command = command_to_run.path;
|
||||
let cwd = command_to_run.cwd.map(|cwd| env.plugin_cwd.join(cwd));
|
||||
let cwd = command_to_run
|
||||
.cwd
|
||||
.map(|cwd| translate_plugin_path(env, cwd));
|
||||
let args = command_to_run.args;
|
||||
let direction = None;
|
||||
let hold_on_close = true;
|
||||
@@ -1655,6 +1867,144 @@ fn open_command_pane_in_place_of_plugin(
|
||||
.non_fatal();
|
||||
}
|
||||
|
||||
fn open_terminal_pane_in_place_of_pane_id(
|
||||
env: &PluginEnv,
|
||||
pane_id_to_replace: zellij_utils::data::PaneId,
|
||||
cwd: FileToOpen,
|
||||
close_replaced_pane: bool,
|
||||
) {
|
||||
let cwd_path = translate_plugin_path(env, cwd.path);
|
||||
let mut default_shell = env.default_shell.clone().unwrap_or_else(|| {
|
||||
TerminalAction::RunCommand(RunCommand {
|
||||
command: env.path_to_default_shell.clone(),
|
||||
use_terminal_title: true,
|
||||
..Default::default()
|
||||
})
|
||||
});
|
||||
default_shell.change_cwd(cwd_path);
|
||||
let name = None;
|
||||
|
||||
let (completion_tx, completion_rx) = oneshot::channel();
|
||||
let _ = env
|
||||
.senders
|
||||
.send_to_pty(PtyInstruction::SpawnInPlaceTerminal(
|
||||
Some(default_shell),
|
||||
name,
|
||||
close_replaced_pane,
|
||||
ClientTabIndexOrPaneId::PaneId(pane_id_to_replace.into()),
|
||||
Some(NotificationEnd::new(completion_tx)),
|
||||
));
|
||||
|
||||
let result = wait_for_action_completion(
|
||||
completion_rx,
|
||||
"open_terminal_pane_in_place_of_pane_id",
|
||||
false,
|
||||
);
|
||||
let pane_id: OpenTerminalPaneInPlaceOfPaneIdResponse =
|
||||
result.affected_pane_id.map(|p| p.into());
|
||||
|
||||
let response = ProtobufOpenTerminalPaneInPlaceOfPaneIdResponse::from(pane_id);
|
||||
wasi_write_object(env, &response.encode_to_vec())
|
||||
.with_context(|| format!("failed to write open_terminal_pane_in_place_of_pane_id response"))
|
||||
.non_fatal();
|
||||
}
|
||||
|
||||
fn open_command_pane_in_place_of_pane_id(
|
||||
env: &PluginEnv,
|
||||
pane_id_to_replace: zellij_utils::data::PaneId,
|
||||
command_to_run: CommandToRun,
|
||||
close_replaced_pane: bool,
|
||||
context: BTreeMap<String, String>,
|
||||
) {
|
||||
let command = command_to_run.path;
|
||||
let cwd = command_to_run
|
||||
.cwd
|
||||
.map(|cwd| translate_plugin_path(env, cwd));
|
||||
let args = command_to_run.args;
|
||||
let direction = None;
|
||||
let hold_on_close = true;
|
||||
let hold_on_start = false;
|
||||
let name = None;
|
||||
let use_terminal_title = false;
|
||||
let run_command_action = RunCommandAction {
|
||||
command,
|
||||
args,
|
||||
cwd,
|
||||
direction,
|
||||
hold_on_close,
|
||||
hold_on_start,
|
||||
originating_plugin: Some(OriginatingPlugin::new(
|
||||
env.plugin_id,
|
||||
env.client_id,
|
||||
context,
|
||||
)),
|
||||
use_terminal_title,
|
||||
};
|
||||
let run_cmd = TerminalAction::RunCommand(run_command_action.into());
|
||||
|
||||
let (completion_tx, completion_rx) = oneshot::channel();
|
||||
let _ = env
|
||||
.senders
|
||||
.send_to_pty(PtyInstruction::SpawnInPlaceTerminal(
|
||||
Some(run_cmd),
|
||||
name,
|
||||
close_replaced_pane,
|
||||
ClientTabIndexOrPaneId::PaneId(pane_id_to_replace.into()),
|
||||
Some(NotificationEnd::new(completion_tx)),
|
||||
));
|
||||
|
||||
let result = wait_for_action_completion(
|
||||
completion_rx,
|
||||
"open_command_pane_in_place_of_pane_id",
|
||||
false,
|
||||
);
|
||||
let pane_id: OpenCommandPaneInPlaceOfPaneIdResponse = result.affected_pane_id.map(|p| p.into());
|
||||
|
||||
let response = ProtobufOpenCommandPaneInPlaceOfPaneIdResponse::from(pane_id);
|
||||
wasi_write_object(env, &response.encode_to_vec())
|
||||
.with_context(|| format!("failed to write open_command_pane_in_place_of_pane_id response"))
|
||||
.non_fatal();
|
||||
}
|
||||
|
||||
fn open_edit_pane_in_place_of_pane_id(
|
||||
env: &PluginEnv,
|
||||
pane_id_to_replace: zellij_utils::data::PaneId,
|
||||
file_to_open: FileToOpen,
|
||||
close_replaced_pane: bool,
|
||||
context: BTreeMap<String, String>,
|
||||
) {
|
||||
let cwd = file_to_open
|
||||
.cwd
|
||||
.map(|cwd| translate_plugin_path(env, cwd))
|
||||
.or_else(|| Some(env.plugin_cwd.clone()));
|
||||
let path = translate_plugin_path(env, file_to_open.path);
|
||||
let open_file_payload =
|
||||
OpenFilePayload::new(path, file_to_open.line_number, cwd).with_originating_plugin(
|
||||
OriginatingPlugin::new(env.plugin_id, env.client_id, context),
|
||||
);
|
||||
let title = format!("Editing: {}", open_file_payload.path.display());
|
||||
let open_file = TerminalAction::OpenFile(open_file_payload);
|
||||
|
||||
let (completion_tx, completion_rx) = oneshot::channel();
|
||||
let pty_instr = PtyInstruction::SpawnInPlaceTerminal(
|
||||
Some(open_file),
|
||||
Some(title),
|
||||
close_replaced_pane,
|
||||
ClientTabIndexOrPaneId::PaneId(pane_id_to_replace.into()),
|
||||
Some(NotificationEnd::new(completion_tx)),
|
||||
);
|
||||
let _ = env.senders.send_to_pty(pty_instr);
|
||||
|
||||
let result =
|
||||
wait_for_action_completion(completion_rx, "open_edit_pane_in_place_of_pane_id", false);
|
||||
let pane_id: OpenEditPaneInPlaceOfPaneIdResponse = result.affected_pane_id.map(|p| p.into());
|
||||
|
||||
let response = ProtobufOpenEditPaneInPlaceOfPaneIdResponse::from(pane_id);
|
||||
wasi_write_object(env, &response.encode_to_vec())
|
||||
.with_context(|| format!("failed to write open_edit_pane_in_place_of_pane_id response"))
|
||||
.non_fatal();
|
||||
}
|
||||
|
||||
fn open_command_pane(
|
||||
env: &PluginEnv,
|
||||
command_to_run: CommandToRun,
|
||||
@@ -1662,7 +2012,9 @@ fn open_command_pane(
|
||||
) {
|
||||
let error_msg = || format!("failed to open command in plugin {}", env.name());
|
||||
let command = command_to_run.path;
|
||||
let cwd = command_to_run.cwd.map(|cwd| env.plugin_cwd.join(cwd));
|
||||
let cwd = command_to_run
|
||||
.cwd
|
||||
.map(|cwd| translate_plugin_path(env, cwd));
|
||||
let args = command_to_run.args;
|
||||
let direction = None;
|
||||
let hold_on_close = true;
|
||||
@@ -1709,7 +2061,9 @@ fn open_command_pane_near_plugin(
|
||||
context: BTreeMap<String, String>,
|
||||
) {
|
||||
let command = command_to_run.path;
|
||||
let cwd = command_to_run.cwd.map(|cwd| env.plugin_cwd.join(cwd));
|
||||
let cwd = command_to_run
|
||||
.cwd
|
||||
.map(|cwd| translate_plugin_path(env, cwd));
|
||||
let args = command_to_run.args;
|
||||
let direction = None;
|
||||
let hold_on_close = true;
|
||||
@@ -1766,7 +2120,9 @@ fn open_command_pane_floating(
|
||||
) {
|
||||
let error_msg = || format!("failed to open command in plugin {}", env.name());
|
||||
let command = command_to_run.path;
|
||||
let cwd = command_to_run.cwd.map(|cwd| env.plugin_cwd.join(cwd));
|
||||
let cwd = command_to_run
|
||||
.cwd
|
||||
.map(|cwd| translate_plugin_path(env, cwd));
|
||||
let args = command_to_run.args;
|
||||
let direction = None;
|
||||
let hold_on_close = true;
|
||||
@@ -1813,7 +2169,9 @@ fn open_command_pane_floating_near_plugin(
|
||||
context: BTreeMap<String, String>,
|
||||
) {
|
||||
let command = command_to_run.path;
|
||||
let cwd = command_to_run.cwd.map(|cwd| env.plugin_cwd.join(cwd));
|
||||
let cwd = command_to_run
|
||||
.cwd
|
||||
.map(|cwd| translate_plugin_path(env, cwd));
|
||||
let args = command_to_run.args;
|
||||
let direction = None;
|
||||
let hold_on_close = true;
|
||||
@@ -1871,7 +2229,9 @@ fn open_command_pane_in_place(
|
||||
) {
|
||||
let error_msg = || format!("failed to open command in plugin {}", env.name());
|
||||
let command = command_to_run.path;
|
||||
let cwd = command_to_run.cwd.map(|cwd| env.plugin_cwd.join(cwd));
|
||||
let cwd = command_to_run
|
||||
.cwd
|
||||
.map(|cwd| translate_plugin_path(env, cwd));
|
||||
let args = command_to_run.args;
|
||||
let direction = None;
|
||||
let hold_on_close = true;
|
||||
@@ -1920,7 +2280,7 @@ fn open_command_pane_background(
|
||||
let command = command_to_run.path;
|
||||
let cwd = command_to_run
|
||||
.cwd
|
||||
.map(|cwd| env.plugin_cwd.join(cwd))
|
||||
.map(|cwd| translate_plugin_path(env, cwd))
|
||||
.or_else(|| Some(env.plugin_cwd.clone()));
|
||||
let args = command_to_run.args;
|
||||
let direction = None;
|
||||
@@ -2060,7 +2420,7 @@ fn run_command(
|
||||
log::error!("Command cannot be empty");
|
||||
} else {
|
||||
let command = command_line.remove(0);
|
||||
let cwd = env.plugin_cwd.join(cwd);
|
||||
let cwd = translate_plugin_path(env, cwd);
|
||||
let _ = env
|
||||
.senders
|
||||
.send_to_background_jobs(BackgroundJob::RunCommand(
|
||||
@@ -4365,13 +4725,16 @@ fn replace_pane_with_existing_pane(
|
||||
existing_pane: PaneId,
|
||||
suppress_replaced_pane: bool,
|
||||
) {
|
||||
let (completion_tx, completion_rx) = oneshot::channel();
|
||||
let _ = env
|
||||
.senders
|
||||
.send_to_screen(ScreenInstruction::ReplacePaneWithExistingPane(
|
||||
pane_to_replace,
|
||||
existing_pane,
|
||||
suppress_replaced_pane,
|
||||
Some(NotificationEnd::new(completion_tx)),
|
||||
));
|
||||
let _ = wait_for_action_completion(completion_rx, "replace_pane_with_existing_pane", false);
|
||||
}
|
||||
|
||||
fn override_layout(
|
||||
@@ -4490,14 +4853,18 @@ fn check_command_permission(
|
||||
| PluginCommand::OpenFileNearPlugin(..)
|
||||
| PluginCommand::OpenFileFloatingNearPlugin(..)
|
||||
| PluginCommand::OpenFileInPlaceOfPlugin(..)
|
||||
| PluginCommand::OpenFileInPlace(..) => PermissionType::OpenFiles,
|
||||
| PluginCommand::OpenFileInPlace(..)
|
||||
| PluginCommand::OpenEditPaneInPlaceOfPaneId(..) => PermissionType::OpenFiles,
|
||||
PluginCommand::OpenTerminal(..)
|
||||
| PluginCommand::OpenTerminalNearPlugin(..)
|
||||
| PluginCommand::StartOrReloadPlugin(..)
|
||||
| PluginCommand::OpenTerminalFloating(..)
|
||||
| PluginCommand::OpenTerminalFloatingNearPlugin(..)
|
||||
| PluginCommand::OpenTerminalInPlace(..)
|
||||
| PluginCommand::OpenTerminalInPlaceOfPlugin(..) => PermissionType::OpenTerminalsOrPlugins,
|
||||
| PluginCommand::OpenTerminalInPlaceOfPlugin(..)
|
||||
| PluginCommand::OpenTerminalPaneInPlaceOfPaneId(..) => {
|
||||
PermissionType::OpenTerminalsOrPlugins
|
||||
},
|
||||
PluginCommand::OpenCommandPane(..)
|
||||
| PluginCommand::OpenCommandPaneNearPlugin(..)
|
||||
| PluginCommand::OpenCommandPaneFloating(..)
|
||||
@@ -4505,6 +4872,7 @@ fn check_command_permission(
|
||||
| PluginCommand::OpenCommandPaneInPlace(..)
|
||||
| PluginCommand::OpenCommandPaneInPlaceOfPlugin(..)
|
||||
| PluginCommand::OpenCommandPaneBackground(..)
|
||||
| PluginCommand::OpenCommandPaneInPlaceOfPaneId(..)
|
||||
| PluginCommand::RunCommand(..)
|
||||
| PluginCommand::ExecCmd(..) => PermissionType::RunCommands,
|
||||
PluginCommand::WebRequest(..) => PermissionType::WebAccess,
|
||||
@@ -4648,6 +5016,9 @@ fn check_command_permission(
|
||||
PluginCommand::GetSessionEnvironmentVariables => {
|
||||
PermissionType::ReadSessionEnvironmentVariables
|
||||
},
|
||||
PluginCommand::OpenCommandPaneInNewTab(..)
|
||||
| PluginCommand::OpenPluginPaneInNewTab { .. }
|
||||
| PluginCommand::OpenEditorPaneInNewTab(..) => PermissionType::ChangeApplicationState,
|
||||
_ => return (PermissionStatus::Granted, None),
|
||||
};
|
||||
|
||||
|
||||
@@ -1164,6 +1164,15 @@ impl Pty {
|
||||
log::warn!("More initial_panes provided than empty slots available");
|
||||
break;
|
||||
}
|
||||
} else if let CommandOrPlugin::File(file_to_open) = initial_pane {
|
||||
if !layout.replace_next_empty_slot_with_run(Run::EditFile(
|
||||
file_to_open.path.clone(),
|
||||
file_to_open.line_number,
|
||||
file_to_open.cwd.clone(),
|
||||
)) {
|
||||
log::warn!("More initial_panes provided than empty slots available");
|
||||
break;
|
||||
}
|
||||
}
|
||||
// Skip CommandOrPlugin::Plugin entries (already handled by plugin thread)
|
||||
}
|
||||
@@ -1556,6 +1565,15 @@ impl Pty {
|
||||
));
|
||||
}
|
||||
});
|
||||
|
||||
let originating_plugin = run_instruction.as_ref().and_then(|r| {
|
||||
if let Run::Command(run_command) = r {
|
||||
run_command.originating_plugin.clone()
|
||||
} else {
|
||||
None
|
||||
}
|
||||
});
|
||||
|
||||
match run_instruction {
|
||||
Some(Run::Command(mut command)) => {
|
||||
let starts_held = command.hold_on_start;
|
||||
@@ -1563,6 +1581,21 @@ impl Pty {
|
||||
let quit_cb = Box::new({
|
||||
let senders = self.bus.senders.clone();
|
||||
move |pane_id, exit_status, command| {
|
||||
if let PaneId::Terminal(terminal_pane_id) = pane_id {
|
||||
if let Some(originating_plugin) = originating_plugin.as_ref() {
|
||||
let update_event = Event::CommandPaneExited(
|
||||
terminal_pane_id,
|
||||
exit_status,
|
||||
originating_plugin.context.clone(),
|
||||
);
|
||||
let _ = senders.send_to_plugin(PluginInstruction::Update(vec![(
|
||||
Some(originating_plugin.plugin_id),
|
||||
Some(originating_plugin.client_id),
|
||||
update_event,
|
||||
)]));
|
||||
}
|
||||
}
|
||||
|
||||
if hold_on_close {
|
||||
let _ = senders.send_to_screen(ScreenInstruction::HoldPane(
|
||||
pane_id,
|
||||
|
||||
@@ -640,7 +640,7 @@ pub enum ScreenInstruction {
|
||||
SetMouseSelectionSupport(PaneId, bool),
|
||||
InterceptKeyPresses(PluginId, ClientId),
|
||||
ClearKeyPressesIntercepts(ClientId),
|
||||
ReplacePaneWithExistingPane(PaneId, PaneId, bool), // bool -> suppress_replaced_pane
|
||||
ReplacePaneWithExistingPane(PaneId, PaneId, bool, Option<NotificationEnd>), // bool -> suppress_replaced_pane
|
||||
AddWatcherClient(ClientId, Size),
|
||||
RemoveWatcherClient(ClientId),
|
||||
SetFollowedClient(ClientId),
|
||||
@@ -3294,6 +3294,7 @@ impl Screen {
|
||||
pane_id_to_replace: PaneId,
|
||||
pane_id_of_existing_pane: PaneId,
|
||||
suppress_replaced_pane: bool,
|
||||
_completion_tx: Option<NotificationEnd>, // ends here
|
||||
) {
|
||||
let Some(tab_index_of_pane_id_to_replace) = self
|
||||
.tabs
|
||||
@@ -3301,7 +3302,10 @@ impl Screen {
|
||||
.find(|(_tab_index, tab)| tab.has_pane_with_pid(&pane_id_to_replace))
|
||||
.map(|(_tab_index, tab)| tab.position)
|
||||
else {
|
||||
log::error!("Could not find tab");
|
||||
log::error!(
|
||||
"Could not find tab with pane_id: {:?} to replace",
|
||||
pane_id_to_replace
|
||||
);
|
||||
return;
|
||||
};
|
||||
let Some(tab_index_of_existing_pane) = self
|
||||
@@ -3310,7 +3314,10 @@ impl Screen {
|
||||
.find(|(_tab_index, tab)| tab.has_pane_with_pid(&pane_id_of_existing_pane))
|
||||
.map(|(_tab_index, tab)| tab.position)
|
||||
else {
|
||||
log::error!("Could not find tab");
|
||||
log::error!(
|
||||
"Could not find tab with pane_id: {:?} to be replaced by",
|
||||
pane_id_of_existing_pane
|
||||
);
|
||||
return;
|
||||
};
|
||||
let Some(extracted_pane_from_other_tab) = self
|
||||
@@ -5220,6 +5227,12 @@ pub(crate) fn screen_thread_main(
|
||||
completion_tx
|
||||
.as_mut()
|
||||
.map(|c| c.set_affected_pane_id(PaneId::Terminal(first_terminal_pane.0)));
|
||||
} else if let Some(plugin_id) =
|
||||
new_plugin_ids.values().next().and_then(|v| v.first())
|
||||
{
|
||||
completion_tx
|
||||
.as_mut()
|
||||
.map(|c| c.set_affected_pane_id(PaneId::Plugin(*plugin_id)));
|
||||
}
|
||||
// Set the affected tab ID for plugin API return value
|
||||
completion_tx
|
||||
@@ -7286,10 +7299,12 @@ pub(crate) fn screen_thread_main(
|
||||
old_pane_id,
|
||||
new_pane_id,
|
||||
suppress_replaced_pane,
|
||||
completion_tx,
|
||||
) => screen.replace_pane_with_existing_pane(
|
||||
old_pane_id,
|
||||
new_pane_id,
|
||||
suppress_replaced_pane,
|
||||
completion_tx,
|
||||
),
|
||||
ScreenInstruction::AddWatcherClient(client_id, size) => {
|
||||
screen
|
||||
|
||||
+198
-13
@@ -14,26 +14,27 @@ use zellij_utils::plugin_api::plugin_command::{
|
||||
dump_layout_response, dump_session_layout_response, get_focused_pane_info_response,
|
||||
get_pane_cwd_response, get_pane_running_command_response, parse_layout_response,
|
||||
CreateTokenResponse, ListTokensResponse, ProtobufBreakPanesToNewTabResponse,
|
||||
ProtobufBreakPanesToTabWithIndexResponse, ProtobufCurrentSessionLastSavedTimeResponse,
|
||||
ProtobufDeleteLayoutResponse, ProtobufDumpLayoutResponse, ProtobufDumpSessionLayoutResponse,
|
||||
ProtobufEditLayoutResponse, ProtobufFocusOrCreateTabResponse,
|
||||
ProtobufGenerateRandomNameResponse, ProtobufGetFocusedPaneInfoResponse,
|
||||
ProtobufGetLayoutDirResponse, ProtobufGetPaneCwdResponse, ProtobufGetPaneInfoResponse,
|
||||
ProtobufGetPanePidResponse, ProtobufGetPaneRunningCommandResponse,
|
||||
ProtobufBreakPanesToTabWithIdResponse, ProtobufBreakPanesToTabWithIndexResponse,
|
||||
ProtobufCurrentSessionLastSavedTimeResponse, ProtobufDeleteLayoutResponse,
|
||||
ProtobufDumpLayoutResponse, ProtobufDumpSessionLayoutResponse, ProtobufEditLayoutResponse,
|
||||
ProtobufFocusOrCreateTabResponse, ProtobufGenerateRandomNameResponse,
|
||||
ProtobufGetFocusedPaneInfoResponse, ProtobufGetLayoutDirResponse, ProtobufGetPaneCwdResponse,
|
||||
ProtobufGetPaneInfoResponse, ProtobufGetPanePidResponse, ProtobufGetPaneRunningCommandResponse,
|
||||
ProtobufGetSessionEnvironmentVariablesResponse, ProtobufGetTabInfoResponse,
|
||||
ProtobufNewTabResponse, ProtobufNewTabsResponse, ProtobufOpenCommandPaneBackgroundResponse,
|
||||
ProtobufOpenCommandPaneFloatingNearPluginResponse, ProtobufOpenCommandPaneFloatingResponse,
|
||||
ProtobufOpenCommandPaneInPlaceOfPluginResponse, ProtobufOpenCommandPaneInPlaceResponse,
|
||||
ProtobufOpenCommandPaneNearPluginResponse, ProtobufOpenCommandPaneResponse,
|
||||
ProtobufOpenCommandPaneInPlaceOfPaneIdResponse, ProtobufOpenCommandPaneInPlaceOfPluginResponse,
|
||||
ProtobufOpenCommandPaneInPlaceResponse, ProtobufOpenCommandPaneNearPluginResponse,
|
||||
ProtobufOpenCommandPaneResponse, ProtobufOpenEditPaneInPlaceOfPaneIdResponse,
|
||||
ProtobufOpenFileFloatingNearPluginResponse, ProtobufOpenFileFloatingResponse,
|
||||
ProtobufOpenFileInPlaceOfPluginResponse, ProtobufOpenFileInPlaceResponse,
|
||||
ProtobufOpenFileNearPluginResponse, ProtobufOpenFileResponse,
|
||||
ProtobufOpenFileNearPluginResponse, ProtobufOpenFileResponse, ProtobufOpenPaneInNewTabResponse,
|
||||
ProtobufOpenTerminalFloatingNearPluginResponse, ProtobufOpenTerminalFloatingResponse,
|
||||
ProtobufOpenTerminalInPlaceOfPluginResponse, ProtobufOpenTerminalInPlaceResponse,
|
||||
ProtobufOpenTerminalNearPluginResponse, ProtobufOpenTerminalResponse,
|
||||
ProtobufParseLayoutResponse, ProtobufPluginCommand, ProtobufRenameLayoutResponse,
|
||||
ProtobufSaveLayoutResponse, ProtobufSaveSessionResponse, RenameWebTokenResponse,
|
||||
RevokeAllWebTokensResponse, RevokeTokenResponse,
|
||||
ProtobufOpenTerminalNearPluginResponse, ProtobufOpenTerminalPaneInPlaceOfPaneIdResponse,
|
||||
ProtobufOpenTerminalResponse, ProtobufParseLayoutResponse, ProtobufPluginCommand,
|
||||
ProtobufRenameLayoutResponse, ProtobufSaveLayoutResponse, ProtobufSaveSessionResponse,
|
||||
RenameWebTokenResponse, RevokeAllWebTokensResponse, RevokeTokenResponse,
|
||||
};
|
||||
use zellij_utils::plugin_api::plugin_ids::{ProtobufPluginIds, ProtobufZellijVersion};
|
||||
|
||||
@@ -692,6 +693,89 @@ pub fn open_command_pane_in_place_of_plugin(
|
||||
OpenCommandPaneInPlaceOfPluginResponse::try_from(response).unwrap()
|
||||
}
|
||||
|
||||
/// Opens a command pane in place of the pane identified by `pane_id`.
|
||||
/// Unlike `open_command_pane_in_place`, this targets an arbitrary pane by ID rather than the
|
||||
/// focused pane, and does not change focus. If `close_replaced_pane` is false, the replaced
|
||||
/// pane is suppressed and restored when the new pane closes; if true, it is permanently closed.
|
||||
/// Returns the `PaneId` of the newly opened pane, or `None` if the operation failed.
|
||||
pub fn open_command_pane_in_place_of_pane_id(
|
||||
pane_id: PaneId,
|
||||
command_to_run: CommandToRun,
|
||||
close_replaced_pane: bool,
|
||||
context: BTreeMap<String, String>,
|
||||
) -> Option<PaneId> {
|
||||
let plugin_command = PluginCommand::OpenCommandPaneInPlaceOfPaneId(
|
||||
pane_id,
|
||||
command_to_run,
|
||||
close_replaced_pane,
|
||||
context,
|
||||
);
|
||||
let protobuf_plugin_command: ProtobufPluginCommand = plugin_command.try_into().unwrap();
|
||||
object_to_stdout(&protobuf_plugin_command.encode_to_vec());
|
||||
unsafe { host_run_plugin_command() };
|
||||
|
||||
let response = ProtobufOpenCommandPaneInPlaceOfPaneIdResponse::decode(
|
||||
bytes_from_stdin().unwrap().as_slice(),
|
||||
)
|
||||
.unwrap();
|
||||
OpenCommandPaneInPlaceOfPaneIdResponse::try_from(response).unwrap()
|
||||
}
|
||||
|
||||
/// Opens a terminal pane in place of the pane identified by `pane_id`.
|
||||
/// Unlike `open_terminal_in_place`, this targets an arbitrary pane by ID rather than the
|
||||
/// focused pane, and does not change focus. If `close_replaced_pane` is false, the replaced
|
||||
/// pane is suppressed and restored when the new pane closes; if true, it is permanently closed.
|
||||
/// `cwd` sets the working directory for the new terminal. Returns the `PaneId` of the newly
|
||||
/// opened pane, or `None` if the operation failed.
|
||||
pub fn open_terminal_pane_in_place_of_pane_id<P: AsRef<Path>>(
|
||||
pane_id: PaneId,
|
||||
cwd: P,
|
||||
close_replaced_pane: bool,
|
||||
) -> Option<PaneId> {
|
||||
let file_to_open = FileToOpen {
|
||||
path: cwd.as_ref().to_path_buf(),
|
||||
..Default::default()
|
||||
};
|
||||
let plugin_command =
|
||||
PluginCommand::OpenTerminalPaneInPlaceOfPaneId(pane_id, file_to_open, close_replaced_pane);
|
||||
let protobuf_plugin_command: ProtobufPluginCommand = plugin_command.try_into().unwrap();
|
||||
object_to_stdout(&protobuf_plugin_command.encode_to_vec());
|
||||
unsafe { host_run_plugin_command() };
|
||||
|
||||
let response = ProtobufOpenTerminalPaneInPlaceOfPaneIdResponse::decode(
|
||||
bytes_from_stdin().unwrap().as_slice(),
|
||||
)
|
||||
.unwrap();
|
||||
OpenTerminalPaneInPlaceOfPaneIdResponse::try_from(response).unwrap()
|
||||
}
|
||||
|
||||
/// Opens an editor pane in place of the pane identified by `pane_id`.
|
||||
/// Unlike `open_file_in_place`, this targets an arbitrary pane by ID rather than the
|
||||
/// focused pane, and does not change focus. If `close_replaced_pane` is false, the replaced
|
||||
/// pane is suppressed and restored when the new pane closes; if true, it is permanently closed.
|
||||
/// Returns the `PaneId` of the newly opened pane, or `None` if the operation failed.
|
||||
pub fn open_edit_pane_in_place_of_pane_id(
|
||||
pane_id: PaneId,
|
||||
file_to_open: FileToOpen,
|
||||
close_replaced_pane: bool,
|
||||
context: BTreeMap<String, String>,
|
||||
) -> Option<PaneId> {
|
||||
let plugin_command = PluginCommand::OpenEditPaneInPlaceOfPaneId(
|
||||
pane_id,
|
||||
file_to_open,
|
||||
close_replaced_pane,
|
||||
context,
|
||||
);
|
||||
let protobuf_plugin_command: ProtobufPluginCommand = plugin_command.try_into().unwrap();
|
||||
object_to_stdout(&protobuf_plugin_command.encode_to_vec());
|
||||
unsafe { host_run_plugin_command() };
|
||||
|
||||
let response =
|
||||
ProtobufOpenEditPaneInPlaceOfPaneIdResponse::decode(bytes_from_stdin().unwrap().as_slice())
|
||||
.unwrap();
|
||||
OpenEditPaneInPlaceOfPaneIdResponse::try_from(response).unwrap()
|
||||
}
|
||||
|
||||
/// Open a new hidden (background) command pane with the specified command and args (this sort of pane allows the user to control the command, re-run it and see its exit status through the Zellij UI).
|
||||
pub fn open_command_pane_background(
|
||||
command_to_run: CommandToRun,
|
||||
@@ -871,6 +955,63 @@ where
|
||||
NewTabResponse::try_from(response).unwrap()
|
||||
}
|
||||
|
||||
/// Opens a new tab with a command pane running `command_to_run`.
|
||||
/// Returns `(tab_id, pane_id)` of the created tab and pane, or `None` if unavailable.
|
||||
pub fn open_command_pane_in_new_tab(
|
||||
command_to_run: CommandToRun,
|
||||
context: BTreeMap<String, String>,
|
||||
) -> (Option<usize>, Option<PaneId>) {
|
||||
let plugin_command = PluginCommand::OpenCommandPaneInNewTab(command_to_run, context);
|
||||
let protobuf_plugin_command: ProtobufPluginCommand = plugin_command.try_into().unwrap();
|
||||
object_to_stdout(&protobuf_plugin_command.encode_to_vec());
|
||||
unsafe { host_run_plugin_command() };
|
||||
|
||||
let response =
|
||||
ProtobufOpenPaneInNewTabResponse::decode(bytes_from_stdin().unwrap().as_slice()).unwrap();
|
||||
let result = OpenPaneInNewTabResponse::try_from(response).unwrap();
|
||||
(result.tab_id, result.pane_id)
|
||||
}
|
||||
|
||||
/// Opens a new tab with a plugin pane loaded from `plugin_url`.
|
||||
/// `plugin_url` can be a path (`file:/path/to/plugin.wasm`) or a named alias.
|
||||
/// Returns `(tab_id, pane_id)` of the created tab and pane.
|
||||
pub fn open_plugin_pane_in_new_tab(
|
||||
plugin_url: impl ToString,
|
||||
configuration: BTreeMap<String, String>,
|
||||
context: BTreeMap<String, String>,
|
||||
) -> (Option<usize>, Option<PaneId>) {
|
||||
let plugin_command = PluginCommand::OpenPluginPaneInNewTab {
|
||||
plugin_url: plugin_url.to_string(),
|
||||
configuration,
|
||||
context,
|
||||
};
|
||||
let protobuf_plugin_command: ProtobufPluginCommand = plugin_command.try_into().unwrap();
|
||||
object_to_stdout(&protobuf_plugin_command.encode_to_vec());
|
||||
unsafe { host_run_plugin_command() };
|
||||
|
||||
let response =
|
||||
ProtobufOpenPaneInNewTabResponse::decode(bytes_from_stdin().unwrap().as_slice()).unwrap();
|
||||
let result = OpenPaneInNewTabResponse::try_from(response).unwrap();
|
||||
(result.tab_id, result.pane_id)
|
||||
}
|
||||
|
||||
/// Opens a new tab with an editor pane for `file_to_open`.
|
||||
/// Returns `(tab_id, pane_id)` of the created tab and pane.
|
||||
pub fn open_editor_pane_in_new_tab(
|
||||
file_to_open: FileToOpen,
|
||||
context: BTreeMap<String, String>,
|
||||
) -> (Option<usize>, Option<PaneId>) {
|
||||
let plugin_command = PluginCommand::OpenEditorPaneInNewTab(file_to_open, context);
|
||||
let protobuf_plugin_command: ProtobufPluginCommand = plugin_command.try_into().unwrap();
|
||||
object_to_stdout(&protobuf_plugin_command.encode_to_vec());
|
||||
unsafe { host_run_plugin_command() };
|
||||
|
||||
let response =
|
||||
ProtobufOpenPaneInNewTabResponse::decode(bytes_from_stdin().unwrap().as_slice()).unwrap();
|
||||
let result = OpenPaneInNewTabResponse::try_from(response).unwrap();
|
||||
(result.tab_id, result.pane_id)
|
||||
}
|
||||
|
||||
/// Change focus to the next tab or loop back to the first
|
||||
pub fn go_to_next_tab() {
|
||||
let plugin_command = PluginCommand::GoToNextTab;
|
||||
@@ -1277,6 +1418,17 @@ where
|
||||
unsafe { host_run_plugin_command() };
|
||||
}
|
||||
|
||||
/// Changes the name (the title that appears in the UI) of the tab with the specified id.
|
||||
pub fn rename_tab_with_id<S: AsRef<str>>(tab_id: u64, new_name: S)
|
||||
where
|
||||
S: ToString,
|
||||
{
|
||||
let plugin_command = PluginCommand::RenameTabWithId(tab_id, new_name.to_string());
|
||||
let protobuf_plugin_command: ProtobufPluginCommand = plugin_command.try_into().unwrap();
|
||||
object_to_stdout(&protobuf_plugin_command.encode_to_vec());
|
||||
unsafe { host_run_plugin_command() };
|
||||
}
|
||||
|
||||
/// Switch to a session with the given name, create one if no name is given
|
||||
pub fn switch_session(name: Option<&str>) {
|
||||
let plugin_command = PluginCommand::SwitchSession(ConnectToSession {
|
||||
@@ -2048,6 +2200,18 @@ pub fn close_tab_with_index(tab_index: usize) {
|
||||
unsafe { host_run_plugin_command() };
|
||||
}
|
||||
|
||||
/// Close the tab with the given stable ID.
|
||||
///
|
||||
/// Unlike `close_tab_with_index`, this function identifies the tab by its stable
|
||||
/// `tab_id` rather than its display position, which may change as tabs are moved
|
||||
/// or closed. The tab_id can be obtained from `TabInfo.tab_id`.
|
||||
pub fn close_tab_with_id(tab_id: u64) {
|
||||
let plugin_command = PluginCommand::CloseTabWithId(tab_id);
|
||||
let protobuf_plugin_command: ProtobufPluginCommand = plugin_command.try_into().unwrap();
|
||||
object_to_stdout(&protobuf_plugin_command.encode_to_vec());
|
||||
unsafe { host_run_plugin_command() };
|
||||
}
|
||||
|
||||
/// Rename the specified pane
|
||||
pub fn rename_pane_with_id<S: AsRef<str>>(pane_id: PaneId, new_name: S)
|
||||
where
|
||||
@@ -2107,6 +2271,27 @@ pub fn break_panes_to_tab_with_index(
|
||||
BreakPanesToTabWithIndexResponse::try_from(response).unwrap()
|
||||
}
|
||||
|
||||
/// Move the pane ids to the tab with the specified id
|
||||
pub fn break_panes_to_tab_with_id(
|
||||
pane_ids: &[PaneId],
|
||||
tab_id: usize,
|
||||
should_change_focus_to_target_tab: bool,
|
||||
) -> Option<usize> {
|
||||
let plugin_command = PluginCommand::BreakPanesToTabWithId(
|
||||
pane_ids.to_vec(),
|
||||
tab_id as u64,
|
||||
should_change_focus_to_target_tab,
|
||||
);
|
||||
let protobuf_plugin_command: ProtobufPluginCommand = plugin_command.try_into().unwrap();
|
||||
object_to_stdout(&protobuf_plugin_command.encode_to_vec());
|
||||
unsafe { host_run_plugin_command() };
|
||||
|
||||
let response =
|
||||
ProtobufBreakPanesToTabWithIdResponse::decode(bytes_from_stdin().unwrap().as_slice())
|
||||
.unwrap();
|
||||
BreakPanesToTabWithIdResponse::try_from(response).unwrap()
|
||||
}
|
||||
|
||||
/// Reload an already-running in this session, optionally skipping the cache
|
||||
pub fn reload_plugin_with_id(plugin_id: u32) {
|
||||
let plugin_command = PluginCommand::ReloadPlugin(plugin_id);
|
||||
|
||||
@@ -220,8 +220,18 @@ pub struct LayoutConstraintFloatingPair {
|
||||
}
|
||||
#[allow(clippy::derive_partial_eq_without_eq)]
|
||||
#[derive(Clone, PartialEq, ::prost::Message)]
|
||||
pub struct CommandOrPluginFile {
|
||||
#[prost(string, tag="1")]
|
||||
pub path: ::prost::alloc::string::String,
|
||||
#[prost(int32, optional, tag="2")]
|
||||
pub line_number: ::core::option::Option<i32>,
|
||||
#[prost(string, optional, tag="3")]
|
||||
pub cwd: ::core::option::Option<::prost::alloc::string::String>,
|
||||
}
|
||||
#[allow(clippy::derive_partial_eq_without_eq)]
|
||||
#[derive(Clone, PartialEq, ::prost::Message)]
|
||||
pub struct CommandOrPlugin {
|
||||
#[prost(oneof="command_or_plugin::CommandOrPluginType", tags="1, 2")]
|
||||
#[prost(oneof="command_or_plugin::CommandOrPluginType", tags="1, 2, 3")]
|
||||
pub command_or_plugin_type: ::core::option::Option<command_or_plugin::CommandOrPluginType>,
|
||||
}
|
||||
/// Nested message and enum types in `CommandOrPlugin`.
|
||||
@@ -233,6 +243,8 @@ pub mod command_or_plugin {
|
||||
Command(super::RunCommandAction),
|
||||
#[prost(message, tag="2")]
|
||||
Plugin(super::RunPluginOrAlias),
|
||||
#[prost(message, tag="3")]
|
||||
File(super::CommandOrPluginFile),
|
||||
}
|
||||
}
|
||||
#[allow(clippy::derive_partial_eq_without_eq)]
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
pub struct PluginCommand {
|
||||
#[prost(enumeration="CommandName", tag="1")]
|
||||
pub name: i32,
|
||||
#[prost(oneof="plugin_command::Payload", tags="2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 91, 92, 93, 94, 95, 96, 97, 98, 99, 100, 101, 102, 103, 104, 105, 106, 107, 108, 109, 110, 111, 112, 113, 114, 119, 120, 121, 122, 123, 124, 125, 126, 127, 128, 129, 130, 131, 132, 133, 134, 135, 136, 137, 138, 139, 140, 141, 142, 143, 144, 145, 146, 147, 148")]
|
||||
#[prost(oneof="plugin_command::Payload", tags="2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 91, 92, 93, 94, 95, 96, 97, 98, 99, 100, 101, 102, 103, 104, 105, 106, 107, 108, 109, 110, 111, 112, 113, 114, 119, 120, 121, 122, 123, 124, 125, 126, 127, 128, 129, 130, 131, 132, 133, 134, 135, 136, 137, 138, 139, 140, 141, 142, 143, 144, 145, 146, 147, 148, 149, 150, 151, 152, 153, 154")]
|
||||
pub payload: ::core::option::Option<plugin_command::Payload>,
|
||||
}
|
||||
/// Nested message and enum types in `PluginCommand`.
|
||||
@@ -279,6 +279,18 @@ pub mod plugin_command {
|
||||
BreakPanesToTabWithIdPayload(super::BreakPanesToTabWithIdPayload),
|
||||
#[prost(message, tag="148")]
|
||||
GetSessionEnvironmentVariablesPayload(super::GetSessionEnvironmentVariablesPayload),
|
||||
#[prost(message, tag="149")]
|
||||
OpenCommandPaneInNewTabPayload(super::OpenCommandPanePayload),
|
||||
#[prost(message, tag="150")]
|
||||
OpenPluginPaneInNewTabPayload(super::OpenPluginPaneInNewTabPayload),
|
||||
#[prost(message, tag="151")]
|
||||
OpenEditorPaneInNewTabPayload(super::OpenFilePayload),
|
||||
#[prost(message, tag="152")]
|
||||
OpenCommandPaneInPlaceOfPaneIdPayload(super::OpenCommandPaneInPlaceOfPaneIdPayload),
|
||||
#[prost(message, tag="153")]
|
||||
OpenTerminalPaneInPlaceOfPaneIdPayload(super::OpenTerminalPaneInPlaceOfPaneIdPayload),
|
||||
#[prost(message, tag="154")]
|
||||
OpenEditPaneInPlaceOfPaneIdPayload(super::OpenEditPaneInPlaceOfPaneIdPayload),
|
||||
}
|
||||
}
|
||||
#[allow(clippy::derive_partial_eq_without_eq)]
|
||||
@@ -1636,6 +1648,76 @@ pub struct GetSessionEnvironmentVariablesResponse {
|
||||
#[prost(message, repeated, tag="1")]
|
||||
pub env_vars: ::prost::alloc::vec::Vec<EnvVariable>,
|
||||
}
|
||||
#[allow(clippy::derive_partial_eq_without_eq)]
|
||||
#[derive(Clone, PartialEq, ::prost::Message)]
|
||||
pub struct OpenPluginPaneInNewTabPayload {
|
||||
#[prost(string, tag="1")]
|
||||
pub plugin_url: ::prost::alloc::string::String,
|
||||
#[prost(map="string, string", tag="2")]
|
||||
pub configuration: ::std::collections::HashMap<::prost::alloc::string::String, ::prost::alloc::string::String>,
|
||||
#[prost(message, repeated, tag="3")]
|
||||
pub context: ::prost::alloc::vec::Vec<ContextItem>,
|
||||
}
|
||||
#[allow(clippy::derive_partial_eq_without_eq)]
|
||||
#[derive(Clone, PartialEq, ::prost::Message)]
|
||||
pub struct OpenPaneInNewTabResponse {
|
||||
#[prost(uint64, optional, tag="1")]
|
||||
pub tab_id: ::core::option::Option<u64>,
|
||||
#[prost(message, optional, tag="2")]
|
||||
pub pane_id: ::core::option::Option<PaneId>,
|
||||
}
|
||||
#[allow(clippy::derive_partial_eq_without_eq)]
|
||||
#[derive(Clone, PartialEq, ::prost::Message)]
|
||||
pub struct OpenCommandPaneInPlaceOfPaneIdPayload {
|
||||
#[prost(message, optional, tag="1")]
|
||||
pub pane_id_to_replace: ::core::option::Option<PaneId>,
|
||||
#[prost(message, optional, tag="2")]
|
||||
pub command_to_run: ::core::option::Option<super::command::Command>,
|
||||
#[prost(bool, tag="3")]
|
||||
pub close_replaced_pane: bool,
|
||||
#[prost(message, repeated, tag="4")]
|
||||
pub context: ::prost::alloc::vec::Vec<ContextItem>,
|
||||
}
|
||||
#[allow(clippy::derive_partial_eq_without_eq)]
|
||||
#[derive(Clone, PartialEq, ::prost::Message)]
|
||||
pub struct OpenTerminalPaneInPlaceOfPaneIdPayload {
|
||||
#[prost(message, optional, tag="1")]
|
||||
pub pane_id_to_replace: ::core::option::Option<PaneId>,
|
||||
#[prost(message, optional, tag="2")]
|
||||
pub cwd: ::core::option::Option<super::file::File>,
|
||||
#[prost(bool, tag="3")]
|
||||
pub close_replaced_pane: bool,
|
||||
}
|
||||
#[allow(clippy::derive_partial_eq_without_eq)]
|
||||
#[derive(Clone, PartialEq, ::prost::Message)]
|
||||
pub struct OpenEditPaneInPlaceOfPaneIdPayload {
|
||||
#[prost(message, optional, tag="1")]
|
||||
pub pane_id_to_replace: ::core::option::Option<PaneId>,
|
||||
#[prost(message, optional, tag="2")]
|
||||
pub file_to_open: ::core::option::Option<super::file::File>,
|
||||
#[prost(bool, tag="3")]
|
||||
pub close_replaced_pane: bool,
|
||||
#[prost(message, repeated, tag="4")]
|
||||
pub context: ::prost::alloc::vec::Vec<ContextItem>,
|
||||
}
|
||||
#[allow(clippy::derive_partial_eq_without_eq)]
|
||||
#[derive(Clone, PartialEq, ::prost::Message)]
|
||||
pub struct OpenCommandPaneInPlaceOfPaneIdResponse {
|
||||
#[prost(message, optional, tag="1")]
|
||||
pub pane_id: ::core::option::Option<PaneId>,
|
||||
}
|
||||
#[allow(clippy::derive_partial_eq_without_eq)]
|
||||
#[derive(Clone, PartialEq, ::prost::Message)]
|
||||
pub struct OpenTerminalPaneInPlaceOfPaneIdResponse {
|
||||
#[prost(message, optional, tag="1")]
|
||||
pub pane_id: ::core::option::Option<PaneId>,
|
||||
}
|
||||
#[allow(clippy::derive_partial_eq_without_eq)]
|
||||
#[derive(Clone, PartialEq, ::prost::Message)]
|
||||
pub struct OpenEditPaneInPlaceOfPaneIdResponse {
|
||||
#[prost(message, optional, tag="1")]
|
||||
pub pane_id: ::core::option::Option<PaneId>,
|
||||
}
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, ::prost::Enumeration)]
|
||||
#[repr(i32)]
|
||||
pub enum CommandName {
|
||||
@@ -1816,6 +1898,12 @@ pub enum CommandName {
|
||||
RenameTabWithId = 195,
|
||||
BreakPanesToTabWithId = 196,
|
||||
GetSessionEnvironmentVariables = 197,
|
||||
OpenCommandPaneInNewTab = 198,
|
||||
OpenPluginPaneInNewTab = 199,
|
||||
OpenEditorPaneInNewTab = 200,
|
||||
OpenCommandPaneInPlaceOfPaneId = 201,
|
||||
OpenTerminalPaneInPlaceOfPaneId = 202,
|
||||
OpenEditPaneInPlaceOfPaneId = 203,
|
||||
}
|
||||
impl CommandName {
|
||||
/// String value of the enum field names used in the ProtoBuf definition.
|
||||
@@ -2001,6 +2089,12 @@ impl CommandName {
|
||||
CommandName::RenameTabWithId => "RenameTabWithId",
|
||||
CommandName::BreakPanesToTabWithId => "BreakPanesToTabWithId",
|
||||
CommandName::GetSessionEnvironmentVariables => "GetSessionEnvironmentVariables",
|
||||
CommandName::OpenCommandPaneInNewTab => "OpenCommandPaneInNewTab",
|
||||
CommandName::OpenPluginPaneInNewTab => "OpenPluginPaneInNewTab",
|
||||
CommandName::OpenEditorPaneInNewTab => "OpenEditorPaneInNewTab",
|
||||
CommandName::OpenCommandPaneInPlaceOfPaneId => "OpenCommandPaneInPlaceOfPaneId",
|
||||
CommandName::OpenTerminalPaneInPlaceOfPaneId => "OpenTerminalPaneInPlaceOfPaneId",
|
||||
CommandName::OpenEditPaneInPlaceOfPaneId => "OpenEditPaneInPlaceOfPaneId",
|
||||
}
|
||||
}
|
||||
/// Creates an enum from field names used in the ProtoBuf definition.
|
||||
@@ -2183,6 +2277,12 @@ impl CommandName {
|
||||
"RenameTabWithId" => Some(Self::RenameTabWithId),
|
||||
"BreakPanesToTabWithId" => Some(Self::BreakPanesToTabWithId),
|
||||
"GetSessionEnvironmentVariables" => Some(Self::GetSessionEnvironmentVariables),
|
||||
"OpenCommandPaneInNewTab" => Some(Self::OpenCommandPaneInNewTab),
|
||||
"OpenPluginPaneInNewTab" => Some(Self::OpenPluginPaneInNewTab),
|
||||
"OpenEditorPaneInNewTab" => Some(Self::OpenEditorPaneInNewTab),
|
||||
"OpenCommandPaneInPlaceOfPaneId" => Some(Self::OpenCommandPaneInPlaceOfPaneId),
|
||||
"OpenTerminalPaneInPlaceOfPaneId" => Some(Self::OpenTerminalPaneInPlaceOfPaneId),
|
||||
"OpenEditPaneInPlaceOfPaneId" => Some(Self::OpenEditPaneInPlaceOfPaneId),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1492,8 +1492,18 @@ pub struct PluginAlias {
|
||||
}
|
||||
#[allow(clippy::derive_partial_eq_without_eq)]
|
||||
#[derive(Clone, PartialEq, ::prost::Message)]
|
||||
pub struct CommandOrPluginFile {
|
||||
#[prost(string, tag="1")]
|
||||
pub path: ::prost::alloc::string::String,
|
||||
#[prost(int32, optional, tag="2")]
|
||||
pub line_number: ::core::option::Option<i32>,
|
||||
#[prost(string, optional, tag="3")]
|
||||
pub cwd: ::core::option::Option<::prost::alloc::string::String>,
|
||||
}
|
||||
#[allow(clippy::derive_partial_eq_without_eq)]
|
||||
#[derive(Clone, PartialEq, ::prost::Message)]
|
||||
pub struct CommandOrPlugin {
|
||||
#[prost(oneof="command_or_plugin::CommandOrPluginType", tags="1, 2")]
|
||||
#[prost(oneof="command_or_plugin::CommandOrPluginType", tags="1, 2, 3")]
|
||||
pub command_or_plugin_type: ::core::option::Option<command_or_plugin::CommandOrPluginType>,
|
||||
}
|
||||
/// Nested message and enum types in `CommandOrPlugin`.
|
||||
@@ -1505,6 +1515,8 @@ pub mod command_or_plugin {
|
||||
Command(super::RunCommandAction),
|
||||
#[prost(message, tag="2")]
|
||||
Plugin(super::RunPluginOrAlias),
|
||||
#[prost(message, tag="3")]
|
||||
File(super::CommandOrPluginFile),
|
||||
}
|
||||
}
|
||||
#[allow(clippy::derive_partial_eq_without_eq)]
|
||||
|
||||
@@ -596,6 +596,34 @@ tail -f /tmp/my-live-logfile | zellij pipe --name logs --plugin https://example.
|
||||
#[clap(short('c'), long, value_parser, display_order(4))]
|
||||
plugin_configuration: Option<PluginUserConfiguration>,
|
||||
},
|
||||
/// Send commands to the sequence plugin (sugar for: zellij pipe --plugin zellij:sequence)
|
||||
#[clap(
|
||||
visible_alias = "seq",
|
||||
override_usage(
|
||||
r#"
|
||||
zellij sequence [OPTIONS] [--] <COMMANDS>
|
||||
|
||||
* Run a sequence of commands:
|
||||
|
||||
zellij sequence -- 'echo hello && echo world'
|
||||
|
||||
* Pipe commands from STDIN:
|
||||
|
||||
echo 'echo hello && echo world' | zellij sequence
|
||||
|
||||
* Wait for the sequence to complete before returning:
|
||||
|
||||
zellij sequence --blocking -- 'echo hello && echo world'
|
||||
"#
|
||||
)
|
||||
)]
|
||||
Sequence {
|
||||
/// The commands to run (if blank, will listen to STDIN)
|
||||
payload: Option<String>,
|
||||
/// Block until the sequence finishes before exiting
|
||||
#[clap(short, long, value_parser, takes_value(false), default_value("false"))]
|
||||
blocking: bool,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Debug, Subcommand, Clone, Serialize, Deserialize)]
|
||||
|
||||
@@ -903,10 +903,17 @@ message PluginAlias {
|
||||
optional RunPlugin run_plugin = 4;
|
||||
}
|
||||
|
||||
message CommandOrPluginFile {
|
||||
string path = 1;
|
||||
optional int32 line_number = 2;
|
||||
optional string cwd = 3;
|
||||
}
|
||||
|
||||
message CommandOrPlugin {
|
||||
oneof command_or_plugin_type {
|
||||
RunCommandAction command = 1;
|
||||
RunPluginOrAlias plugin = 2;
|
||||
CommandOrPluginFile file = 3;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -54,6 +54,7 @@ impl UnblockCondition {
|
||||
pub enum CommandOrPlugin {
|
||||
Command(RunCommandAction),
|
||||
Plugin(RunPluginOrAlias),
|
||||
File(FileToOpen), // open file in configured editor
|
||||
}
|
||||
|
||||
impl CommandOrPlugin {
|
||||
@@ -2622,7 +2623,7 @@ pub enum PermissionStatus {
|
||||
Denied,
|
||||
}
|
||||
|
||||
#[derive(Debug, Default, Clone)]
|
||||
#[derive(Debug, Default, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct FileToOpen {
|
||||
pub path: PathBuf,
|
||||
pub line_number: Option<usize>,
|
||||
@@ -3450,6 +3451,23 @@ pub enum PluginCommand {
|
||||
GetPaneInfo(PaneId),
|
||||
GetTabInfo(usize), // tab_id
|
||||
GetSessionEnvironmentVariables,
|
||||
OpenCommandPaneInNewTab(CommandToRun, Context),
|
||||
OpenPluginPaneInNewTab {
|
||||
plugin_url: String,
|
||||
configuration: BTreeMap<String, String>,
|
||||
context: Context,
|
||||
},
|
||||
OpenEditorPaneInNewTab(FileToOpen, Context),
|
||||
OpenCommandPaneInPlaceOfPaneId(PaneId, CommandToRun, bool, Context), // bool = close_replaced_pane
|
||||
OpenTerminalPaneInPlaceOfPaneId(PaneId, FileToOpen, bool),
|
||||
OpenEditPaneInPlaceOfPaneId(PaneId, FileToOpen, bool, Context),
|
||||
}
|
||||
|
||||
// Response type for plugin API methods that open a pane in a new tab
|
||||
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
|
||||
pub struct OpenPaneInNewTabResponse {
|
||||
pub tab_id: Option<usize>,
|
||||
pub pane_id: Option<PaneId>,
|
||||
}
|
||||
|
||||
// Response types for plugin API methods that create tabs
|
||||
@@ -3482,3 +3500,6 @@ pub type OpenCommandPaneNearPluginResponse = Option<PaneId>;
|
||||
pub type OpenCommandPaneFloatingNearPluginResponse = Option<PaneId>;
|
||||
pub type OpenCommandPaneInPlaceOfPluginResponse = Option<PaneId>;
|
||||
pub type OpenCommandPaneBackgroundResponse = Option<PaneId>;
|
||||
pub type OpenCommandPaneInPlaceOfPaneIdResponse = Option<PaneId>;
|
||||
pub type OpenTerminalPaneInPlaceOfPaneIdResponse = Option<PaneId>;
|
||||
pub type OpenEditPaneInPlaceOfPaneIdResponse = Option<PaneId>;
|
||||
|
||||
@@ -3276,6 +3276,7 @@ impl From<crate::data::CommandOrPlugin>
|
||||
{
|
||||
fn from(cmd_or_plugin: crate::data::CommandOrPlugin) -> Self {
|
||||
use crate::client_server_contract::client_server_contract::command_or_plugin::CommandOrPluginType;
|
||||
use crate::client_server_contract::client_server_contract::CommandOrPluginFile;
|
||||
match cmd_or_plugin {
|
||||
crate::data::CommandOrPlugin::Command(cmd) => Self {
|
||||
command_or_plugin_type: Some(CommandOrPluginType::Command(cmd.into())),
|
||||
@@ -3283,6 +3284,13 @@ impl From<crate::data::CommandOrPlugin>
|
||||
crate::data::CommandOrPlugin::Plugin(plugin) => Self {
|
||||
command_or_plugin_type: Some(CommandOrPluginType::Plugin(plugin.into())),
|
||||
},
|
||||
crate::data::CommandOrPlugin::File(f) => Self {
|
||||
command_or_plugin_type: Some(CommandOrPluginType::File(CommandOrPluginFile {
|
||||
path: f.path.display().to_string(),
|
||||
line_number: f.line_number.map(|n| n as i32),
|
||||
cwd: f.cwd.map(|c| c.display().to_string()),
|
||||
})),
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -3307,6 +3315,13 @@ impl TryFrom<crate::client_server_contract::client_server_contract::CommandOrPlu
|
||||
CommandOrPluginType::Plugin(plugin) => {
|
||||
Ok(crate::data::CommandOrPlugin::Plugin(plugin.try_into()?))
|
||||
},
|
||||
CommandOrPluginType::File(f) => Ok(crate::data::CommandOrPlugin::File(
|
||||
crate::data::FileToOpen {
|
||||
path: std::path::PathBuf::from(&f.path),
|
||||
line_number: f.line_number.map(|n| n as usize),
|
||||
cwd: f.cwd.map(std::path::PathBuf::from),
|
||||
},
|
||||
)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -144,10 +144,17 @@ message LayoutConstraintFloatingPair {
|
||||
repeated FloatingPaneLayout layouts = 2;
|
||||
}
|
||||
|
||||
message CommandOrPluginFile {
|
||||
string path = 1;
|
||||
optional int32 line_number = 2;
|
||||
optional string cwd = 3;
|
||||
}
|
||||
|
||||
message CommandOrPlugin {
|
||||
oneof command_or_plugin_type {
|
||||
RunCommandAction command = 1;
|
||||
RunPluginOrAlias plugin = 2;
|
||||
CommandOrPluginFile file = 3;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -2724,6 +2724,13 @@ impl TryFrom<ProtobufCommandOrPlugin> for CommandOrPlugin {
|
||||
Some(CommandOrPluginType::Plugin(plugin)) => {
|
||||
Ok(CommandOrPlugin::Plugin(plugin.try_into()?))
|
||||
},
|
||||
Some(CommandOrPluginType::File(f)) => {
|
||||
Ok(CommandOrPlugin::File(crate::data::FileToOpen {
|
||||
path: std::path::PathBuf::from(&f.path),
|
||||
line_number: f.line_number.map(|n| n as usize),
|
||||
cwd: f.cwd.map(std::path::PathBuf::from),
|
||||
}))
|
||||
},
|
||||
None => Err("CommandOrPlugin must have command_or_plugin_type"),
|
||||
}
|
||||
}
|
||||
@@ -2733,11 +2740,17 @@ impl TryFrom<CommandOrPlugin> for ProtobufCommandOrPlugin {
|
||||
type Error = &'static str;
|
||||
fn try_from(internal: CommandOrPlugin) -> Result<Self, Self::Error> {
|
||||
use super::generated_api::api::action::command_or_plugin::CommandOrPluginType;
|
||||
use super::generated_api::api::action::CommandOrPluginFile;
|
||||
let command_or_plugin_type = match internal {
|
||||
CommandOrPlugin::Command(cmd) => Some(CommandOrPluginType::Command(cmd.try_into()?)),
|
||||
CommandOrPlugin::Plugin(plugin) => {
|
||||
Some(CommandOrPluginType::Plugin(plugin.try_into()?))
|
||||
},
|
||||
CommandOrPlugin::File(f) => Some(CommandOrPluginType::File(CommandOrPluginFile {
|
||||
path: f.path.display().to_string(),
|
||||
line_number: f.line_number.map(|n| n as i32),
|
||||
cwd: f.cwd.map(|c| c.display().to_string()),
|
||||
})),
|
||||
};
|
||||
Ok(ProtobufCommandOrPlugin {
|
||||
command_or_plugin_type,
|
||||
|
||||
@@ -190,6 +190,12 @@ enum CommandName {
|
||||
RenameTabWithId = 195;
|
||||
BreakPanesToTabWithId = 196;
|
||||
GetSessionEnvironmentVariables = 197;
|
||||
OpenCommandPaneInNewTab = 198;
|
||||
OpenPluginPaneInNewTab = 199;
|
||||
OpenEditorPaneInNewTab = 200;
|
||||
OpenCommandPaneInPlaceOfPaneId = 201;
|
||||
OpenTerminalPaneInPlaceOfPaneId = 202;
|
||||
OpenEditPaneInPlaceOfPaneId = 203;
|
||||
}
|
||||
|
||||
message PluginCommand {
|
||||
@@ -329,6 +335,12 @@ message PluginCommand {
|
||||
RenameTabWithIdPayload rename_tab_with_id_payload = 146;
|
||||
BreakPanesToTabWithIdPayload break_panes_to_tab_with_id_payload = 147;
|
||||
GetSessionEnvironmentVariablesPayload get_session_environment_variables_payload = 148;
|
||||
OpenCommandPanePayload open_command_pane_in_new_tab_payload = 149;
|
||||
OpenPluginPaneInNewTabPayload open_plugin_pane_in_new_tab_payload = 150;
|
||||
OpenFilePayload open_editor_pane_in_new_tab_payload = 151;
|
||||
OpenCommandPaneInPlaceOfPaneIdPayload open_command_pane_in_place_of_pane_id_payload = 152;
|
||||
OpenTerminalPaneInPlaceOfPaneIdPayload open_terminal_pane_in_place_of_pane_id_payload = 153;
|
||||
OpenEditPaneInPlaceOfPaneIdPayload open_edit_pane_in_place_of_pane_id_payload = 154;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1131,3 +1143,46 @@ message GetSessionEnvironmentVariablesPayload {}
|
||||
message GetSessionEnvironmentVariablesResponse {
|
||||
repeated EnvVariable env_vars = 1;
|
||||
}
|
||||
|
||||
message OpenPluginPaneInNewTabPayload {
|
||||
string plugin_url = 1;
|
||||
map<string, string> configuration = 2;
|
||||
repeated ContextItem context = 3;
|
||||
}
|
||||
|
||||
message OpenPaneInNewTabResponse {
|
||||
optional uint64 tab_id = 1;
|
||||
optional PaneId pane_id = 2;
|
||||
}
|
||||
|
||||
message OpenCommandPaneInPlaceOfPaneIdPayload {
|
||||
PaneId pane_id_to_replace = 1;
|
||||
command.Command command_to_run = 2;
|
||||
bool close_replaced_pane = 3;
|
||||
repeated ContextItem context = 4;
|
||||
}
|
||||
|
||||
message OpenTerminalPaneInPlaceOfPaneIdPayload {
|
||||
PaneId pane_id_to_replace = 1;
|
||||
optional file.File cwd = 2;
|
||||
bool close_replaced_pane = 3;
|
||||
}
|
||||
|
||||
message OpenEditPaneInPlaceOfPaneIdPayload {
|
||||
PaneId pane_id_to_replace = 1;
|
||||
file.File file_to_open = 2;
|
||||
bool close_replaced_pane = 3;
|
||||
repeated ContextItem context = 4;
|
||||
}
|
||||
|
||||
message OpenCommandPaneInPlaceOfPaneIdResponse {
|
||||
optional PaneId pane_id = 1;
|
||||
}
|
||||
|
||||
message OpenTerminalPaneInPlaceOfPaneIdResponse {
|
||||
optional PaneId pane_id = 1;
|
||||
}
|
||||
|
||||
message OpenEditPaneInPlaceOfPaneIdResponse {
|
||||
optional PaneId pane_id = 1;
|
||||
}
|
||||
|
||||
@@ -56,12 +56,16 @@ pub use super::generated_api::api::{
|
||||
OpenCommandPaneFloatingNearPluginPayload,
|
||||
OpenCommandPaneFloatingNearPluginResponse as ProtobufOpenCommandPaneFloatingNearPluginResponse,
|
||||
OpenCommandPaneFloatingResponse as ProtobufOpenCommandPaneFloatingResponse,
|
||||
OpenCommandPaneInPlaceOfPaneIdPayload,
|
||||
OpenCommandPaneInPlaceOfPaneIdResponse as ProtobufOpenCommandPaneInPlaceOfPaneIdResponse,
|
||||
OpenCommandPaneInPlaceOfPluginPayload,
|
||||
OpenCommandPaneInPlaceOfPluginResponse as ProtobufOpenCommandPaneInPlaceOfPluginResponse,
|
||||
OpenCommandPaneInPlaceResponse as ProtobufOpenCommandPaneInPlaceResponse,
|
||||
OpenCommandPaneNearPluginPayload,
|
||||
OpenCommandPaneNearPluginResponse as ProtobufOpenCommandPaneNearPluginResponse,
|
||||
OpenCommandPanePayload, OpenCommandPaneResponse as ProtobufOpenCommandPaneResponse,
|
||||
OpenEditPaneInPlaceOfPaneIdPayload,
|
||||
OpenEditPaneInPlaceOfPaneIdResponse as ProtobufOpenEditPaneInPlaceOfPaneIdResponse,
|
||||
OpenFileFloatingNearPluginPayload,
|
||||
OpenFileFloatingNearPluginResponse as ProtobufOpenFileFloatingNearPluginResponse,
|
||||
OpenFileFloatingResponse as ProtobufOpenFileFloatingResponse,
|
||||
@@ -69,7 +73,10 @@ pub use super::generated_api::api::{
|
||||
OpenFileInPlaceOfPluginResponse as ProtobufOpenFileInPlaceOfPluginResponse,
|
||||
OpenFileInPlaceResponse as ProtobufOpenFileInPlaceResponse, OpenFileNearPluginPayload,
|
||||
OpenFileNearPluginResponse as ProtobufOpenFileNearPluginResponse, OpenFilePayload,
|
||||
OpenFileResponse as ProtobufOpenFileResponse, OpenTerminalFloatingNearPluginPayload,
|
||||
OpenFileResponse as ProtobufOpenFileResponse,
|
||||
OpenPaneInNewTabResponse as ProtobufOpenPaneInNewTabResponse,
|
||||
OpenPluginPaneInNewTabPayload as ProtobufOpenPluginPaneInNewTabPayload,
|
||||
OpenTerminalFloatingNearPluginPayload,
|
||||
OpenTerminalFloatingNearPluginResponse as ProtobufOpenTerminalFloatingNearPluginResponse,
|
||||
OpenTerminalFloatingResponse as ProtobufOpenTerminalFloatingResponse,
|
||||
OpenTerminalInPlaceOfPluginPayload,
|
||||
@@ -77,6 +84,8 @@ pub use super::generated_api::api::{
|
||||
OpenTerminalInPlaceResponse as ProtobufOpenTerminalInPlaceResponse,
|
||||
OpenTerminalNearPluginPayload,
|
||||
OpenTerminalNearPluginResponse as ProtobufOpenTerminalNearPluginResponse,
|
||||
OpenTerminalPaneInPlaceOfPaneIdPayload,
|
||||
OpenTerminalPaneInPlaceOfPaneIdResponse as ProtobufOpenTerminalPaneInPlaceOfPaneIdResponse,
|
||||
OpenTerminalResponse as ProtobufOpenTerminalResponse, OverrideLayoutPayload,
|
||||
PageScrollDownInPaneIdPayload, PageScrollUpInPaneIdPayload, PaneId as ProtobufPaneId,
|
||||
PaneIdAndFloatingPaneCoordinates, PaneType as ProtobufPaneType, ParseLayoutPayload,
|
||||
@@ -2269,6 +2278,129 @@ impl TryFrom<ProtobufPluginCommand> for PluginCommand {
|
||||
_ => Err("Mismatched payload for GetSessionEnvironmentVariables"),
|
||||
}
|
||||
},
|
||||
Some(CommandName::OpenCommandPaneInNewTab) => match protobuf_plugin_command.payload {
|
||||
Some(Payload::OpenCommandPaneInNewTabPayload(payload)) => {
|
||||
match payload.command_to_run {
|
||||
Some(command_to_run) => {
|
||||
let context: BTreeMap<String, String> = payload
|
||||
.context
|
||||
.into_iter()
|
||||
.map(|e| (e.name, e.value))
|
||||
.collect();
|
||||
Ok(PluginCommand::OpenCommandPaneInNewTab(
|
||||
command_to_run.try_into()?,
|
||||
context,
|
||||
))
|
||||
},
|
||||
None => Err("Malformed open_command_pane_in_new_tab payload"),
|
||||
}
|
||||
},
|
||||
_ => Err("Mismatched payload for OpenCommandPaneInNewTab"),
|
||||
},
|
||||
Some(CommandName::OpenPluginPaneInNewTab) => match protobuf_plugin_command.payload {
|
||||
Some(Payload::OpenPluginPaneInNewTabPayload(payload)) => {
|
||||
let configuration: BTreeMap<String, String> =
|
||||
payload.configuration.into_iter().collect();
|
||||
let context: BTreeMap<String, String> = payload
|
||||
.context
|
||||
.into_iter()
|
||||
.map(|e| (e.name, e.value))
|
||||
.collect();
|
||||
Ok(PluginCommand::OpenPluginPaneInNewTab {
|
||||
plugin_url: payload.plugin_url,
|
||||
configuration,
|
||||
context,
|
||||
})
|
||||
},
|
||||
_ => Err("Mismatched payload for OpenPluginPaneInNewTab"),
|
||||
},
|
||||
Some(CommandName::OpenEditorPaneInNewTab) => match protobuf_plugin_command.payload {
|
||||
Some(Payload::OpenEditorPaneInNewTabPayload(payload)) => {
|
||||
match payload.file_to_open {
|
||||
Some(file) => {
|
||||
let context: BTreeMap<String, String> = payload
|
||||
.context
|
||||
.into_iter()
|
||||
.map(|e| (e.name, e.value))
|
||||
.collect();
|
||||
Ok(PluginCommand::OpenEditorPaneInNewTab(
|
||||
file.try_into()?,
|
||||
context,
|
||||
))
|
||||
},
|
||||
None => Err("Malformed open_editor_pane_in_new_tab payload"),
|
||||
}
|
||||
},
|
||||
_ => Err("Mismatched payload for OpenEditorPaneInNewTab"),
|
||||
},
|
||||
Some(CommandName::OpenCommandPaneInPlaceOfPaneId) => {
|
||||
match protobuf_plugin_command.payload {
|
||||
Some(Payload::OpenCommandPaneInPlaceOfPaneIdPayload(payload)) => {
|
||||
match (payload.pane_id_to_replace, payload.command_to_run) {
|
||||
(Some(pane_id), Some(command_to_run)) => {
|
||||
let context: BTreeMap<String, String> = payload
|
||||
.context
|
||||
.into_iter()
|
||||
.map(|e| (e.name, e.value))
|
||||
.collect();
|
||||
Ok(PluginCommand::OpenCommandPaneInPlaceOfPaneId(
|
||||
pane_id.try_into()?,
|
||||
command_to_run.try_into()?,
|
||||
payload.close_replaced_pane,
|
||||
context,
|
||||
))
|
||||
},
|
||||
_ => Err("Malformed open_command_pane_in_place_of_pane_id payload"),
|
||||
}
|
||||
},
|
||||
_ => Err("Mismatched payload for OpenCommandPaneInPlaceOfPaneId"),
|
||||
}
|
||||
},
|
||||
Some(CommandName::OpenTerminalPaneInPlaceOfPaneId) => {
|
||||
match protobuf_plugin_command.payload {
|
||||
Some(Payload::OpenTerminalPaneInPlaceOfPaneIdPayload(payload)) => {
|
||||
match payload.pane_id_to_replace {
|
||||
Some(pane_id) => {
|
||||
let cwd = payload.cwd.map(|f| f.try_into()).transpose()?;
|
||||
let cwd = cwd.unwrap_or_else(|| crate::data::FileToOpen {
|
||||
path: std::path::PathBuf::from("."),
|
||||
..Default::default()
|
||||
});
|
||||
Ok(PluginCommand::OpenTerminalPaneInPlaceOfPaneId(
|
||||
pane_id.try_into()?,
|
||||
cwd,
|
||||
payload.close_replaced_pane,
|
||||
))
|
||||
},
|
||||
None => Err("Malformed open_terminal_pane_in_place_of_pane_id payload"),
|
||||
}
|
||||
},
|
||||
_ => Err("Mismatched payload for OpenTerminalPaneInPlaceOfPaneId"),
|
||||
}
|
||||
},
|
||||
Some(CommandName::OpenEditPaneInPlaceOfPaneId) => {
|
||||
match protobuf_plugin_command.payload {
|
||||
Some(Payload::OpenEditPaneInPlaceOfPaneIdPayload(payload)) => {
|
||||
match (payload.pane_id_to_replace, payload.file_to_open) {
|
||||
(Some(pane_id), Some(file_to_open)) => {
|
||||
let context: BTreeMap<String, String> = payload
|
||||
.context
|
||||
.into_iter()
|
||||
.map(|e| (e.name, e.value))
|
||||
.collect();
|
||||
Ok(PluginCommand::OpenEditPaneInPlaceOfPaneId(
|
||||
pane_id.try_into()?,
|
||||
file_to_open.try_into()?,
|
||||
payload.close_replaced_pane,
|
||||
context,
|
||||
))
|
||||
},
|
||||
_ => Err("Malformed open_edit_pane_in_place_of_pane_id payload"),
|
||||
}
|
||||
},
|
||||
_ => Err("Mismatched payload for OpenEditPaneInPlaceOfPaneId"),
|
||||
}
|
||||
},
|
||||
None => Err("Unrecognized plugin command"),
|
||||
}
|
||||
}
|
||||
@@ -3697,6 +3829,114 @@ impl TryFrom<PluginCommand> for ProtobufPluginCommand {
|
||||
payload: Some(Payload::GetSessionEnvironmentVariablesPayload(payload)),
|
||||
})
|
||||
},
|
||||
PluginCommand::OpenCommandPaneInNewTab(command_to_run, context) => {
|
||||
let context: Vec<_> = context
|
||||
.into_iter()
|
||||
.map(|(name, value)| ContextItem { name, value })
|
||||
.collect();
|
||||
Ok(ProtobufPluginCommand {
|
||||
name: CommandName::OpenCommandPaneInNewTab as i32,
|
||||
payload: Some(Payload::OpenCommandPaneInNewTabPayload(
|
||||
OpenCommandPanePayload {
|
||||
command_to_run: Some(command_to_run.try_into()?),
|
||||
floating_pane_coordinates: None,
|
||||
context,
|
||||
},
|
||||
)),
|
||||
})
|
||||
},
|
||||
PluginCommand::OpenPluginPaneInNewTab {
|
||||
plugin_url,
|
||||
configuration,
|
||||
context,
|
||||
} => {
|
||||
let context: Vec<_> = context
|
||||
.into_iter()
|
||||
.map(|(name, value)| ContextItem { name, value })
|
||||
.collect();
|
||||
let configuration: std::collections::HashMap<String, String> =
|
||||
configuration.into_iter().collect();
|
||||
Ok(ProtobufPluginCommand {
|
||||
name: CommandName::OpenPluginPaneInNewTab as i32,
|
||||
payload: Some(Payload::OpenPluginPaneInNewTabPayload(
|
||||
ProtobufOpenPluginPaneInNewTabPayload {
|
||||
plugin_url,
|
||||
configuration,
|
||||
context,
|
||||
},
|
||||
)),
|
||||
})
|
||||
},
|
||||
PluginCommand::OpenEditorPaneInNewTab(file_to_open, context) => {
|
||||
let context: Vec<_> = context
|
||||
.into_iter()
|
||||
.map(|(name, value)| ContextItem { name, value })
|
||||
.collect();
|
||||
Ok(ProtobufPluginCommand {
|
||||
name: CommandName::OpenEditorPaneInNewTab as i32,
|
||||
payload: Some(Payload::OpenEditorPaneInNewTabPayload(OpenFilePayload {
|
||||
file_to_open: Some(file_to_open.try_into()?),
|
||||
floating_pane_coordinates: None,
|
||||
context,
|
||||
})),
|
||||
})
|
||||
},
|
||||
PluginCommand::OpenCommandPaneInPlaceOfPaneId(
|
||||
pane_id,
|
||||
command_to_run,
|
||||
close_replaced_pane,
|
||||
context,
|
||||
) => {
|
||||
let context: Vec<_> = context
|
||||
.into_iter()
|
||||
.map(|(name, value)| ContextItem { name, value })
|
||||
.collect();
|
||||
Ok(ProtobufPluginCommand {
|
||||
name: CommandName::OpenCommandPaneInPlaceOfPaneId as i32,
|
||||
payload: Some(Payload::OpenCommandPaneInPlaceOfPaneIdPayload(
|
||||
OpenCommandPaneInPlaceOfPaneIdPayload {
|
||||
pane_id_to_replace: Some(pane_id.try_into()?),
|
||||
command_to_run: Some(command_to_run.try_into()?),
|
||||
close_replaced_pane,
|
||||
context,
|
||||
},
|
||||
)),
|
||||
})
|
||||
},
|
||||
PluginCommand::OpenTerminalPaneInPlaceOfPaneId(pane_id, cwd, close_replaced_pane) => {
|
||||
Ok(ProtobufPluginCommand {
|
||||
name: CommandName::OpenTerminalPaneInPlaceOfPaneId as i32,
|
||||
payload: Some(Payload::OpenTerminalPaneInPlaceOfPaneIdPayload(
|
||||
OpenTerminalPaneInPlaceOfPaneIdPayload {
|
||||
pane_id_to_replace: Some(pane_id.try_into()?),
|
||||
cwd: Some(cwd.try_into()?),
|
||||
close_replaced_pane,
|
||||
},
|
||||
)),
|
||||
})
|
||||
},
|
||||
PluginCommand::OpenEditPaneInPlaceOfPaneId(
|
||||
pane_id,
|
||||
file_to_open,
|
||||
close_replaced_pane,
|
||||
context,
|
||||
) => {
|
||||
let context: Vec<_> = context
|
||||
.into_iter()
|
||||
.map(|(name, value)| ContextItem { name, value })
|
||||
.collect();
|
||||
Ok(ProtobufPluginCommand {
|
||||
name: CommandName::OpenEditPaneInPlaceOfPaneId as i32,
|
||||
payload: Some(Payload::OpenEditPaneInPlaceOfPaneIdPayload(
|
||||
OpenEditPaneInPlaceOfPaneIdPayload {
|
||||
pane_id_to_replace: Some(pane_id.try_into()?),
|
||||
file_to_open: Some(file_to_open.try_into()?),
|
||||
close_replaced_pane,
|
||||
context,
|
||||
},
|
||||
)),
|
||||
})
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -3706,12 +3946,14 @@ use crate::data::{
|
||||
BreakPanesToNewTabResponse, BreakPanesToTabWithIdResponse, BreakPanesToTabWithIndexResponse,
|
||||
FocusOrCreateTabResponse, NewTabResponse, NewTabsResponse, OpenCommandPaneBackgroundResponse,
|
||||
OpenCommandPaneFloatingNearPluginResponse, OpenCommandPaneFloatingResponse,
|
||||
OpenCommandPaneInPlaceOfPluginResponse, OpenCommandPaneInPlaceResponse,
|
||||
OpenCommandPaneNearPluginResponse, OpenCommandPaneResponse, OpenFileFloatingNearPluginResponse,
|
||||
OpenCommandPaneInPlaceOfPaneIdResponse, OpenCommandPaneInPlaceOfPluginResponse,
|
||||
OpenCommandPaneInPlaceResponse, OpenCommandPaneNearPluginResponse, OpenCommandPaneResponse,
|
||||
OpenEditPaneInPlaceOfPaneIdResponse, OpenFileFloatingNearPluginResponse,
|
||||
OpenFileFloatingResponse, OpenFileInPlaceOfPluginResponse, OpenFileInPlaceResponse,
|
||||
OpenFileNearPluginResponse, OpenFileResponse, OpenTerminalFloatingNearPluginResponse,
|
||||
OpenTerminalFloatingResponse, OpenTerminalInPlaceOfPluginResponse, OpenTerminalInPlaceResponse,
|
||||
OpenTerminalNearPluginResponse, OpenTerminalResponse,
|
||||
OpenFileNearPluginResponse, OpenFileResponse, OpenPaneInNewTabResponse,
|
||||
OpenTerminalFloatingNearPluginResponse, OpenTerminalFloatingResponse,
|
||||
OpenTerminalInPlaceOfPluginResponse, OpenTerminalInPlaceResponse,
|
||||
OpenTerminalNearPluginResponse, OpenTerminalPaneInPlaceOfPaneIdResponse, OpenTerminalResponse,
|
||||
};
|
||||
|
||||
impl TryFrom<ProtobufNewTabResponse> for NewTabResponse {
|
||||
@@ -3827,6 +4069,16 @@ impl From<BreakPanesToTabWithIndexResponse> for ProtobufBreakPanesToTabWithIndex
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<ProtobufBreakPanesToTabWithIdResponse> for BreakPanesToTabWithIdResponse {
|
||||
type Error = &'static str;
|
||||
fn try_from(protobuf: ProtobufBreakPanesToTabWithIdResponse) -> Result<Self, Self::Error> {
|
||||
match protobuf.result {
|
||||
Some(break_panes_to_tab_with_id_response::Result::TabId(id)) => Ok(Some(id as usize)),
|
||||
Some(break_panes_to_tab_with_id_response::Result::None(_)) | None => Ok(None),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<BreakPanesToTabWithIdResponse> for ProtobufBreakPanesToTabWithIdResponse {
|
||||
fn from(response: BreakPanesToTabWithIdResponse) -> Self {
|
||||
match response {
|
||||
@@ -4205,3 +4457,92 @@ impl From<OpenCommandPaneBackgroundResponse> for ProtobufOpenCommandPaneBackgrou
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<ProtobufOpenPaneInNewTabResponse> for OpenPaneInNewTabResponse {
|
||||
type Error = &'static str;
|
||||
fn try_from(protobuf: ProtobufOpenPaneInNewTabResponse) -> Result<Self, Self::Error> {
|
||||
let tab_id = protobuf.tab_id.map(|id| id as usize);
|
||||
let pane_id = match protobuf.pane_id {
|
||||
Some(pane_id) => Some(pane_id.try_into()?),
|
||||
None => None,
|
||||
};
|
||||
Ok(OpenPaneInNewTabResponse { tab_id, pane_id })
|
||||
}
|
||||
}
|
||||
|
||||
impl From<OpenPaneInNewTabResponse> for ProtobufOpenPaneInNewTabResponse {
|
||||
fn from(response: OpenPaneInNewTabResponse) -> Self {
|
||||
ProtobufOpenPaneInNewTabResponse {
|
||||
tab_id: response.tab_id.map(|id| id as u64),
|
||||
pane_id: response.pane_id.map(|p| p.try_into().unwrap()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<ProtobufOpenCommandPaneInPlaceOfPaneIdResponse>
|
||||
for OpenCommandPaneInPlaceOfPaneIdResponse
|
||||
{
|
||||
type Error = &'static str;
|
||||
fn try_from(
|
||||
protobuf: ProtobufOpenCommandPaneInPlaceOfPaneIdResponse,
|
||||
) -> Result<Self, Self::Error> {
|
||||
match protobuf.pane_id {
|
||||
Some(pane_id) => Ok(Some(pane_id.try_into()?)),
|
||||
None => Ok(None),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<OpenCommandPaneInPlaceOfPaneIdResponse>
|
||||
for ProtobufOpenCommandPaneInPlaceOfPaneIdResponse
|
||||
{
|
||||
fn from(response: OpenCommandPaneInPlaceOfPaneIdResponse) -> Self {
|
||||
ProtobufOpenCommandPaneInPlaceOfPaneIdResponse {
|
||||
pane_id: response.map(|p| p.try_into().unwrap()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<ProtobufOpenTerminalPaneInPlaceOfPaneIdResponse>
|
||||
for OpenTerminalPaneInPlaceOfPaneIdResponse
|
||||
{
|
||||
type Error = &'static str;
|
||||
fn try_from(
|
||||
protobuf: ProtobufOpenTerminalPaneInPlaceOfPaneIdResponse,
|
||||
) -> Result<Self, Self::Error> {
|
||||
match protobuf.pane_id {
|
||||
Some(pane_id) => Ok(Some(pane_id.try_into()?)),
|
||||
None => Ok(None),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<OpenTerminalPaneInPlaceOfPaneIdResponse>
|
||||
for ProtobufOpenTerminalPaneInPlaceOfPaneIdResponse
|
||||
{
|
||||
fn from(response: OpenTerminalPaneInPlaceOfPaneIdResponse) -> Self {
|
||||
ProtobufOpenTerminalPaneInPlaceOfPaneIdResponse {
|
||||
pane_id: response.map(|p| p.try_into().unwrap()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<ProtobufOpenEditPaneInPlaceOfPaneIdResponse> for OpenEditPaneInPlaceOfPaneIdResponse {
|
||||
type Error = &'static str;
|
||||
fn try_from(
|
||||
protobuf: ProtobufOpenEditPaneInPlaceOfPaneIdResponse,
|
||||
) -> Result<Self, Self::Error> {
|
||||
match protobuf.pane_id {
|
||||
Some(pane_id) => Ok(Some(pane_id.try_into()?)),
|
||||
None => Ok(None),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<OpenEditPaneInPlaceOfPaneIdResponse> for ProtobufOpenEditPaneInPlaceOfPaneIdResponse {
|
||||
fn from(response: OpenEditPaneInPlaceOfPaneIdResponse) -> Self {
|
||||
ProtobufOpenEditPaneInPlaceOfPaneIdResponse {
|
||||
pane_id: response.map(|p| p.try_into().unwrap()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user