From 63e3a1eae2e727a808084d9fe9ff6eca7816ef7e Mon Sep 17 00:00:00 2001 From: Aram Drevekenin Date: Sat, 17 Jun 2023 14:41:49 +0200 Subject: [PATCH] feat(plugins): more plugin api methods (#2550) * feat(plugins): close, focus, rename pane, rename tab and show_self api methods * style(fmt): rustfmt --- .../fixture-plugin-for-tests/src/main.rs | 27 + zellij-server/src/panes/plugin_pane.rs | 4 + zellij-server/src/panes/terminal_pane.rs | 4 + .../src/plugins/unit/plugin_tests.rs | 480 ++++++++++++++++++ ...sts__close_plugin_pane_plugin_command.snap | 13 + ...s__close_terminal_pane_plugin_command.snap | 13 + ...sts__focus_plugin_pane_plugin_command.snap | 14 + ...s__focus_terminal_pane_plugin_command.snap | 14 + ...ts__rename_plugin_pane_plugin_command.snap | 34 ++ ...ugin_tests__rename_tab_plugin_command.snap | 24 + ...__rename_terminal_pane_plugin_command.snap | 36 ++ ...lugin_tests__show_self_plugin_command.snap | 14 + zellij-server/src/plugins/zellij_exports.rs | 97 ++++ zellij-server/src/route.rs | 61 +++ zellij-server/src/screen.rs | 65 ++- zellij-server/src/tab/mod.rs | 19 + zellij-tile/src/shim.rs | 66 +++ zellij-utils/src/errors.rs | 3 + zellij-utils/src/input/actions.rs | 7 + 19 files changed, 994 insertions(+), 1 deletion(-) create mode 100644 zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__close_plugin_pane_plugin_command.snap create mode 100644 zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__close_terminal_pane_plugin_command.snap create mode 100644 zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__focus_plugin_pane_plugin_command.snap create mode 100644 zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__focus_terminal_pane_plugin_command.snap create mode 100644 zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__rename_plugin_pane_plugin_command.snap create mode 100644 zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__rename_tab_plugin_command.snap create mode 100644 zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__rename_terminal_pane_plugin_command.snap create mode 100644 zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__show_self_plugin_command.snap diff --git a/default-plugins/fixture-plugin-for-tests/src/main.rs b/default-plugins/fixture-plugin-for-tests/src/main.rs index 2107137d..9ce45f8a 100644 --- a/default-plugins/fixture-plugin-for-tests/src/main.rs +++ b/default-plugins/fixture-plugin-for-tests/src/main.rs @@ -183,6 +183,33 @@ impl ZellijPlugin for State { Key::Ctrl('p') => { hide_self(); }, + Key::Ctrl('q') => { + let should_float_if_hidden = false; + show_self(should_float_if_hidden); + }, + Key::Ctrl('r') => { + close_terminal_pane(1); + }, + Key::Ctrl('s') => { + close_plugin_pane(1); + }, + Key::Ctrl('t') => { + let should_float_if_hidden = false; + focus_terminal_pane(1, should_float_if_hidden); + }, + Key::Ctrl('u') => { + let should_float_if_hidden = false; + focus_plugin_pane(1, should_float_if_hidden); + }, + Key::Ctrl('v') => { + rename_terminal_pane(1, "new terminal_pane_name"); + }, + Key::Ctrl('w') => { + rename_plugin_pane(1, "new plugin_pane_name"); + }, + Key::Ctrl('x') => { + rename_tab(1, "new tab name"); + }, _ => {}, }, Event::CustomMessage(message, payload) => { diff --git a/zellij-server/src/panes/plugin_pane.rs b/zellij-server/src/panes/plugin_pane.rs index 557769f0..46552960 100644 --- a/zellij-server/src/panes/plugin_pane.rs +++ b/zellij-server/src/panes/plugin_pane.rs @@ -571,6 +571,10 @@ impl Pane for PluginPane { self.pane_name.to_owned() } } + fn rename(&mut self, buf: Vec) { + self.pane_name = String::from_utf8_lossy(&buf).to_string(); + self.set_should_render(true); + } } impl PluginPane { diff --git a/zellij-server/src/panes/terminal_pane.rs b/zellij-server/src/panes/terminal_pane.rs index df74afd6..a01d815b 100644 --- a/zellij-server/src/panes/terminal_pane.rs +++ b/zellij-server/src/panes/terminal_pane.rs @@ -745,6 +745,10 @@ impl Pane for TerminalPane { None => false, } } + fn rename(&mut self, buf: Vec) { + self.pane_name = String::from_utf8_lossy(&buf).to_string(); + self.set_should_render(true); + } } impl TerminalPane { diff --git a/zellij-server/src/plugins/unit/plugin_tests.rs b/zellij-server/src/plugins/unit/plugin_tests.rs index 6084c9fe..3ec8485b 100644 --- a/zellij-server/src/plugins/unit/plugin_tests.rs +++ b/zellij-server/src/plugins/unit/plugin_tests.rs @@ -3608,3 +3608,483 @@ pub fn hide_self_plugin_command() { .clone(); assert_snapshot!(format!("{:#?}", new_tab_event)); } + +#[test] +#[ignore] +pub fn show_self_plugin_command() { + let temp_folder = tempdir().unwrap(); // placed explicitly in the test scope because its + // destructor removes the directory + let plugin_host_folder = PathBuf::from(temp_folder.path()); + let (plugin_thread_sender, screen_receiver, mut teardown) = + create_plugin_thread(Some(plugin_host_folder)); + let plugin_should_float = Some(false); + let plugin_title = Some("test_plugin".to_owned()); + let run_plugin = RunPlugin { + _allow_exec_host_cmd: false, + location: RunPluginLocation::File(PathBuf::from(&*PLUGIN_FIXTURE)), + }; + let tab_index = 1; + let client_id = 1; + let size = Size { + cols: 121, + rows: 20, + }; + let received_screen_instructions = Arc::new(Mutex::new(vec![])); + let screen_thread = log_actions_in_thread!( + received_screen_instructions, + ScreenInstruction::FocusPaneWithId, + screen_receiver, + 1 + ); + + let _ = plugin_thread_sender.send(PluginInstruction::AddClient(client_id)); + let _ = plugin_thread_sender.send(PluginInstruction::Load( + plugin_should_float, + plugin_title, + run_plugin, + tab_index, + client_id, + size, + )); + let _ = plugin_thread_sender.send(PluginInstruction::Update(vec![( + None, + Some(client_id), + Event::Key(Key::Ctrl('q')), // this triggers the enent in the fixture plugin + )])); + std::thread::sleep(std::time::Duration::from_millis(100)); + screen_thread.join().unwrap(); // this might take a while if the cache is cold + teardown(); + let new_tab_event = received_screen_instructions + .lock() + .unwrap() + .iter() + .find_map(|i| { + if let ScreenInstruction::FocusPaneWithId(..) = i { + Some(i.clone()) + } else { + None + } + }) + .clone(); + assert_snapshot!(format!("{:#?}", new_tab_event)); +} + +#[test] +#[ignore] +pub fn close_terminal_pane_plugin_command() { + let temp_folder = tempdir().unwrap(); // placed explicitly in the test scope because its + // destructor removes the directory + let plugin_host_folder = PathBuf::from(temp_folder.path()); + let (plugin_thread_sender, screen_receiver, mut teardown) = + create_plugin_thread(Some(plugin_host_folder)); + let plugin_should_float = Some(false); + let plugin_title = Some("test_plugin".to_owned()); + let run_plugin = RunPlugin { + _allow_exec_host_cmd: false, + location: RunPluginLocation::File(PathBuf::from(&*PLUGIN_FIXTURE)), + }; + let tab_index = 1; + let client_id = 1; + let size = Size { + cols: 121, + rows: 20, + }; + let received_screen_instructions = Arc::new(Mutex::new(vec![])); + let screen_thread = log_actions_in_thread!( + received_screen_instructions, + ScreenInstruction::ClosePane, + screen_receiver, + 1 + ); + + let _ = plugin_thread_sender.send(PluginInstruction::AddClient(client_id)); + let _ = plugin_thread_sender.send(PluginInstruction::Load( + plugin_should_float, + plugin_title, + run_plugin, + tab_index, + client_id, + size, + )); + let _ = plugin_thread_sender.send(PluginInstruction::Update(vec![( + None, + Some(client_id), + Event::Key(Key::Ctrl('r')), // this triggers the enent in the fixture plugin + )])); + std::thread::sleep(std::time::Duration::from_millis(100)); + screen_thread.join().unwrap(); // this might take a while if the cache is cold + teardown(); + let new_tab_event = received_screen_instructions + .lock() + .unwrap() + .iter() + .find_map(|i| { + if let ScreenInstruction::ClosePane(..) = i { + Some(i.clone()) + } else { + None + } + }) + .clone(); + assert_snapshot!(format!("{:#?}", new_tab_event)); +} + +#[test] +#[ignore] +pub fn close_plugin_pane_plugin_command() { + let temp_folder = tempdir().unwrap(); // placed explicitly in the test scope because its + // destructor removes the directory + let plugin_host_folder = PathBuf::from(temp_folder.path()); + let (plugin_thread_sender, screen_receiver, mut teardown) = + create_plugin_thread(Some(plugin_host_folder)); + let plugin_should_float = Some(false); + let plugin_title = Some("test_plugin".to_owned()); + let run_plugin = RunPlugin { + _allow_exec_host_cmd: false, + location: RunPluginLocation::File(PathBuf::from(&*PLUGIN_FIXTURE)), + }; + let tab_index = 1; + let client_id = 1; + let size = Size { + cols: 121, + rows: 20, + }; + let received_screen_instructions = Arc::new(Mutex::new(vec![])); + let screen_thread = log_actions_in_thread!( + received_screen_instructions, + ScreenInstruction::ClosePane, + screen_receiver, + 1 + ); + + let _ = plugin_thread_sender.send(PluginInstruction::AddClient(client_id)); + let _ = plugin_thread_sender.send(PluginInstruction::Load( + plugin_should_float, + plugin_title, + run_plugin, + tab_index, + client_id, + size, + )); + let _ = plugin_thread_sender.send(PluginInstruction::Update(vec![( + None, + Some(client_id), + Event::Key(Key::Ctrl('s')), // this triggers the enent in the fixture plugin + )])); + std::thread::sleep(std::time::Duration::from_millis(100)); + screen_thread.join().unwrap(); // this might take a while if the cache is cold + teardown(); + let new_tab_event = received_screen_instructions + .lock() + .unwrap() + .iter() + .find_map(|i| { + if let ScreenInstruction::ClosePane(..) = i { + Some(i.clone()) + } else { + None + } + }) + .clone(); + assert_snapshot!(format!("{:#?}", new_tab_event)); +} + +#[test] +#[ignore] +pub fn focus_terminal_pane_plugin_command() { + let temp_folder = tempdir().unwrap(); // placed explicitly in the test scope because its + // destructor removes the directory + let plugin_host_folder = PathBuf::from(temp_folder.path()); + let (plugin_thread_sender, screen_receiver, mut teardown) = + create_plugin_thread(Some(plugin_host_folder)); + let plugin_should_float = Some(false); + let plugin_title = Some("test_plugin".to_owned()); + let run_plugin = RunPlugin { + _allow_exec_host_cmd: false, + location: RunPluginLocation::File(PathBuf::from(&*PLUGIN_FIXTURE)), + }; + let tab_index = 1; + let client_id = 1; + let size = Size { + cols: 121, + rows: 20, + }; + let received_screen_instructions = Arc::new(Mutex::new(vec![])); + let screen_thread = log_actions_in_thread!( + received_screen_instructions, + ScreenInstruction::FocusPaneWithId, + screen_receiver, + 1 + ); + + let _ = plugin_thread_sender.send(PluginInstruction::AddClient(client_id)); + let _ = plugin_thread_sender.send(PluginInstruction::Load( + plugin_should_float, + plugin_title, + run_plugin, + tab_index, + client_id, + size, + )); + let _ = plugin_thread_sender.send(PluginInstruction::Update(vec![( + None, + Some(client_id), + Event::Key(Key::Ctrl('t')), // this triggers the enent in the fixture plugin + )])); + std::thread::sleep(std::time::Duration::from_millis(100)); + screen_thread.join().unwrap(); // this might take a while if the cache is cold + teardown(); + let new_tab_event = received_screen_instructions + .lock() + .unwrap() + .iter() + .find_map(|i| { + if let ScreenInstruction::FocusPaneWithId(..) = i { + Some(i.clone()) + } else { + None + } + }) + .clone(); + assert_snapshot!(format!("{:#?}", new_tab_event)); +} + +#[test] +#[ignore] +pub fn focus_plugin_pane_plugin_command() { + let temp_folder = tempdir().unwrap(); // placed explicitly in the test scope because its + // destructor removes the directory + let plugin_host_folder = PathBuf::from(temp_folder.path()); + let (plugin_thread_sender, screen_receiver, mut teardown) = + create_plugin_thread(Some(plugin_host_folder)); + let plugin_should_float = Some(false); + let plugin_title = Some("test_plugin".to_owned()); + let run_plugin = RunPlugin { + _allow_exec_host_cmd: false, + location: RunPluginLocation::File(PathBuf::from(&*PLUGIN_FIXTURE)), + }; + let tab_index = 1; + let client_id = 1; + let size = Size { + cols: 121, + rows: 20, + }; + let received_screen_instructions = Arc::new(Mutex::new(vec![])); + let screen_thread = log_actions_in_thread!( + received_screen_instructions, + ScreenInstruction::FocusPaneWithId, + screen_receiver, + 1 + ); + + let _ = plugin_thread_sender.send(PluginInstruction::AddClient(client_id)); + let _ = plugin_thread_sender.send(PluginInstruction::Load( + plugin_should_float, + plugin_title, + run_plugin, + tab_index, + client_id, + size, + )); + let _ = plugin_thread_sender.send(PluginInstruction::Update(vec![( + None, + Some(client_id), + Event::Key(Key::Ctrl('u')), // this triggers the enent in the fixture plugin + )])); + std::thread::sleep(std::time::Duration::from_millis(100)); + screen_thread.join().unwrap(); // this might take a while if the cache is cold + teardown(); + let new_tab_event = received_screen_instructions + .lock() + .unwrap() + .iter() + .find_map(|i| { + if let ScreenInstruction::FocusPaneWithId(..) = i { + Some(i.clone()) + } else { + None + } + }) + .clone(); + assert_snapshot!(format!("{:#?}", new_tab_event)); +} + +#[test] +#[ignore] +pub fn rename_terminal_pane_plugin_command() { + let temp_folder = tempdir().unwrap(); // placed explicitly in the test scope because its + // destructor removes the directory + let plugin_host_folder = PathBuf::from(temp_folder.path()); + let (plugin_thread_sender, screen_receiver, mut teardown) = + create_plugin_thread(Some(plugin_host_folder)); + let plugin_should_float = Some(false); + let plugin_title = Some("test_plugin".to_owned()); + let run_plugin = RunPlugin { + _allow_exec_host_cmd: false, + location: RunPluginLocation::File(PathBuf::from(&*PLUGIN_FIXTURE)), + }; + let tab_index = 1; + let client_id = 1; + let size = Size { + cols: 121, + rows: 20, + }; + let received_screen_instructions = Arc::new(Mutex::new(vec![])); + let screen_thread = log_actions_in_thread!( + received_screen_instructions, + ScreenInstruction::RenamePane, + screen_receiver, + 1 + ); + + let _ = plugin_thread_sender.send(PluginInstruction::AddClient(client_id)); + let _ = plugin_thread_sender.send(PluginInstruction::Load( + plugin_should_float, + plugin_title, + run_plugin, + tab_index, + client_id, + size, + )); + let _ = plugin_thread_sender.send(PluginInstruction::Update(vec![( + None, + Some(client_id), + Event::Key(Key::Ctrl('v')), // this triggers the enent in the fixture plugin + )])); + std::thread::sleep(std::time::Duration::from_millis(100)); + screen_thread.join().unwrap(); // this might take a while if the cache is cold + teardown(); + let new_tab_event = received_screen_instructions + .lock() + .unwrap() + .iter() + .find_map(|i| { + if let ScreenInstruction::RenamePane(..) = i { + Some(i.clone()) + } else { + None + } + }) + .clone(); + assert_snapshot!(format!("{:#?}", new_tab_event)); +} + +#[test] +#[ignore] +pub fn rename_plugin_pane_plugin_command() { + let temp_folder = tempdir().unwrap(); // placed explicitly in the test scope because its + // destructor removes the directory + let plugin_host_folder = PathBuf::from(temp_folder.path()); + let (plugin_thread_sender, screen_receiver, mut teardown) = + create_plugin_thread(Some(plugin_host_folder)); + let plugin_should_float = Some(false); + let plugin_title = Some("test_plugin".to_owned()); + let run_plugin = RunPlugin { + _allow_exec_host_cmd: false, + location: RunPluginLocation::File(PathBuf::from(&*PLUGIN_FIXTURE)), + }; + let tab_index = 1; + let client_id = 1; + let size = Size { + cols: 121, + rows: 20, + }; + let received_screen_instructions = Arc::new(Mutex::new(vec![])); + let screen_thread = log_actions_in_thread!( + received_screen_instructions, + ScreenInstruction::RenamePane, + screen_receiver, + 1 + ); + + let _ = plugin_thread_sender.send(PluginInstruction::AddClient(client_id)); + let _ = plugin_thread_sender.send(PluginInstruction::Load( + plugin_should_float, + plugin_title, + run_plugin, + tab_index, + client_id, + size, + )); + let _ = plugin_thread_sender.send(PluginInstruction::Update(vec![( + None, + Some(client_id), + Event::Key(Key::Ctrl('w')), // this triggers the enent in the fixture plugin + )])); + std::thread::sleep(std::time::Duration::from_millis(100)); + screen_thread.join().unwrap(); // this might take a while if the cache is cold + teardown(); + let new_tab_event = received_screen_instructions + .lock() + .unwrap() + .iter() + .find_map(|i| { + if let ScreenInstruction::RenamePane(..) = i { + Some(i.clone()) + } else { + None + } + }) + .clone(); + assert_snapshot!(format!("{:#?}", new_tab_event)); +} + +#[test] +#[ignore] +pub fn rename_tab_plugin_command() { + let temp_folder = tempdir().unwrap(); // placed explicitly in the test scope because its + // destructor removes the directory + let plugin_host_folder = PathBuf::from(temp_folder.path()); + let (plugin_thread_sender, screen_receiver, mut teardown) = + create_plugin_thread(Some(plugin_host_folder)); + let plugin_should_float = Some(false); + let plugin_title = Some("test_plugin".to_owned()); + let run_plugin = RunPlugin { + _allow_exec_host_cmd: false, + location: RunPluginLocation::File(PathBuf::from(&*PLUGIN_FIXTURE)), + }; + let tab_index = 1; + let client_id = 1; + let size = Size { + cols: 121, + rows: 20, + }; + let received_screen_instructions = Arc::new(Mutex::new(vec![])); + let screen_thread = log_actions_in_thread!( + received_screen_instructions, + ScreenInstruction::RenameTab, + screen_receiver, + 1 + ); + + let _ = plugin_thread_sender.send(PluginInstruction::AddClient(client_id)); + let _ = plugin_thread_sender.send(PluginInstruction::Load( + plugin_should_float, + plugin_title, + run_plugin, + tab_index, + client_id, + size, + )); + let _ = plugin_thread_sender.send(PluginInstruction::Update(vec![( + None, + Some(client_id), + Event::Key(Key::Ctrl('x')), // this triggers the enent in the fixture plugin + )])); + std::thread::sleep(std::time::Duration::from_millis(100)); + screen_thread.join().unwrap(); // this might take a while if the cache is cold + teardown(); + let new_tab_event = received_screen_instructions + .lock() + .unwrap() + .iter() + .find_map(|i| { + if let ScreenInstruction::RenameTab(..) = i { + Some(i.clone()) + } else { + None + } + }) + .clone(); + assert_snapshot!(format!("{:#?}", new_tab_event)); +} diff --git a/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__close_plugin_pane_plugin_command.snap b/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__close_plugin_pane_plugin_command.snap new file mode 100644 index 00000000..269986f1 --- /dev/null +++ b/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__close_plugin_pane_plugin_command.snap @@ -0,0 +1,13 @@ +--- +source: zellij-server/src/plugins/./unit/plugin_tests.rs +assertion_line: 3789 +expression: "format!(\"{:#?}\", new_tab_event)" +--- +Some( + ClosePane( + Plugin( + 1, + ), + None, + ), +) diff --git a/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__close_terminal_pane_plugin_command.snap b/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__close_terminal_pane_plugin_command.snap new file mode 100644 index 00000000..778d07b9 --- /dev/null +++ b/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__close_terminal_pane_plugin_command.snap @@ -0,0 +1,13 @@ +--- +source: zellij-server/src/plugins/./unit/plugin_tests.rs +assertion_line: 3729 +expression: "format!(\"{:#?}\", new_tab_event)" +--- +Some( + ClosePane( + Terminal( + 1, + ), + None, + ), +) diff --git a/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__focus_plugin_pane_plugin_command.snap b/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__focus_plugin_pane_plugin_command.snap new file mode 100644 index 00000000..d830391e --- /dev/null +++ b/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__focus_plugin_pane_plugin_command.snap @@ -0,0 +1,14 @@ +--- +source: zellij-server/src/plugins/./unit/plugin_tests.rs +assertion_line: 3909 +expression: "format!(\"{:#?}\", new_tab_event)" +--- +Some( + FocusPaneWithId( + Plugin( + 1, + ), + false, + 1, + ), +) diff --git a/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__focus_terminal_pane_plugin_command.snap b/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__focus_terminal_pane_plugin_command.snap new file mode 100644 index 00000000..38685b01 --- /dev/null +++ b/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__focus_terminal_pane_plugin_command.snap @@ -0,0 +1,14 @@ +--- +source: zellij-server/src/plugins/./unit/plugin_tests.rs +assertion_line: 3849 +expression: "format!(\"{:#?}\", new_tab_event)" +--- +Some( + FocusPaneWithId( + Terminal( + 1, + ), + false, + 1, + ), +) diff --git a/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__rename_plugin_pane_plugin_command.snap b/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__rename_plugin_pane_plugin_command.snap new file mode 100644 index 00000000..cda7d3c4 --- /dev/null +++ b/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__rename_plugin_pane_plugin_command.snap @@ -0,0 +1,34 @@ +--- +source: zellij-server/src/plugins/./unit/plugin_tests.rs +assertion_line: 4029 +expression: "format!(\"{:#?}\", new_tab_event)" +--- +Some( + RenamePane( + Plugin( + 1, + ), + [ + 110, + 101, + 119, + 32, + 112, + 108, + 117, + 103, + 105, + 110, + 95, + 112, + 97, + 110, + 101, + 95, + 110, + 97, + 109, + 101, + ], + ), +) diff --git a/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__rename_tab_plugin_command.snap b/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__rename_tab_plugin_command.snap new file mode 100644 index 00000000..21fe9260 --- /dev/null +++ b/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__rename_tab_plugin_command.snap @@ -0,0 +1,24 @@ +--- +source: zellij-server/src/plugins/./unit/plugin_tests.rs +assertion_line: 4089 +expression: "format!(\"{:#?}\", new_tab_event)" +--- +Some( + RenameTab( + 1, + [ + 110, + 101, + 119, + 32, + 116, + 97, + 98, + 32, + 110, + 97, + 109, + 101, + ], + ), +) diff --git a/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__rename_terminal_pane_plugin_command.snap b/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__rename_terminal_pane_plugin_command.snap new file mode 100644 index 00000000..64fdf4a9 --- /dev/null +++ b/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__rename_terminal_pane_plugin_command.snap @@ -0,0 +1,36 @@ +--- +source: zellij-server/src/plugins/./unit/plugin_tests.rs +assertion_line: 3969 +expression: "format!(\"{:#?}\", new_tab_event)" +--- +Some( + RenamePane( + Terminal( + 1, + ), + [ + 110, + 101, + 119, + 32, + 116, + 101, + 114, + 109, + 105, + 110, + 97, + 108, + 95, + 112, + 97, + 110, + 101, + 95, + 110, + 97, + 109, + 101, + ], + ), +) diff --git a/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__show_self_plugin_command.snap b/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__show_self_plugin_command.snap new file mode 100644 index 00000000..b42e5db1 --- /dev/null +++ b/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__show_self_plugin_command.snap @@ -0,0 +1,14 @@ +--- +source: zellij-server/src/plugins/./unit/plugin_tests.rs +assertion_line: 3673 +expression: "format!(\"{:#?}\", new_tab_event)" +--- +Some( + FocusPaneWithId( + Plugin( + 0, + ), + false, + 1, + ), +) diff --git a/zellij-server/src/plugins/zellij_exports.rs b/zellij-server/src/plugins/zellij_exports.rs index 08d54c84..403d8ced 100644 --- a/zellij-server/src/plugins/zellij_exports.rs +++ b/zellij-server/src/plugins/zellij_exports.rs @@ -85,6 +85,7 @@ pub fn zellij_exports( host_post_message_to, host_post_message_to_plugin, host_hide_self, + host_show_self, host_switch_to_mode, host_new_tabs_with_layout, host_new_tab, @@ -125,6 +126,13 @@ pub fn zellij_exports( host_focus_or_create_tab, host_go_to_tab, host_start_or_reload_plugin, + host_close_terminal_pane, + host_close_plugin_pane, + host_focus_terminal_pane, + host_focus_plugin_pane, + host_rename_terminal_pane, + host_rename_plugin_pane, + host_rename_tab, } } @@ -541,6 +549,13 @@ fn host_hide_self(env: &ForeignFunctionEnv) { .fatal(); } +fn host_show_self(env: &ForeignFunctionEnv, should_float_if_hidden: i32) { + let should_float_if_hidden = should_float_if_hidden != 0; + let action = Action::FocusPluginPaneWithId(env.plugin_env.plugin_id, should_float_if_hidden); + let error_msg = || format!("Failed to show self for plugin"); + apply_action!(action, error_msg, env); +} + fn host_switch_to_mode(env: &ForeignFunctionEnv) { wasi_read_object::(&env.plugin_env.wasi_env) .and_then(|input_mode| { @@ -966,6 +981,88 @@ fn host_start_or_reload_plugin(env: &ForeignFunctionEnv) { .fatal(); } +fn host_close_terminal_pane(env: &ForeignFunctionEnv, terminal_pane_id: i32) { + let error_msg = || { + format!( + "failed to change tab focus in plugin {}", + env.plugin_env.name() + ) + }; + let action = Action::CloseTerminalPane(terminal_pane_id as u32); + apply_action!(action, error_msg, env); +} + +fn host_close_plugin_pane(env: &ForeignFunctionEnv, plugin_pane_id: i32) { + let error_msg = || { + format!( + "failed to change tab focus in plugin {}", + env.plugin_env.name() + ) + }; + let action = Action::ClosePluginPane(plugin_pane_id as u32); + apply_action!(action, error_msg, env); +} + +fn host_focus_terminal_pane( + env: &ForeignFunctionEnv, + terminal_pane_id: i32, + should_float_if_hidden: i32, +) { + let should_float_if_hidden = should_float_if_hidden != 0; + let action = Action::FocusTerminalPaneWithId(terminal_pane_id as u32, should_float_if_hidden); + let error_msg = || format!("Failed to focus terminal pane"); + apply_action!(action, error_msg, env); +} + +fn host_focus_plugin_pane( + env: &ForeignFunctionEnv, + plugin_pane_id: i32, + should_float_if_hidden: i32, +) { + let should_float_if_hidden = should_float_if_hidden != 0; + let action = Action::FocusPluginPaneWithId(plugin_pane_id as u32, should_float_if_hidden); + let error_msg = || format!("Failed to focus plugin pane"); + apply_action!(action, error_msg, env); +} + +fn host_rename_terminal_pane(env: &ForeignFunctionEnv) { + let error_msg = || format!("Failed to rename terminal pane"); + wasi_read_object::<(u32, String)>(&env.plugin_env.wasi_env) + .and_then(|(terminal_pane_id, new_name)| { + let rename_pane_action = + Action::RenameTerminalPane(terminal_pane_id, new_name.as_bytes().to_vec()); + apply_action!(rename_pane_action, error_msg, env); + Ok(()) + }) + .with_context(error_msg) + .fatal(); +} + +fn host_rename_plugin_pane(env: &ForeignFunctionEnv) { + let error_msg = || format!("Failed to rename plugin pane"); + wasi_read_object::<(u32, String)>(&env.plugin_env.wasi_env) + .and_then(|(plugin_pane_id, new_name)| { + let rename_pane_action = + Action::RenamePluginPane(plugin_pane_id, new_name.as_bytes().to_vec()); + apply_action!(rename_pane_action, error_msg, env); + Ok(()) + }) + .with_context(error_msg) + .fatal(); +} + +fn host_rename_tab(env: &ForeignFunctionEnv) { + let error_msg = || format!("Failed to rename tab"); + wasi_read_object::<(u32, String)>(&env.plugin_env.wasi_env) + .and_then(|(tab_index, new_name)| { + let rename_tab_action = Action::RenameTab(tab_index, new_name.as_bytes().to_vec()); + apply_action!(rename_tab_action, error_msg, env); + Ok(()) + }) + .with_context(error_msg) + .fatal(); +} + // Custom panic handler for plugins. // // This is called when a panic occurs in a plugin. Since most panics will likely originate in the diff --git a/zellij-server/src/route.rs b/zellij-server/src/route.rs index 7ff85946..e1575e7f 100644 --- a/zellij-server/src/route.rs +++ b/zellij-server/src/route.rs @@ -4,6 +4,7 @@ use std::sync::{Arc, RwLock}; use crate::thread_bus::ThreadSenders; use crate::{ os_input_output::ServerOsApi, + panes::PaneId, plugins::PluginInstruction, pty::{ClientOrTabIndex, PtyInstruction}, screen::ScreenInstruction, @@ -629,6 +630,66 @@ pub(crate) fn route_action( )) .with_context(err_context)?; }, + Action::CloseTerminalPane(terminal_pane_id) => { + senders + .send_to_screen(ScreenInstruction::ClosePane( + PaneId::Terminal(terminal_pane_id), + None, // we send None here so that the terminal pane would be closed anywhere + // in the app, not just in the client's tab + )) + .with_context(err_context)?; + }, + Action::ClosePluginPane(plugin_pane_id) => { + senders + .send_to_screen(ScreenInstruction::ClosePane( + PaneId::Plugin(plugin_pane_id), + None, // we send None here so that the terminal pane would be closed anywhere + // in the app, not just in the client's tab + )) + .with_context(err_context)?; + }, + Action::FocusTerminalPaneWithId(pane_id, should_float_if_hidden) => { + senders + .send_to_screen(ScreenInstruction::FocusPaneWithId( + PaneId::Terminal(pane_id), + should_float_if_hidden, + client_id, + )) + .with_context(err_context)?; + }, + Action::FocusPluginPaneWithId(pane_id, should_float_if_hidden) => { + senders + .send_to_screen(ScreenInstruction::FocusPaneWithId( + PaneId::Plugin(pane_id), + should_float_if_hidden, + client_id, + )) + .with_context(err_context)?; + }, + Action::RenameTerminalPane(pane_id, name_bytes) => { + senders + .send_to_screen(ScreenInstruction::RenamePane( + PaneId::Terminal(pane_id), + name_bytes, + )) + .with_context(err_context)?; + }, + Action::RenamePluginPane(pane_id, name_bytes) => { + senders + .send_to_screen(ScreenInstruction::RenamePane( + PaneId::Plugin(pane_id), + name_bytes, + )) + .with_context(err_context)?; + }, + Action::RenameTab(tab_position, name_bytes) => { + senders + .send_to_screen(ScreenInstruction::RenameTab( + tab_position as usize, + name_bytes, + )) + .with_context(err_context)?; + }, } Ok(should_break) } diff --git a/zellij-server/src/screen.rs b/zellij-server/src/screen.rs index ae62be37..2bb93841 100644 --- a/zellij-server/src/screen.rs +++ b/zellij-server/src/screen.rs @@ -274,7 +274,10 @@ pub enum ScreenInstruction { ProgressPluginLoadingOffset(u32), // u32 - plugin id RequestStateUpdateForPlugins, LaunchOrFocusPlugin(RunPlugin, bool, ClientId), // bool is should_float - SuppressPane(PaneId, ClientId), + SuppressPane(PaneId, ClientId), // bool is should_float + FocusPaneWithId(PaneId, bool, ClientId), // bool is should_float + RenamePane(PaneId, Vec), + RenameTab(usize, Vec), } impl From<&ScreenInstruction> for ScreenContext { @@ -439,6 +442,9 @@ impl From<&ScreenInstruction> for ScreenContext { }, ScreenInstruction::LaunchOrFocusPlugin(..) => ScreenContext::LaunchOrFocusPlugin, ScreenInstruction::SuppressPane(..) => ScreenContext::SuppressPane, + ScreenInstruction::FocusPaneWithId(..) => ScreenContext::FocusPaneWithId, + ScreenInstruction::RenamePane(..) => ScreenContext::RenamePane, + ScreenInstruction::RenameTab(..) => ScreenContext::RenameTab, } } } @@ -1521,6 +1527,34 @@ impl Screen { } } + pub fn focus_pane_with_id( + &mut self, + pane_id: PaneId, + should_float_if_hidden: bool, + client_id: ClientId, + ) -> Result<()> { + let err_context = || format!("failed to focus_plugin_pane"); + let tab_index = self + .tabs + .iter() + .find(|(_tab_index, tab)| tab.has_pane_with_pid(&pane_id)) + .map(|(tab_index, _tab)| *tab_index); + match tab_index { + Some(tab_index) => { + self.go_to_tab(tab_index + 1, client_id)?; + self.tabs + .get_mut(&tab_index) + .with_context(err_context)? + .focus_pane_with_id(pane_id, should_float_if_hidden, client_id) + .context("failed to focus pane with id")?; + }, + None => { + log::error!("Could not find pane with id: {:?}", pane_id); + }, + }; + Ok(()) + } + fn unblock_input(&self) -> Result<()> { self.bus .senders @@ -2753,6 +2787,35 @@ pub(crate) fn screen_thread_main( } screen.report_pane_state()?; }, + ScreenInstruction::FocusPaneWithId(pane_id, should_float_if_hidden, client_id) => { + screen.focus_pane_with_id(pane_id, should_float_if_hidden, client_id)?; + screen.report_pane_state()?; + screen.report_tab_state()?; + }, + ScreenInstruction::RenamePane(pane_id, new_name) => { + let all_tabs = screen.get_tabs_mut(); + for tab in all_tabs.values_mut() { + if tab.has_pane_with_pid(&pane_id) { + match tab.rename_pane(new_name, pane_id) { + Ok(()) => drop(screen.render()), + Err(e) => log::error!("Failed to rename pane: {:?}", e), + } + break; + } + } + screen.report_pane_state()?; + }, + ScreenInstruction::RenameTab(tab_index, new_name) => { + match screen.tabs.get_mut(&tab_index.saturating_sub(1)) { + Some(tab) => { + tab.name = String::from_utf8_lossy(&new_name).to_string(); + }, + None => { + log::error!("Failed to find tab with index: {:?}", tab_index); + }, + } + screen.report_tab_state()?; + }, } } Ok(()) diff --git a/zellij-server/src/tab/mod.rs b/zellij-server/src/tab/mod.rs index cf326b86..f1326c31 100644 --- a/zellij-server/src/tab/mod.rs +++ b/zellij-server/src/tab/mod.rs @@ -456,6 +456,7 @@ pub trait Pane { fn exit_status(&self) -> Option { None } + fn rename(&mut self, _buf: Vec) {} } #[derive(Clone, Debug)] @@ -3073,6 +3074,23 @@ impl Tab { Ok(()) } + pub fn rename_pane(&mut self, buf: Vec, pane_id: PaneId) -> Result<()> { + let err_context = || { + format!( + "failed to update name of active pane to '{buf:?}' for pane_id {:?}", + pane_id + ) + }; + let pane = self + .floating_panes + .get_pane_mut(pane_id) + .or_else(|| self.tiled_panes.get_pane_mut(pane_id)) + .or_else(|| self.suppressed_panes.get_mut(&pane_id)) + .with_context(err_context)?; + pane.rename(buf); + Ok(()) + } + pub fn undo_active_rename_pane(&mut self, client_id: ClientId) -> Result<()> { if let Some(active_terminal_id) = self.get_active_terminal_id(client_id) { let active_terminal = if self.are_floating_panes_visible() { @@ -3282,6 +3300,7 @@ impl Tab { should_float: bool, client_id: ClientId, ) -> Result<()> { + // TODO: should error if pane is not selectable self.tiled_panes .focus_pane_if_exists(pane_id, client_id) .or_else(|_| { diff --git a/zellij-tile/src/shim.rs b/zellij-tile/src/shim.rs index 0a3102a1..cfa02eaf 100644 --- a/zellij-tile/src/shim.rs +++ b/zellij-tile/src/shim.rs @@ -1,4 +1,5 @@ use serde::{de::DeserializeOwned, Serialize}; +use std::str::FromStr; use std::{io, path::Path}; use zellij_utils::data::*; use zellij_utils::errors::prelude::*; @@ -90,6 +91,10 @@ pub fn hide_self() { unsafe { host_hide_self() }; } +pub fn show_self(should_float_if_hidden: bool) { + unsafe { host_show_self(should_float_if_hidden as i32) }; +} + pub fn switch_to_input_mode(mode: &InputMode) { object_to_stdout(&mode); unsafe { host_switch_to_mode() }; @@ -269,6 +274,58 @@ pub fn start_or_reload_plugin(url: &str) { unsafe { host_start_or_reload_plugin() }; } +pub fn close_terminal_pane(terminal_pane_id: i32) { + unsafe { host_close_terminal_pane(terminal_pane_id) }; +} + +pub fn close_plugin_pane(plugin_pane_id: i32) { + unsafe { host_close_plugin_pane(plugin_pane_id) }; +} + +pub fn focus_terminal_pane(terminal_pane_id: i32, should_float_if_hidden: bool) { + unsafe { host_focus_terminal_pane(terminal_pane_id, should_float_if_hidden as i32) }; +} + +pub fn focus_plugin_pane(plugin_pane_id: i32, should_float_if_hidden: bool) { + unsafe { host_focus_plugin_pane(plugin_pane_id, should_float_if_hidden as i32) }; +} + +pub fn rename_terminal_pane(terminal_pane_id: i32, new_name: &str) { + match String::from_str(new_name) { + Ok(new_name) => { + object_to_stdout(&(terminal_pane_id, new_name)); + unsafe { host_rename_terminal_pane() }; + }, + Err(e) => { + eprintln!("Failed to rename terminal: {:?}", e) + }, + } +} + +pub fn rename_plugin_pane(plugin_pane_id: i32, new_name: &str) { + match String::from_str(new_name) { + Ok(new_name) => { + object_to_stdout(&(plugin_pane_id, new_name)); + unsafe { host_rename_plugin_pane() }; + }, + Err(e) => { + eprintln!("Failed to rename plugin: {:?}", e) + }, + } +} + +pub fn rename_tab(tab_position: i32, new_name: &str) { + match String::from_str(new_name) { + Ok(new_name) => { + object_to_stdout(&(tab_position, new_name)); + unsafe { host_rename_tab() }; + }, + Err(e) => { + eprintln!("Failed to rename tab: {:?}", e) + }, + } +} + // Internal Functions #[doc(hidden)] @@ -282,6 +339,7 @@ pub fn object_from_stdin() -> Result { #[doc(hidden)] pub fn object_to_stdout(object: &impl Serialize) { + // TODO: no crashy println!("{}", serde_json::to_string(object).unwrap()); } @@ -325,6 +383,7 @@ extern "C" { fn host_post_message_to(); fn host_post_message_to_plugin(); fn host_hide_self(); + fn host_show_self(should_float_if_hidden: i32); fn host_switch_to_mode(); fn host_new_tabs_with_layout(); fn host_new_tab(); @@ -365,4 +424,11 @@ extern "C" { fn host_focus_or_create_tab(); fn host_go_to_tab(tab_index: i32); fn host_start_or_reload_plugin(); + fn host_close_terminal_pane(terminal_pane: i32); + fn host_close_plugin_pane(plugin_pane: i32); + fn host_focus_terminal_pane(terminal_pane: i32, should_float_if_hidden: i32); + fn host_focus_plugin_pane(plugin_pane: i32, should_float_if_hidden: i32); + fn host_rename_terminal_pane(); + fn host_rename_plugin_pane(); + fn host_rename_tab(); } diff --git a/zellij-utils/src/errors.rs b/zellij-utils/src/errors.rs index 78468891..e434067e 100644 --- a/zellij-utils/src/errors.rs +++ b/zellij-utils/src/errors.rs @@ -335,6 +335,9 @@ pub enum ScreenContext { RequestStateUpdateForPlugins, LaunchOrFocusPlugin, SuppressPane, + FocusPaneWithId, + RenamePane, + RenameTab, } /// Stack call representations corresponding to the different types of [`PtyInstruction`]s. diff --git a/zellij-utils/src/input/actions.rs b/zellij-utils/src/input/actions.rs index f6e080d8..c2071846 100644 --- a/zellij-utils/src/input/actions.rs +++ b/zellij-utils/src/input/actions.rs @@ -233,6 +233,13 @@ pub enum Action { NewTiledPluginPane(RunPluginLocation, Option), // String is an optional name NewFloatingPluginPane(RunPluginLocation, Option), // String is an optional name StartOrReloadPlugin(RunPlugin), + CloseTerminalPane(u32), + ClosePluginPane(u32), + FocusTerminalPaneWithId(u32, bool), // bool is should_float_if_hidden + FocusPluginPaneWithId(u32, bool), // bool is should_float_if_hidden + RenameTerminalPane(u32, Vec), + RenamePluginPane(u32, Vec), + RenameTab(u32, Vec), } impl Action {