diff --git a/Cargo.lock b/Cargo.lock index 33c7629621..b410d17f46 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -75,11 +75,11 @@ checksum = "8da52d66c7071e2e3fa2a1e5c6d088fec47b593032b254f5e980de8ea54454d6" [[package]] name = "async-channel" -version = "1.6.1" +version = "1.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2114d64672151c0c5eaa5e131ec84a74f06e1e559830dabba01ca30605d66319" +checksum = "cf46fee83e5ccffc220104713af3292ff9bc7c64c7de289f66dae8e38d826833" dependencies = [ - "concurrent-queue", + "concurrent-queue 2.2.0", "event-listener", "futures-core", ] @@ -91,7 +91,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "871f9bb5e0a22eeb7e8cf16641feb87c9dc67032ccf8ff49e772eb9941d3a965" dependencies = [ "async-task", - "concurrent-queue", + "concurrent-queue 1.2.2", "fastrand", "futures-lite", "once_cell", @@ -119,7 +119,7 @@ version = "1.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e5e18f61464ae81cde0a23e713ae8fd299580c54d697a35820cfd0625b8b0e07" dependencies = [ - "concurrent-queue", + "concurrent-queue 1.2.2", "futures-lite", "libc", "log", @@ -501,6 +501,15 @@ dependencies = [ "cache-padded", ] +[[package]] +name = "concurrent-queue" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62ec6771ecfa0762d24683ee5a32ad78487a3d3afdc0fb8cae19d2c5deb50b7c" +dependencies = [ + "crossbeam-utils", +] + [[package]] name = "console" version = "0.15.0" @@ -679,12 +688,11 @@ dependencies = [ [[package]] name = "crossbeam-utils" -version = "0.8.8" +version = "0.8.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0bf124c720b7686e3c2663cf54062ab0f68a88af2fb6a030e87e30bf721fcb38" +checksum = "3c063cd8cc95f5c377ed0d4b49a4b21f632396ff690e8470c29b3359b346984b" dependencies = [ "cfg-if 1.0.0", - "lazy_static", ] [[package]] @@ -1008,6 +1016,18 @@ dependencies = [ "winapi", ] +[[package]] +name = "filetime" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5cbc844cecaee9d4443931972e1289c8ff485cb4cc2767cb03ca139ed6885153" +dependencies = [ + "cfg-if 1.0.0", + "libc", + "redox_syscall", + "windows-sys 0.48.0", +] + [[package]] name = "finl_unicode" version = "1.2.0" @@ -1045,6 +1065,15 @@ dependencies = [ "percent-encoding", ] +[[package]] +name = "fsevent-sys" +version = "4.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76ee7a02da4d231650c7cea31349b889be2f45ddb3ef3032d2ec8185f6313fd2" +dependencies = [ + "libc", +] + [[package]] name = "futures-channel" version = "0.3.21" @@ -1277,6 +1306,26 @@ dependencies = [ "serde", ] +[[package]] +name = "inotify" +version = "0.9.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8069d3ec154eb856955c1c0fbffefbf5f3c40a104ec912d4797314c1801abff" +dependencies = [ + "bitflags", + "inotify-sys", + "libc", +] + +[[package]] +name = "inotify-sys" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e05c02b5e89bff3b946cedeca278abc628fe811e604f027c45a8aa3cf793d0eb" +dependencies = [ + "libc", +] + [[package]] name = "insta" version = "1.14.0" @@ -1391,6 +1440,26 @@ dependencies = [ "thiserror", ] +[[package]] +name = "kqueue" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c8fc60ba15bf51257aa9807a48a61013db043fcf3a78cb0d916e8e396dcad98" +dependencies = [ + "kqueue-sys", + "libc", +] + +[[package]] +name = "kqueue-sys" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8367585489f01bc55dd27404dcf56b95e6da061a256a666ab23be9ba96a2e587" +dependencies = [ + "bitflags", + "libc", +] + [[package]] name = "kv-log-macro" version = "1.0.7" @@ -1663,6 +1732,18 @@ dependencies = [ "winapi", ] +[[package]] +name = "mio" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b9d9a46eff5b4ff64b45a9e316a6d1e0bc719ef429cbec4dc630684212bfdf9" +dependencies = [ + "libc", + "log", + "wasi 0.11.0+wasi-snapshot-preview1", + "windows-sys 0.45.0", +] + [[package]] name = "miow" version = "0.3.7" @@ -1732,6 +1813,24 @@ dependencies = [ "minimal-lexical", ] +[[package]] +name = "notify" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4d9ba6c734de18ca27c8cef5cd7058aa4ac9f63596131e4c7e41e579319032a2" +dependencies = [ + "bitflags", + "crossbeam-channel", + "filetime", + "fsevent-sys", + "inotify", + "kqueue", + "libc", + "mio 0.8.6", + "walkdir", + "windows-sys 0.45.0", +] + [[package]] name = "ntapi" version = "0.3.7" @@ -2838,6 +2937,7 @@ dependencies = [ "pretty-bytes", "serde", "serde_json", + "strip-ansi-escapes", "unicode-width", "walkdir", "zellij-tile", @@ -4243,7 +4343,7 @@ version = "0.37.0" dependencies = [ "insta", "log", - "mio", + "mio 0.7.14", "serde", "serde_json", "serde_yaml", @@ -4306,6 +4406,7 @@ name = "zellij-utils" version = "0.37.0" dependencies = [ "anyhow", + "async-channel", "async-std", "backtrace", "clap", @@ -4323,6 +4424,7 @@ dependencies = [ "log4rs", "miette", "nix 0.23.1", + "notify", "once_cell", "percent-encoding", "regex", diff --git a/default-plugins/fixture-plugin-for-tests/src/main.rs b/default-plugins/fixture-plugin-for-tests/src/main.rs index 124b2dc75a..7e4139f55f 100644 --- a/default-plugins/fixture-plugin-for-tests/src/main.rs +++ b/default-plugins/fixture-plugin-for-tests/src/main.rs @@ -32,7 +32,7 @@ impl<'de> ZellijWorker<'de> for TestWorker { } register_plugin!(State); -register_worker!(TestWorker, test_worker); +register_worker!(TestWorker, test_worker, TEST_WORKER); impl ZellijPlugin for State { fn load(&mut self) { @@ -40,6 +40,10 @@ impl ZellijPlugin for State { EventType::InputReceived, EventType::SystemClipboardFailure, EventType::CustomMessage, + EventType::FileSystemCreate, + EventType::FileSystemRead, + EventType::FileSystemUpdate, + EventType::FileSystemDelete, ]); } diff --git a/default-plugins/strider/Cargo.toml b/default-plugins/strider/Cargo.toml index d45a8ff21f..65b5d23e98 100644 --- a/default-plugins/strider/Cargo.toml +++ b/default-plugins/strider/Cargo.toml @@ -16,3 +16,4 @@ serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" unicode-width = "0.1.8" ansi_term = "0.12.1" +strip-ansi-escapes = "0.1.1" diff --git a/default-plugins/strider/src/main.rs b/default-plugins/strider/src/main.rs index 4f299c5083..966b53a4f8 100644 --- a/default-plugins/strider/src/main.rs +++ b/default-plugins/strider/src/main.rs @@ -2,26 +2,45 @@ mod search; mod state; use colored::*; -use search::{ResultsOfSearch, SearchWorker}; +use search::{FileContentsWorker, FileNameWorker, MessageToSearch, ResultsOfSearch}; +use serde::{Deserialize, Serialize}; use serde_json; -use state::{refresh_directory, FsEntry, State, CURRENT_SEARCH_TERM}; +use state::{refresh_directory, FsEntry, State}; use std::{cmp::min, time::Instant}; use zellij_tile::prelude::*; register_plugin!(State); -register_worker!(SearchWorker, search_worker); +register_worker!(FileNameWorker, file_name_search_worker, FILE_NAME_WORKER); +register_worker!( + FileContentsWorker, + file_contents_search_worker, + FILE_CONTENTS_WORKER +); impl ZellijPlugin for State { fn load(&mut self) { refresh_directory(self); - self.loading = true; + self.search_state.loading = true; subscribe(&[ EventType::Key, EventType::Mouse, EventType::CustomMessage, EventType::Timer, + EventType::FileSystemCreate, + EventType::FileSystemUpdate, + EventType::FileSystemDelete, ]); - post_message_to("search", String::from("scan_folder"), String::new()); + post_message_to( + "file_name_search", + serde_json::to_string(&MessageToSearch::ScanFolder).unwrap(), + "".to_owned(), + ); + post_message_to( + "file_contents_search", + serde_json::to_string(&MessageToSearch::ScanFolder).unwrap(), + "".to_owned(), + ); + self.search_state.loading = true; set_timeout(0.5); // for displaying loading animation } @@ -35,57 +54,43 @@ impl ZellijPlugin for State { self.ev_history.push_back((event.clone(), Instant::now())); match event { Event::Timer(_elapsed) => { - should_render = true; - if self.loading { + if self.search_state.loading { set_timeout(0.5); - if self.loading_animation_offset == u8::MAX { - self.loading_animation_offset = 0; - } else { - self.loading_animation_offset = - self.loading_animation_offset.saturating_add(1); - } + self.search_state.progress_animation(); + should_render = true; } }, - Event::CustomMessage(message, payload) => match message.as_str() { - "update_search_results" => { - if let Ok(mut results_of_search) = - serde_json::from_str::(&payload) + Event::CustomMessage(message, payload) => match serde_json::from_str(&message) { + Ok(MessageToPlugin::UpdateFileNameSearchResults) => { + if let Ok(results_of_search) = serde_json::from_str::(&payload) { - if Some(results_of_search.search_term) == self.search_term { - self.search_results = - results_of_search.search_results.drain(..).collect(); - should_render = true; - } + self.search_state + .update_file_name_search_results(results_of_search); + should_render = true; + } + }, + Ok(MessageToPlugin::UpdateFileContentsSearchResults) => { + if let Ok(results_of_search) = serde_json::from_str::(&payload) + { + self.search_state + .update_file_contents_search_results(results_of_search); + should_render = true; } }, - "done_scanning_folder" => { - self.loading = false; + Ok(MessageToPlugin::DoneScanningFolder) => { + self.search_state.loading = false; should_render = true; }, - _ => {}, + Err(e) => eprintln!("Failed to deserialize custom message: {:?}", e), }, Event::Key(key) => match key { - // modes: - // 1. typing_search_term - // 2. exploring_search_results - // 3. normal - Key::Esc | Key::Char('\n') if self.typing_search_term() => { - self.accept_search_term(); - }, - _ if self.typing_search_term() => { - self.append_to_search_term(key); - if let Some(search_term) = self.search_term.as_ref() { - std::fs::write(CURRENT_SEARCH_TERM, search_term.as_bytes()).unwrap(); - post_message_to( - "search", - String::from("search"), - String::from(&self.search_term.clone().unwrap()), - ); - } + Key::Esc if self.typing_search_term() => { + self.stop_typing_search_term(); + self.search_state.handle_key(key); should_render = true; }, - Key::Esc if self.exploring_search_results() => { - self.stop_exploring_search_results(); + _ if self.typing_search_term() => { + self.search_state.handle_key(key); should_render = true; }, Key::Char('/') => { @@ -94,40 +99,27 @@ impl ZellijPlugin for State { }, Key::Esc => { self.stop_typing_search_term(); + hide_self(); should_render = true; }, Key::Up | Key::Char('k') => { - if self.exploring_search_results() { - self.move_search_selection_up(); + let currently_selected = self.selected(); + *self.selected_mut() = self.selected().saturating_sub(1); + if currently_selected != self.selected() { should_render = true; - } else { - let currently_selected = self.selected(); - *self.selected_mut() = self.selected().saturating_sub(1); - if currently_selected != self.selected() { - should_render = true; - } } }, Key::Down | Key::Char('j') => { - if self.exploring_search_results() { - self.move_search_selection_down(); + let currently_selected = self.selected(); + let next = self.selected().saturating_add(1); + *self.selected_mut() = min(self.files.len().saturating_sub(1), next); + if currently_selected != self.selected() { should_render = true; - } else { - let currently_selected = self.selected(); - let next = self.selected().saturating_add(1); - *self.selected_mut() = min(self.files.len().saturating_sub(1), next); - if currently_selected != self.selected() { - should_render = true; - } } }, Key::Right | Key::Char('\n') | Key::Char('l') if !self.files.is_empty() => { - if self.exploring_search_results() { - self.open_search_result(); - } else { - self.traverse_dir_or_open_file(); - self.ev_history.clear(); - } + self.traverse_dir_or_open_file(); + self.ev_history.clear(); should_render = true; }, Key::Left | Key::Char('h') => { @@ -190,6 +182,54 @@ impl ZellijPlugin for State { }, _ => {}, }, + Event::FileSystemCreate(paths) => { + let paths: Vec = paths + .iter() + .map(|p| p.to_string_lossy().to_string()) + .collect(); + post_message_to( + "file_name_search", + serde_json::to_string(&MessageToSearch::FileSystemCreate).unwrap(), + serde_json::to_string(&paths).unwrap(), + ); + post_message_to( + "file_contents_search", + serde_json::to_string(&MessageToSearch::FileSystemCreate).unwrap(), + serde_json::to_string(&paths).unwrap(), + ); + }, + Event::FileSystemUpdate(paths) => { + let paths: Vec = paths + .iter() + .map(|p| p.to_string_lossy().to_string()) + .collect(); + post_message_to( + "file_name_search", + serde_json::to_string(&MessageToSearch::FileSystemUpdate).unwrap(), + serde_json::to_string(&paths).unwrap(), + ); + post_message_to( + "file_contents_search", + serde_json::to_string(&MessageToSearch::FileSystemUpdate).unwrap(), + serde_json::to_string(&paths).unwrap(), + ); + }, + Event::FileSystemDelete(paths) => { + let paths: Vec = paths + .iter() + .map(|p| p.to_string_lossy().to_string()) + .collect(); + post_message_to( + "file_name_search", + serde_json::to_string(&MessageToSearch::FileSystemDelete).unwrap(), + serde_json::to_string(&paths).unwrap(), + ); + post_message_to( + "file_contents_search", + serde_json::to_string(&MessageToSearch::FileSystemDelete).unwrap(), + serde_json::to_string(&paths).unwrap(), + ); + }, _ => { dbg!("Unknown event {:?}", event); }, @@ -198,8 +238,10 @@ impl ZellijPlugin for State { } fn render(&mut self, rows: usize, cols: usize) { - if self.typing_search_term() || self.exploring_search_results() { - return self.render_search(rows, cols); + if self.typing_search_term() { + self.search_state.change_size(rows, cols); + print!("{}", self.search_state); + return; } for i in 0..rows { @@ -221,9 +263,9 @@ impl ZellijPlugin for State { if i == self.selected() { if is_last_row { - print!("{}", path.reversed()); + print!("{}", path.clone().reversed()); } else { - println!("{}", path.reversed()); + println!("{}", path.clone().reversed()); } } else { if is_last_row { @@ -238,3 +280,10 @@ impl ZellijPlugin for State { } } } + +#[derive(Serialize, Deserialize)] +pub enum MessageToPlugin { + UpdateFileNameSearchResults, + UpdateFileContentsSearchResults, + DoneScanningFolder, +} diff --git a/default-plugins/strider/src/search.rs b/default-plugins/strider/src/search.rs deleted file mode 100644 index 299882eac0..0000000000 --- a/default-plugins/strider/src/search.rs +++ /dev/null @@ -1,415 +0,0 @@ -use crate::state::{State, CURRENT_SEARCH_TERM, ROOT}; - -use unicode_width::UnicodeWidthStr; -use zellij_tile::prelude::*; - -use fuzzy_matcher::skim::SkimMatcherV2; -use fuzzy_matcher::FuzzyMatcher; -use serde::{Deserialize, Serialize}; -use walkdir::WalkDir; - -use std::io::{self, BufRead}; - -#[derive(Serialize, Deserialize, Debug, Clone)] -pub enum SearchResult { - File { - path: String, - score: i64, - indices: Vec, - }, - LineInFile { - path: String, - line: String, - line_number: usize, - score: i64, - indices: Vec, - }, -} - -impl SearchResult { - pub fn new_file_name(score: i64, indices: Vec, path: String) -> Self { - SearchResult::File { - path, - score, - indices, - } - } - pub fn new_file_line( - score: i64, - indices: Vec, - path: String, - line: String, - line_number: usize, - ) -> Self { - SearchResult::LineInFile { - path, - score, - indices, - line, - line_number, - } - } - pub fn score(&self) -> i64 { - match self { - SearchResult::File { score, .. } => *score, - SearchResult::LineInFile { score, .. } => *score, - } - } - pub fn rendered_height(&self) -> usize { - match self { - SearchResult::File { .. } => 1, - SearchResult::LineInFile { .. } => 2, - } - } - pub fn render(&self, max_width: usize, is_selected: bool) -> String { - let green_code = 154; - let orange_code = 166; - let bold_code = "\u{1b}[1m"; - let green_foreground = format!("\u{1b}[38;5;{}m", green_code); - let orange_foreground = format!("\u{1b}[38;5;{}m", orange_code); - let reset_code = "\u{1b}[m"; - let max_width = max_width.saturating_sub(3); // for the UI left line separator - match self { - SearchResult::File { path, indices, .. } => { - if is_selected { - let line = self.render_line_with_indices( - path, - indices, - max_width, - None, - Some(green_code), - true, - ); - format!("{} | {}{}", green_foreground, reset_code, line) - } else { - let line = - self.render_line_with_indices(path, indices, max_width, None, None, true); - format!(" | {}", line) - } - }, - SearchResult::LineInFile { - path, - line, - line_number, - indices, - .. - } => { - if is_selected { - let first_line = self.render_line_with_indices( - path, - &vec![], - max_width, - None, - Some(green_code), - true, - ); - let line_indication_text = format!("{}-> {}", bold_code, line_number); - let line_indication = format!( - "{}{}{}", - orange_foreground, line_indication_text, reset_code - ); // TODO: also truncate - let second_line = self.render_line_with_indices( - line, - indices, - max_width.saturating_sub(line_indication_text.width()), - None, - Some(orange_code), - false, - ); - format!( - " {}│{} {}\n {}│{} {} {}", - green_foreground, - reset_code, - first_line, - green_foreground, - reset_code, - line_indication, - second_line - ) - } else { - let first_line = - self.render_line_with_indices(path, &vec![], max_width, None, None, true); // TODO: - let line_indication_text = format!("{}-> {}", bold_code, line_number); - let second_line = self.render_line_with_indices( - line, - indices, - max_width.saturating_sub(line_indication_text.width()), - None, - None, - false, - ); - format!( - " │ {}\n │ {} {}", - first_line, line_indication_text, second_line - ) - } - }, - } - } - fn render_line_with_indices( - &self, - line_to_render: &String, - indices: &Vec, - max_width: usize, - background_color: Option, - foreground_color: Option, - is_bold: bool, - ) -> String { - // TODO: get these from Zellij - let reset_code = "\u{1b}[m"; - let underline_code = "\u{1b}[4m"; - let foreground_color = foreground_color - .map(|c| format!("\u{1b}[38;5;{}m", c)) - .unwrap_or_else(|| format!("")); - let background_color = background_color - .map(|c| format!("\u{1b}[48;5;{}m", c)) - .unwrap_or_else(|| format!("")); - let bold = if is_bold { "\u{1b}[1m" } else { "" }; - let non_index_character_style = format!("{}{}{}", background_color, foreground_color, bold); - let index_character_style = format!( - "{}{}{}{}", - background_color, foreground_color, bold, underline_code - ); - - let mut truncate_start_position = None; - let mut truncate_end_position = None; - if line_to_render.width() > max_width { - let length_of_each_half = max_width.saturating_sub(4) / 2; - truncate_start_position = Some(length_of_each_half); - truncate_end_position = - Some(line_to_render.width().saturating_sub(length_of_each_half)); - } - let mut first_half = format!("{}", reset_code); - let mut second_half = format!("{}", reset_code); - for (i, character) in line_to_render.chars().enumerate() { - if (truncate_start_position.is_none() && truncate_end_position.is_none()) - || Some(i) < truncate_start_position - { - if indices.contains(&i) { - first_half.push_str(&index_character_style); - first_half.push(character); - first_half.push_str(reset_code); - } else { - first_half.push_str(&non_index_character_style); - first_half.push(character); - first_half.push_str(reset_code); - } - } else if Some(i) > truncate_end_position { - if indices.contains(&i) { - second_half.push_str(&index_character_style); - second_half.push(character); - second_half.push_str(reset_code); - } else { - second_half.push_str(&non_index_character_style); - second_half.push(character); - second_half.push_str(reset_code); - } - } - } - if let Some(_truncate_start_position) = truncate_start_position { - format!( - "{}{}{}[..]{}{}{}", - first_half, reset_code, foreground_color, reset_code, second_half, reset_code - ) - } else { - format!("{}{}", first_half, reset_code) - } - } -} - -#[derive(Serialize, Deserialize, Debug, Clone)] -pub struct ResultsOfSearch { - pub search_term: String, - pub search_results: Vec, -} - -impl ResultsOfSearch { - pub fn new(search_term: String, search_results: Vec) -> Self { - ResultsOfSearch { - search_term, - search_results, - } - } - pub fn limit_search_results(mut self, max_results: usize) -> Self { - self.search_results - .sort_by(|a, b| b.score().cmp(&a.score())); - self.search_results = if self.search_results.len() > max_results { - self.search_results.drain(..max_results).collect() - } else { - self.search_results.drain(..).collect() - }; - self - } -} - -#[derive(Default, Serialize, Deserialize)] -pub struct SearchWorker { - pub search_paths: Vec, - pub search_file_contents: Vec<(String, usize, String)>, // file_name, line_number, line - skip_hidden_files: bool, -} - -impl<'de> ZellijWorker<'de> for SearchWorker { - // TODO: handle out of order messages, likely when rendering - fn on_message(&mut self, message: String, payload: String) { - match message.as_str() { - // TODO: deserialize to type - "scan_folder" => { - self.populate_search_paths(); - post_message_to_plugin("done_scanning_folder".into(), "".into()); - }, - "search" => { - let search_term = payload; - let (search_term, matches) = self.search(search_term); - let search_results = - ResultsOfSearch::new(search_term, matches).limit_search_results(100); - post_message_to_plugin( - "update_search_results".into(), - serde_json::to_string(&search_results).unwrap(), - ); - }, - "skip_hidden_files" => match serde_json::from_str::(&payload) { - Ok(should_skip_hidden_files) => { - self.skip_hidden_files = should_skip_hidden_files; - }, - Err(e) => { - eprintln!("Failed to deserialize payload: {:?}", e); - }, - }, - _ => {}, - } - } -} - -impl SearchWorker { - fn search(&mut self, search_term: String) -> (String, Vec) { - if self.search_paths.is_empty() { - self.populate_search_paths(); - } - let mut matches = vec![]; - let mut matcher = SkimMatcherV2::default().use_cache(true).element_limit(100); // TODO: no hard - // coded limit! - self.search_file_names(&search_term, &mut matcher, &mut matches); - self.search_file_contents(&search_term, &mut matcher, &mut matches); - - // if the search term changed before we finished, let's search again! - if let Ok(current_search_term) = std::fs::read(CURRENT_SEARCH_TERM) { - let current_search_term = String::from_utf8_lossy(¤t_search_term); // TODO: not lossy, search can be lots of stuff - if current_search_term != search_term { - return self.search(current_search_term.into()); - } - } - (search_term, matches) - } - fn populate_search_paths(&mut self) { - for entry in WalkDir::new(ROOT).into_iter().filter_map(|e| e.ok()) { - if self.skip_hidden_files - && entry - .file_name() - .to_str() - .map(|s| s.starts_with('.')) - .unwrap_or(false) - { - continue; - } - let file_path = entry.path().display().to_string(); - - if entry.metadata().unwrap().is_file() { - if let Ok(file) = std::fs::File::open(&file_path) { - let lines = io::BufReader::new(file).lines(); - for (index, line) in lines.enumerate() { - match line { - Ok(line) => { - self.search_file_contents.push(( - file_path.clone(), - index + 1, - line, - )); - }, - Err(_) => { - break; // probably a binary file, skip it - }, - } - } - } - } - - self.search_paths.push(file_path); - } - } - fn search_file_names( - &self, - search_term: &str, - matcher: &mut SkimMatcherV2, - matches: &mut Vec, - ) { - for entry in &self.search_paths { - if let Some((score, indices)) = matcher.fuzzy_indices(&entry, &search_term) { - matches.push(SearchResult::new_file_name( - score, - indices, - entry.to_owned(), - )); - } - } - } - fn search_file_contents( - &self, - search_term: &str, - matcher: &mut SkimMatcherV2, - matches: &mut Vec, - ) { - for (file_name, line_number, line_entry) in &self.search_file_contents { - if let Some((score, indices)) = matcher.fuzzy_indices(&line_entry, &search_term) { - matches.push(SearchResult::new_file_line( - score, - indices, - file_name.clone(), - line_entry.clone(), - *line_number, - )); - } - } - } -} - -impl State { - pub fn render_search(&mut self, rows: usize, cols: usize) { - if let Some(search_term) = self.search_term.as_ref() { - let mut to_render = String::new(); - to_render.push_str(&format!( - " \u{1b}[38;5;51;1mSEARCH:\u{1b}[m {}\n", - search_term - )); - let mut rows_left_to_render = rows.saturating_sub(3); - if self.loading && self.search_results.is_empty() { - to_render.push_str(&self.render_loading()); - } - for (i, result) in self - .search_results - .iter() - .enumerate() - .take(rows.saturating_sub(3)) - { - let result_height = result.rendered_height(); - if result_height + 1 > rows_left_to_render { - break; - } - rows_left_to_render -= result_height; - rows_left_to_render -= 1; // space between - let is_selected = i == self.selected_search_result; - let rendered_result = result.render(cols, is_selected); - to_render.push_str(&format!("\n{}\n", rendered_result)); - } - print!("{}", to_render); - } - } - pub fn render_loading(&self) -> String { - let mut rendered = String::from("Scanning folder"); - let dot_count = self.loading_animation_offset % 4; - for _ in 0..dot_count { - rendered.push('.'); - } - rendered - } -} diff --git a/default-plugins/strider/src/search/controls_line.rs b/default-plugins/strider/src/search/controls_line.rs new file mode 100644 index 0000000000..a75715ef12 --- /dev/null +++ b/default-plugins/strider/src/search/controls_line.rs @@ -0,0 +1,353 @@ +use crate::search::search_state::SearchType; +use crate::search::ui::{ + arrow, bold, color_line_to_end, dot, styled_text, BLACK, GRAY_DARK, GRAY_LIGHT, RED, WHITE, +}; + +#[derive(Default)] +pub struct ControlsLine { + controls: Vec, + scanning_indication: Option>, + animation_offset: u8, +} + +impl ControlsLine { + pub fn new(controls: Vec, scanning_indication: Option>) -> Self { + ControlsLine { + controls, + scanning_indication, + ..Default::default() + } + } + pub fn with_animation_offset(mut self, animation_offset: u8) -> Self { + self.animation_offset = animation_offset; + self + } + pub fn render(&self, max_width: usize, show_controls: bool) -> String { + if show_controls { + self.render_controls(max_width) + } else { + self.render_empty_line(max_width) + } + } + pub fn render_controls(&self, max_width: usize) -> String { + let loading_animation = + LoadingAnimation::new(&self.scanning_indication, self.animation_offset); + let full_length = loading_animation.full_len() + + self.controls.iter().map(|c| c.full_len()).sum::(); + let mid_length = + loading_animation.mid_len() + self.controls.iter().map(|c| c.mid_len()).sum::(); + let short_length = loading_animation.short_len() + + self.controls.iter().map(|c| c.short_len()).sum::(); + if max_width >= full_length { + let mut to_render = String::new(); + for control in &self.controls { + to_render.push_str(&control.render_full_length()); + } + to_render.push_str(&self.render_padding(max_width.saturating_sub(full_length))); + to_render.push_str(&loading_animation.render_full_length()); + to_render + } else if max_width >= mid_length { + let mut to_render = String::new(); + for control in &self.controls { + to_render.push_str(&control.render_mid_length()); + } + to_render.push_str(&self.render_padding(max_width.saturating_sub(mid_length))); + to_render.push_str(&loading_animation.render_mid_length()); + to_render + } else if max_width >= short_length { + let mut to_render = String::new(); + for control in &self.controls { + to_render.push_str(&control.render_short_length()); + } + to_render.push_str(&self.render_padding(max_width.saturating_sub(short_length))); + to_render.push_str(&loading_animation.render_short_length()); + to_render + } else { + format!("") + } + } + pub fn render_empty_line(&self, max_width: usize) -> String { + let loading_animation = + LoadingAnimation::new(&self.scanning_indication, self.animation_offset); + let mut to_render = String::new(); + if max_width >= loading_animation.full_len() { + to_render.push_str( + &self.render_padding(max_width.saturating_sub(loading_animation.full_len())), + ); + to_render.push_str(&loading_animation.render_full_length()); + } else if max_width >= loading_animation.mid_len() { + to_render.push_str( + &self.render_padding(max_width.saturating_sub(loading_animation.mid_len())), + ); + to_render.push_str(&loading_animation.render_mid_length()); + } else if max_width >= loading_animation.short_len() { + to_render.push_str( + &self.render_padding(max_width.saturating_sub(loading_animation.short_len())), + ); + to_render.push_str(&loading_animation.render_short_length()); + } + to_render + } + fn render_padding(&self, padding: usize) -> String { + // TODO: color whole line + format!("{}\u{1b}[{}C", color_line_to_end(GRAY_LIGHT), padding) + } +} + +pub struct Control { + key: &'static str, + options: Vec<&'static str>, + option_index: (usize, usize), // eg. 1 out of 2 (1, 2) + keycode_background_color: u8, + keycode_foreground_color: u8, + control_text_background_color: u8, + control_text_foreground_color: u8, + active_dot_color: u8, +} + +impl Default for Control { + fn default() -> Self { + Control { + key: "", + options: vec![], + option_index: (0, 0), + keycode_background_color: GRAY_LIGHT, + keycode_foreground_color: WHITE, + control_text_background_color: GRAY_DARK, + control_text_foreground_color: BLACK, + active_dot_color: RED, + } + } +} + +impl Control { + pub fn new( + key: &'static str, + options: Vec<&'static str>, + option_index: (usize, usize), + ) -> Self { + Control { + key, + options, + option_index, + ..Default::default() + } + } + pub fn new_floating_control(key: &'static str, should_open_floating: bool) -> Self { + if should_open_floating { + Control::new(key, vec!["OPEN FLOATING", "FLOATING", "F"], (2, 2)) + } else { + Control::new(key, vec!["OPEN TILED", "TILED", "T"], (1, 2)) + } + } + pub fn new_filter_control(key: &'static str, search_filter: &SearchType) -> Self { + match search_filter { + SearchType::NamesAndContents => Control::new( + key, + vec!["FILE NAMES AND CONTENTS", "NAMES + CONTENTS", "N+C"], + (1, 3), + ), + SearchType::Names => Control::new(key, vec!["FILE NAMES", "NAMES", "N"], (2, 3)), + SearchType::Contents => { + Control::new(key, vec!["FILE CONTENTS", "CONTENTS", "C"], (3, 3)) + }, + } + } + pub fn short_len(&self) -> usize { + let short_text = self + .options + .get(2) + .or_else(|| self.options.get(1)) + .or_else(|| self.options.get(0)) + .unwrap_or(&""); + short_text.chars().count() + self.key.chars().count() + self.option_index.1 + 7 + // 7 for all the spaces and decorations + } + pub fn mid_len(&self) -> usize { + let mid_text = self + .options + .get(1) + .or_else(|| self.options.get(0)) + .unwrap_or(&""); + mid_text.chars().count() + self.key.chars().count() + self.option_index.1 + 7 + // 7 for all the spaces and decorations + } + pub fn full_len(&self) -> usize { + let full_text = self.options.get(0).unwrap_or(&""); + full_text.chars().count() + self.key.chars().count() + self.option_index.1 + 7 + // 7 for all the spaces and decorations + } + pub fn render_short_length(&self) -> String { + let short_text = self + .options + .get(2) + .or_else(|| self.options.get(1)) + .or_else(|| self.options.get(0)) + .unwrap_or(&""); + self.render(short_text) + } + pub fn render_mid_length(&self) -> String { + let mid_text = self + .options + .get(1) + .or_else(|| self.options.get(0)) + .unwrap_or(&""); + self.render(mid_text) + } + pub fn render_full_length(&self) -> String { + let full_text = self.options.get(0).unwrap_or(&""); + self.render(full_text) + } + fn render(&self, text: &str) -> String { + format!( + "{}{}{}{}{}{}", + self.render_keycode(&format!(" {} ", self.key)), + arrow( + self.keycode_background_color, + self.control_text_background_color + ), + self.render_selection_dots(), + self.render_control_text(&format!("{} ", text)), + arrow( + self.control_text_background_color, + self.keycode_background_color + ), + color_line_to_end(self.keycode_background_color), + ) + } + fn render_keycode(&self, text: &str) -> String { + styled_text( + self.keycode_foreground_color, + self.keycode_background_color, + &bold(text), + ) + } + fn render_control_text(&self, text: &str) -> String { + styled_text( + self.control_text_foreground_color, + self.control_text_background_color, + &bold(text), + ) + } + fn render_selection_dots(&self) -> String { + let mut selection_dots = String::from(" "); + for i in 1..=self.option_index.1 { + if i == self.option_index.0 { + selection_dots.push_str(&dot( + self.active_dot_color, + self.control_text_background_color, + )); + } else { + selection_dots.push_str(&dot( + self.control_text_foreground_color, + self.control_text_background_color, + )); + } + } + selection_dots.push_str(" "); + selection_dots + } +} + +struct LoadingAnimation { + scanning_indication: Option>, + animation_offset: u8, + background_color: u8, + foreground_color: u8, +} +impl LoadingAnimation { + pub fn new(scanning_indication: &Option>, animation_offset: u8) -> Self { + LoadingAnimation { + scanning_indication: scanning_indication.clone(), + animation_offset, + background_color: GRAY_LIGHT, + foreground_color: WHITE, + } + } + pub fn full_len(&self) -> usize { + self.scanning_indication + .as_ref() + .and_then(|scanning_indication| scanning_indication.get(0)) + .map(|s| s.chars().count() + 3) // 3 for animation dots + .unwrap_or(0) + } + pub fn mid_len(&self) -> usize { + self.scanning_indication + .as_ref() + .and_then(|scanning_indication| { + scanning_indication + .get(1) + .or_else(|| scanning_indication.get(0)) + }) + .map(|s| s.chars().count() + 3) // 3 for animation dots + .unwrap_or(0) + } + pub fn short_len(&self) -> usize { + self.scanning_indication + .as_ref() + .and_then(|scanning_indication| { + scanning_indication + .get(2) + .or_else(|| scanning_indication.get(1)) + .or_else(|| scanning_indication.get(0)) + }) + .map(|s| s.chars().count() + 3) // 3 for animation dots + .unwrap_or(0) + } + pub fn render_full_length(&self) -> String { + self.scanning_indication + .as_ref() + .and_then(|scanning_indication| scanning_indication.get(0)) + .map(|s| { + styled_text( + self.foreground_color, + self.background_color, + &bold(&(s.to_string() + &self.animation_dots())), + ) + }) + .unwrap_or_else(String::new) + } + pub fn render_mid_length(&self) -> String { + self.scanning_indication + .as_ref() + .and_then(|scanning_indication| { + scanning_indication + .get(1) + .or_else(|| scanning_indication.get(0)) + }) + .map(|s| { + styled_text( + self.background_color, + self.foreground_color, + &bold(&(s.to_string() + &self.animation_dots())), + ) + }) + .unwrap_or_else(String::new) + } + pub fn render_short_length(&self) -> String { + self.scanning_indication + .as_ref() + .and_then(|scanning_indication| { + scanning_indication + .get(2) + .or_else(|| scanning_indication.get(1)) + .or_else(|| scanning_indication.get(0)) + }) + .map(|s| { + styled_text( + self.background_color, + self.foreground_color, + &bold(&(s.to_string() + &self.animation_dots())), + ) + }) + .unwrap_or_else(String::new) + } + fn animation_dots(&self) -> String { + let mut to_render = String::from(""); + let dot_count = self.animation_offset % 4; + for _ in 0..dot_count { + to_render.push('.'); + } + to_render + } +} diff --git a/default-plugins/strider/src/search/mod.rs b/default-plugins/strider/src/search/mod.rs new file mode 100644 index 0000000000..5aba9c4d9c --- /dev/null +++ b/default-plugins/strider/src/search/mod.rs @@ -0,0 +1,329 @@ +pub mod controls_line; +pub mod search_results; +pub mod search_state; +pub mod selection_controls_area; +pub mod ui; + +use crate::state::{CURRENT_SEARCH_TERM, ROOT}; +use crate::MessageToPlugin; +use search_state::SearchType; +use std::collections::{BTreeMap, BTreeSet, HashMap}; +use std::path::{Path, PathBuf}; + +use unicode_width::UnicodeWidthStr; +use zellij_tile::prelude::*; + +use fuzzy_matcher::skim::SkimMatcherV2; +use fuzzy_matcher::FuzzyMatcher; +use search_results::SearchResult; +use serde::{Deserialize, Serialize}; +use walkdir::WalkDir; + +use std::io::{self, BufRead}; + +#[derive(Default, Serialize, Deserialize)] +pub struct Search { + search_type: SearchType, + file_names: BTreeSet, + file_contents: BTreeMap<(String, usize), String>, // file_name, line_number, line + cached_file_name_results: HashMap>, + cached_file_contents_results: HashMap>, +} + +impl Search { + pub fn new(search_type: SearchType) -> Self { + Search { + search_type, + ..Default::default() + } + } + fn on_message(&mut self, message: String, payload: String) { + match serde_json::from_str::(&message) { + Ok(MessageToSearch::ScanFolder) => { + self.scan_hd(); + post_message_to_plugin( + serde_json::to_string(&MessageToPlugin::DoneScanningFolder).unwrap(), + "".to_owned(), + ); + }, + Ok(MessageToSearch::Search) => { + if let Some(current_search_term) = self.read_search_term_from_hd_cache() { + self.search(current_search_term); + } + }, + Ok(MessageToSearch::FileSystemCreate) => { + self.rescan_files(payload); + }, + Ok(MessageToSearch::FileSystemUpdate) => { + self.rescan_files(payload); + }, + Ok(MessageToSearch::FileSystemDelete) => { + self.delete_files(payload); + }, + Err(e) => eprintln!("Failed to deserialize worker message {:?}", e), + } + } + pub fn scan_hd(&mut self) { + for entry in WalkDir::new(ROOT).into_iter().filter_map(|e| e.ok()) { + self.add_file_entry(entry.path(), entry.metadata().ok()); + } + } + pub fn search(&mut self, search_term: String) { + let search_results_limit = 100; // artificial limit to prevent probably unwanted chaos + // let mut matcher = SkimMatcherV2::default().use_cache(true).element_limit(search_results_limit); + let mut file_names_search_results = None; + let mut file_contents_search_results = None; + if let SearchType::Names | SearchType::NamesAndContents = self.search_type { + let file_names_matches = match self.cached_file_name_results.get(&search_term) { + Some(cached_results) => cached_results.clone(), + None => { + let mut matcher = SkimMatcherV2::default().use_cache(true); + let results = self.search_file_names(&search_term, &mut matcher); + self.cached_file_name_results + .insert(search_term.clone(), results.clone()); + results + }, + }; + file_names_search_results = Some( + ResultsOfSearch::new(search_term.clone(), file_names_matches) + .limit_search_results(search_results_limit), + ); + }; + if let SearchType::Contents | SearchType::NamesAndContents = self.search_type { + let file_contents_matches = match self.cached_file_contents_results.get(&search_term) { + Some(cached_results) => cached_results.clone(), + None => { + let mut matcher = SkimMatcherV2::default().use_cache(true); + let results = self.search_file_contents(&search_term, &mut matcher); + self.cached_file_contents_results + .insert(search_term.clone(), results.clone()); + results + }, + }; + file_contents_search_results = Some( + ResultsOfSearch::new(search_term.clone(), file_contents_matches) + .limit_search_results(search_results_limit), + ); + }; + + // if the search term changed before we finished, let's search again! + if let Some(current_search_term) = self.read_search_term_from_hd_cache() { + if current_search_term != search_term { + return self.search(current_search_term.into()); + } + } + if let Some(file_names_search_results) = file_names_search_results { + post_message_to_plugin( + serde_json::to_string(&MessageToPlugin::UpdateFileNameSearchResults).unwrap(), + serde_json::to_string(&file_names_search_results).unwrap(), + ); + } + if let Some(file_contents_search_results) = file_contents_search_results { + post_message_to_plugin( + serde_json::to_string(&MessageToPlugin::UpdateFileContentsSearchResults).unwrap(), + serde_json::to_string(&file_contents_search_results).unwrap(), + ); + } + } + pub fn rescan_files(&mut self, paths: String) { + match serde_json::from_str::>(&paths) { + Ok(paths) => { + for path in paths { + self.add_file_entry(&path, path.metadata().ok()); + } + self.cached_file_name_results.clear(); + self.cached_file_contents_results.clear(); + }, + Err(e) => eprintln!("Failed to deserialize paths: {:?}", e), + } + } + pub fn delete_files(&mut self, paths: String) { + match serde_json::from_str::>(&paths) { + Ok(paths) => { + self.remove_existing_entries(&paths); + self.cached_file_name_results.clear(); + self.cached_file_contents_results.clear(); + }, + Err(e) => eprintln!("Failed to deserialize paths: {:?}", e), + } + } + fn add_file_entry(&mut self, file_name: &Path, file_metadata: Option) { + let file_path = file_name.display().to_string(); + let file_path_stripped_prefix = self.strip_file_prefix(&file_name); + + self.file_names.insert(file_path_stripped_prefix.clone()); + if let SearchType::NamesAndContents | SearchType::Contents = self.search_type { + if file_metadata.map(|f| f.is_file()).unwrap_or(false) { + if let Ok(file) = std::fs::File::open(&file_path) { + let lines = io::BufReader::new(file).lines(); + for (index, line) in lines.enumerate() { + match line { + Ok(line) => { + self.file_contents.insert( + ( + // String::from_utf8_lossy(&strip_ansi_escapes::strip(file_path_stripped_prefix.clone()).unwrap()).to_string(), + file_path_stripped_prefix.clone(), + index + 1, + ), + String::from_utf8_lossy( + &strip_ansi_escapes::strip(line).unwrap(), + ) + .to_string(), + ); + }, + Err(_) => { + break; // probably a binary file, skip it + }, + } + } + } + } + } + } + fn search_file_names( + &self, + search_term: &str, + matcher: &mut SkimMatcherV2, + ) -> Vec { + let mut matches = vec![]; + for entry in &self.file_names { + if let Some((score, indices)) = matcher.fuzzy_indices(&entry, &search_term) { + matches.push(SearchResult::new_file_name( + score, + indices, + entry.to_owned(), + )); + } + } + matches + } + fn search_file_contents( + &self, + search_term: &str, + matcher: &mut SkimMatcherV2, + ) -> Vec { + let mut matches = vec![]; + for ((file_name, line_number), line_entry) in &self.file_contents { + if line_entry.contains("struct") { + if line_entry.len() < 400 { + eprintln!("matching against: {:?}", line_entry) + } else { + eprintln!("matching again line that has struct but is very long") + } + } + if let Some((score, indices)) = matcher.fuzzy_indices(&line_entry, &search_term) { + if line_entry.contains("struct") { + eprintln!("score: {:?}", score) + } + matches.push(SearchResult::new_file_line( + score, + indices, + file_name.clone(), + line_entry.clone(), + *line_number, + )); + } else { + if line_entry.contains("struct") { + eprintln!("no score!") + } + } + } + matches + } + fn strip_file_prefix(&self, file_name: &Path) -> String { + let mut file_path_stripped_prefix = file_name.display().to_string().split_off(ROOT.width()); + if file_path_stripped_prefix.starts_with('/') { + file_path_stripped_prefix.remove(0); + } + file_path_stripped_prefix + } + fn read_search_term_from_hd_cache(&self) -> Option { + match std::fs::read(CURRENT_SEARCH_TERM) { + Ok(current_search_term) => { + Some(String::from_utf8_lossy(¤t_search_term).to_string()) + }, + _ => None, + } + } + fn remove_existing_entries(&mut self, paths: &Vec) { + let file_path_stripped_prefixes: Vec = + paths.iter().map(|p| self.strip_file_prefix(&p)).collect(); + self.file_names + .retain(|file_name| !file_path_stripped_prefixes.contains(file_name)); + self.file_contents.retain(|(file_name, _line_in_file), _| { + !file_path_stripped_prefixes.contains(file_name) + }); + } +} + +#[derive(Serialize, Deserialize)] +pub enum MessageToSearch { + ScanFolder, + Search, + FileSystemCreate, + FileSystemUpdate, + FileSystemDelete, +} + +#[derive(Serialize, Deserialize)] +pub struct FileNameWorker { + search: Search, +} + +impl Default for FileNameWorker { + fn default() -> Self { + FileNameWorker { + search: Search::new(SearchType::Names), + } + } +} + +#[derive(Serialize, Deserialize)] +pub struct FileContentsWorker { + search: Search, +} + +impl Default for FileContentsWorker { + fn default() -> Self { + FileContentsWorker { + search: Search::new(SearchType::Contents), + } + } +} + +impl<'de> ZellijWorker<'de> for FileNameWorker { + fn on_message(&mut self, message: String, payload: String) { + self.search.on_message(message, payload); + } +} + +impl<'de> ZellijWorker<'de> for FileContentsWorker { + fn on_message(&mut self, message: String, payload: String) { + self.search.on_message(message, payload); + } +} + +#[derive(Serialize, Deserialize, Debug, Clone)] +pub struct ResultsOfSearch { + pub search_term: String, + pub search_results: Vec, +} + +impl ResultsOfSearch { + pub fn new(search_term: String, search_results: Vec) -> Self { + ResultsOfSearch { + search_term, + search_results, + } + } + pub fn limit_search_results(mut self, max_results: usize) -> Self { + self.search_results + .sort_by(|a, b| b.score().cmp(&a.score())); + self.search_results = if self.search_results.len() > max_results { + self.search_results.drain(..max_results).collect() + } else { + self.search_results.drain(..).collect() + }; + self + } +} diff --git a/default-plugins/strider/src/search/search_results.rs b/default-plugins/strider/src/search/search_results.rs new file mode 100644 index 0000000000..b76200ae80 --- /dev/null +++ b/default-plugins/strider/src/search/search_results.rs @@ -0,0 +1,308 @@ +use crate::search::ui::{ + bold, styled_text, styled_text_background, styled_text_foreground, underline, GRAY_LIGHT, + GREEN, ORANGE, +}; +use serde::{Deserialize, Serialize}; +use unicode_width::UnicodeWidthStr; + +#[derive(Serialize, Deserialize, Debug, Clone)] +pub enum SearchResult { + File { + path: String, + score: i64, + indices: Vec, + }, + LineInFile { + path: String, + line: String, + line_number: usize, + score: i64, + indices: Vec, + }, +} + +impl SearchResult { + pub fn new_file_name(score: i64, indices: Vec, path: String) -> Self { + SearchResult::File { + path, + score, + indices, + } + } + pub fn new_file_line( + score: i64, + indices: Vec, + path: String, + line: String, + line_number: usize, + ) -> Self { + SearchResult::LineInFile { + path, + score, + indices, + line, + line_number, + } + } + pub fn score(&self) -> i64 { + match self { + SearchResult::File { score, .. } => *score, + SearchResult::LineInFile { score, .. } => *score, + } + } + pub fn rendered_height(&self) -> usize { + match self { + SearchResult::File { .. } => 1, + SearchResult::LineInFile { .. } => 2, + } + } + pub fn is_same_entry(&self, other: &Self) -> bool { + match (&self, other) { + ( + SearchResult::File { path: my_path, .. }, + SearchResult::File { + path: other_path, .. + }, + ) => my_path == other_path, + ( + SearchResult::LineInFile { + path: my_path, + line_number: my_line_number, + .. + }, + SearchResult::LineInFile { + path: other_path, + line_number: other_line_number, + .. + }, + ) => my_path == other_path && my_line_number == other_line_number, + _ => false, + } + } + pub fn render( + &self, + max_width: usize, + is_selected: bool, + is_below_search_result: bool, + ) -> String { + let max_width = max_width.saturating_sub(4); // for the UI left line separator + match self { + SearchResult::File { path, indices, .. } => self.render_file_result( + path, + indices, + is_selected, + is_below_search_result, + max_width, + ), + SearchResult::LineInFile { + path, + line, + line_number, + indices, + .. + } => self.render_line_in_file_result( + path, + line, + *line_number, + indices, + is_selected, + is_below_search_result, + max_width, + ), + } + } + fn render_file_result( + &self, + path: &String, + indices: &Vec, + is_selected: bool, + is_below_search_result: bool, + max_width: usize, + ) -> String { + if is_selected { + let line = self.render_line_with_indices( + path, + indices, + max_width.saturating_sub(3), + Some(GREEN), + ); + let selection_arrow = styled_text_foreground(ORANGE, "┌>"); + format!("{} {}", selection_arrow, line) + } else { + let line_prefix = if is_below_search_result { "│ " } else { " " }; + let line = + self.render_line_with_indices(path, indices, max_width.saturating_sub(3), None); + format!("{} {}", line_prefix, line) + } + } + fn render_line_in_file_result( + &self, + path: &String, + line: &String, + line_number: usize, + indices: &Vec, + is_selected: bool, + is_below_search_result: bool, + max_width: usize, + ) -> String { + let line_number_prefix_text = format!("└ {} ", line_number); + let max_width_of_line_in_file = max_width + .saturating_sub(3) + .saturating_sub(line_number_prefix_text.width()); + if is_selected { + let file_name_line = self.render_line_with_indices( + path, + &vec![], + max_width.saturating_sub(3), + Some(GREEN), + ); + let line_in_file = self.render_line_with_indices( + line, + indices, + max_width_of_line_in_file, + Some(GREEN), + ); + let line_number_prefix = styled_text_foreground(GREEN, &bold(&line_number_prefix_text)); + format!( + "{} {}\n│ {}{}", + styled_text_foreground(ORANGE, "┌>"), + file_name_line, + line_number_prefix, + line_in_file + ) + } else { + let file_name_line = + self.render_line_with_indices(path, &vec![], max_width.saturating_sub(3), None); + let line_in_file = + self.render_line_with_indices(line, indices, max_width_of_line_in_file, None); + let line_number_prefix = bold(&line_number_prefix_text); + let line_prefix = if is_below_search_result { "│ " } else { " " }; + format!( + "{} {}\n{} {}{}", + line_prefix, file_name_line, line_prefix, line_number_prefix, line_in_file + ) + } + } + fn render_line_with_indices( + &self, + line_to_render: &String, + indices: &Vec, + max_width: usize, + foreground_color: Option, + ) -> String { + let non_index_character_style = |c: &str| match foreground_color { + Some(foreground_color) => styled_text_foreground(foreground_color, &bold(c)), + None => bold(c), + }; + let index_character_style = |c: &str| match foreground_color { + Some(foreground_color) => { + styled_text(foreground_color, GRAY_LIGHT, &bold(&underline(c))) + }, + None => styled_text_background(GRAY_LIGHT, &bold(&underline(c))), + }; + + let truncate_positions = + self.truncate_line_with_indices(line_to_render, indices, max_width); + let truncate_start_position = truncate_positions.map(|p| p.0).unwrap_or(0); + let truncate_end_position = truncate_positions + .map(|p| p.1) + .unwrap_or(line_to_render.chars().count()); + let mut visible_portion = String::new(); + for (i, character) in line_to_render.chars().enumerate() { + if i >= truncate_start_position && i <= truncate_end_position { + if indices.contains(&i) { + visible_portion.push_str(&index_character_style(&character.to_string())); + } else { + visible_portion.push_str(&non_index_character_style(&character.to_string())); + } + } + } + if truncate_positions.is_some() { + let left_truncate_sign = if truncate_start_position == 0 { + "" + } else { + ".." + }; + let right_truncate_sign = if truncate_end_position == line_to_render.chars().count() { + "" + } else { + ".." + }; + format!( + "{}{}{}", + non_index_character_style(left_truncate_sign), + visible_portion, + non_index_character_style(right_truncate_sign) + ) + } else { + visible_portion + } + } + fn truncate_line_with_indices( + &self, + line_to_render: &String, + indices: &Vec, + max_width: usize, + ) -> Option<(usize, usize)> { + let first_index = indices.get(0).copied().unwrap_or(0); + let last_index = indices + .last() + .copied() + .unwrap_or_else(|| std::cmp::min(line_to_render.chars().count(), max_width)); + if line_to_render.width() <= max_width { + // there's enough room, no need to truncate + None + } else if last_index.saturating_sub(first_index) < max_width { + // truncate around the indices + let mut width_remaining = max_width + .saturating_sub(1) + .saturating_sub(last_index.saturating_sub(first_index)); + + let mut string_start_position = first_index; + let mut string_end_position = last_index; + + let mut i = 0; + loop { + if i >= width_remaining { + break; + } + if string_start_position > 0 && string_end_position < line_to_render.chars().count() + { + let take_from_start = i % 2 == 0; + if take_from_start { + string_start_position -= 1; + if string_start_position == 0 { + width_remaining += 2; // no need for truncating dots + } + } else { + string_end_position += 1; + if string_end_position == line_to_render.chars().count() { + width_remaining += 2; // no need for truncating dots + } + } + } else if string_end_position < line_to_render.chars().count() { + string_end_position += 1; + if string_end_position == line_to_render.chars().count() { + width_remaining += 2; // no need for truncating dots + } + } else if string_start_position > 0 { + string_start_position -= 1; + if string_start_position == 0 { + width_remaining += 2; // no need for truncating dots + } + } else { + break; + } + i += 1; + } + Some((string_start_position, string_end_position)) + } else if !indices.is_empty() { + // no room for all indices, remove the last one and try again + let mut new_indices = indices.clone(); + new_indices.pop(); + self.truncate_line_with_indices(line_to_render, &new_indices, max_width) + } else { + Some((first_index, last_index)) + } + } +} diff --git a/default-plugins/strider/src/search/search_state.rs b/default-plugins/strider/src/search/search_state.rs new file mode 100644 index 0000000000..5766d1c29b --- /dev/null +++ b/default-plugins/strider/src/search/search_state.rs @@ -0,0 +1,241 @@ +use crate::search::search_results::SearchResult; +use crate::search::{MessageToSearch, ResultsOfSearch}; +use serde::{Deserialize, Serialize}; +use std::path::PathBuf; +use zellij_tile::prelude::{ + hide_self, open_file, open_file_floating, open_file_with_line, open_file_with_line_floating, + open_terminal, open_terminal_floating, post_message_to, Key, +}; + +pub const CURRENT_SEARCH_TERM: &str = "/data/current_search_term"; + +#[derive(Default)] +pub struct SearchState { + pub search_term: String, + pub file_name_search_results: Vec, + pub file_contents_search_results: Vec, + pub loading: bool, + pub loading_animation_offset: u8, + pub selected_search_result: usize, + pub should_open_floating: bool, + pub search_filter: SearchType, + pub display_rows: usize, + pub display_columns: usize, + pub displayed_search_results: (usize, Vec), // usize is selected index +} + +impl SearchState { + pub fn handle_key(&mut self, key: Key) { + match key { + Key::Down => self.move_search_selection_down(), + Key::Up => self.move_search_selection_up(), + Key::Char('\n') => self.open_search_result_in_editor(), + Key::BackTab => self.open_search_result_in_terminal(), + Key::Ctrl('f') => { + self.should_open_floating = !self.should_open_floating; + }, + Key::Ctrl('r') => self.toggle_search_filter(), + Key::Esc => { + hide_self(); + self.clear_state(); + }, + _ => self.append_to_search_term(key), + } + } + pub fn update_file_name_search_results(&mut self, mut results_of_search: ResultsOfSearch) { + if self.search_term == results_of_search.search_term { + self.file_name_search_results = results_of_search.search_results.drain(..).collect(); + self.update_displayed_search_results(); + } + } + pub fn update_file_contents_search_results(&mut self, mut results_of_search: ResultsOfSearch) { + if self.search_term == results_of_search.search_term { + self.file_contents_search_results = + results_of_search.search_results.drain(..).collect(); + self.update_displayed_search_results(); + } + } + pub fn change_size(&mut self, rows: usize, cols: usize) { + self.display_rows = rows; + self.display_columns = cols; + } + pub fn progress_animation(&mut self) { + if self.loading_animation_offset == u8::MAX { + self.loading_animation_offset = 0; + } else { + self.loading_animation_offset = self.loading_animation_offset.saturating_add(1); + } + } + pub fn number_of_lines_in_displayed_search_results(&self) -> usize { + self.displayed_search_results + .1 + .iter() + .map(|l| l.rendered_height()) + .sum() + } + fn move_search_selection_down(&mut self) { + if self.displayed_search_results.0 < self.max_search_selection_index() { + self.displayed_search_results.0 += 1; + } + } + fn move_search_selection_up(&mut self) { + self.displayed_search_results.0 = self.displayed_search_results.0.saturating_sub(1); + } + fn open_search_result_in_editor(&mut self) { + match self.selected_search_result_entry() { + Some(SearchResult::File { path, .. }) => { + if self.should_open_floating { + open_file_floating(&PathBuf::from(path)) + } else { + open_file(&PathBuf::from(path)); + } + }, + Some(SearchResult::LineInFile { + path, line_number, .. + }) => { + if self.should_open_floating { + open_file_with_line_floating(&PathBuf::from(path), line_number); + } else { + open_file_with_line(&PathBuf::from(path), line_number); + } + }, + None => eprintln!("Search results not found"), + } + } + fn open_search_result_in_terminal(&mut self) { + let dir_path_of_result = |path: &str| -> PathBuf { + let file_path = PathBuf::from(path); + let mut dir_path = file_path.components(); + dir_path.next_back(); // remove file name to stay with just the folder + dir_path.as_path().into() + }; + let selected_search_result_entry = self.selected_search_result_entry(); + if let Some(SearchResult::File { path, .. }) | Some(SearchResult::LineInFile { path, .. }) = + selected_search_result_entry + { + let dir_path = dir_path_of_result(&path); + if self.should_open_floating { + open_terminal_floating(&dir_path); + } else { + open_terminal(&dir_path); + } + } + } + fn toggle_search_filter(&mut self) { + self.search_filter.progress(); + self.send_search_query(); + } + fn clear_state(&mut self) { + self.file_name_search_results.clear(); + self.file_contents_search_results.clear(); + self.displayed_search_results = (0, vec![]); + self.search_term.clear(); + } + fn append_to_search_term(&mut self, key: Key) { + match key { + Key::Char(character) => { + self.search_term.push(character); + }, + Key::Backspace => { + self.search_term.pop(); + if self.search_term.len() == 0 { + self.clear_state(); + } + }, + _ => {}, + } + self.send_search_query(); + } + fn send_search_query(&mut self) { + match std::fs::write(CURRENT_SEARCH_TERM, &self.search_term) { + Ok(_) => { + if !self.search_term.is_empty() { + post_message_to( + "file_name_search", + serde_json::to_string(&MessageToSearch::Search).unwrap(), + "".to_owned(), + ); + post_message_to( + "file_contents_search", + serde_json::to_string(&MessageToSearch::Search).unwrap(), + "".to_owned(), + ); + self.file_name_search_results.clear(); + self.file_contents_search_results.clear(); + } + }, + Err(e) => eprintln!("Failed to write search term to HD, aborting search: {}", e), + } + } + fn max_search_selection_index(&self) -> usize { + self.displayed_search_results.1.len().saturating_sub(1) + } + fn update_displayed_search_results(&mut self) { + if self.search_term.is_empty() { + self.clear_state(); + return; + } + let mut search_results_of_interest = match self.search_filter { + SearchType::NamesAndContents => { + let mut all_search_results = self.file_name_search_results.clone(); + all_search_results.append(&mut self.file_contents_search_results.clone()); + all_search_results.sort_by(|a, b| b.score().cmp(&a.score())); + all_search_results + }, + SearchType::Names => self.file_name_search_results.clone(), + SearchType::Contents => self.file_contents_search_results.clone(), + }; + let mut height_taken_up_by_results = 0; + let mut displayed_search_results = vec![]; + for search_result in search_results_of_interest.drain(..) { + if height_taken_up_by_results + search_result.rendered_height() + > self.rows_for_results() + { + break; + } + height_taken_up_by_results += search_result.rendered_height(); + displayed_search_results.push(search_result); + } + let new_index = self + .selected_search_result_entry() + .and_then(|currently_selected_search_result| { + displayed_search_results + .iter() + .position(|r| r.is_same_entry(¤tly_selected_search_result)) + }) + .unwrap_or(0); + self.displayed_search_results = (new_index, displayed_search_results); + } + fn selected_search_result_entry(&self) -> Option { + self.displayed_search_results + .1 + .get(self.displayed_search_results.0) + .cloned() + } + pub fn rows_for_results(&self) -> usize { + self.display_rows.saturating_sub(3) // search line and 2 controls lines + } +} + +#[derive(Serialize, Deserialize)] +pub enum SearchType { + NamesAndContents, + Names, + Contents, +} + +impl SearchType { + pub fn progress(&mut self) { + match &self { + &SearchType::NamesAndContents => *self = SearchType::Names, + &SearchType::Names => *self = SearchType::Contents, + &SearchType::Contents => *self = SearchType::NamesAndContents, + } + } +} + +impl Default for SearchType { + fn default() -> Self { + SearchType::NamesAndContents + } +} diff --git a/default-plugins/strider/src/search/selection_controls_area.rs b/default-plugins/strider/src/search/selection_controls_area.rs new file mode 100644 index 0000000000..0805e1e414 --- /dev/null +++ b/default-plugins/strider/src/search/selection_controls_area.rs @@ -0,0 +1,61 @@ +use crate::search::ui::{bold, styled_text_foreground, ORANGE}; + +pub struct SelectionControlsArea { + display_lines: usize, + display_columns: usize, +} + +impl SelectionControlsArea { + pub fn new(display_lines: usize, display_columns: usize) -> Self { + SelectionControlsArea { + display_lines, + display_columns, + } + } + pub fn render(&self, result_count: usize) -> String { + let mut to_render = String::new(); + let padding = self.display_lines.saturating_sub(result_count); + for _ in 0..padding { + to_render.push_str(&self.render_padding_line()); + } + let selection_controls = self.render_selection_controls(); + to_render.push_str(&selection_controls); + to_render + } + pub fn render_empty_lines(&self) -> String { + let mut to_render = String::new(); + for _ in 0..self.display_lines { + to_render.push_str("\n"); + } + to_render + } + fn render_padding_line(&self) -> String { + format!("│\n") + } + fn render_selection_controls(&self) -> String { + if self.display_columns >= self.full_selection_controls_len() { + self.render_full_selection_controls() + } else { + self.render_truncated_selection_controls() + } + } + fn full_selection_controls_len(&self) -> usize { + 62 + } + fn render_full_selection_controls(&self) -> String { + let arrow_tail = "└ "; + let enter = styled_text_foreground(ORANGE, &bold("")); + let enter_tip = bold(" - open in editor. "); + let tab = styled_text_foreground(ORANGE, &bold("")); + let tab_tip = bold(" - open terminal at location."); + format!("{}{}{}{}{}", arrow_tail, enter, enter_tip, tab, tab_tip) + } + fn render_truncated_selection_controls(&self) -> String { + let arrow_tail = "└ "; + let enter = styled_text_foreground(ORANGE, &bold("")); + let enter_tip = bold(" - edit. "); + let tab = styled_text_foreground(ORANGE, &bold("")); + let tab_tip = bold(" - terminal."); + format!("{}{}{}{}{}", arrow_tail, enter, enter_tip, tab, tab_tip) + } +} diff --git a/default-plugins/strider/src/search/ui.rs b/default-plugins/strider/src/search/ui.rs new file mode 100644 index 0000000000..5ee5a3e832 --- /dev/null +++ b/default-plugins/strider/src/search/ui.rs @@ -0,0 +1,120 @@ +use crate::search::controls_line::{Control, ControlsLine}; +use crate::search::search_state::SearchState; +use crate::search::selection_controls_area::SelectionControlsArea; +use std::fmt::{Display, Formatter, Result}; + +pub const CYAN: u8 = 51; +pub const GRAY_LIGHT: u8 = 238; +pub const GRAY_DARK: u8 = 245; +pub const WHITE: u8 = 15; +pub const BLACK: u8 = 16; +pub const RED: u8 = 124; +pub const GREEN: u8 = 154; +pub const ORANGE: u8 = 166; + +impl Display for SearchState { + fn fmt(&self, f: &mut Formatter) -> Result { + write!(f, "{}", self.render_search_line())?; + write!(f, "{}", self.render_search_results())?; + write!(f, "{}", self.render_selection_control_area())?; + write!(f, "{}", self.render_controls_line())?; + Ok(()) + } +} + +impl SearchState { + pub fn render_search_line(&self) -> String { + format!( + "{}{}\n", + styled_text_foreground(CYAN, &bold("SEARCH: ")), + self.search_term + ) + } + pub fn render_search_results(&self) -> String { + let mut space_for_results = self.display_rows.saturating_sub(3); // title and both controls lines + let mut to_render = String::new(); + for (i, search_result) in self.displayed_search_results.1.iter().enumerate() { + let result_height = search_result.rendered_height(); + if space_for_results < result_height { + break; + } + space_for_results -= result_height; + let index_of_selected_result = self.displayed_search_results.0; + let is_selected = i == index_of_selected_result; + let is_below_search_result = i > index_of_selected_result; + let rendered_result = + search_result.render(self.display_columns, is_selected, is_below_search_result); + to_render.push_str(&format!("{}", rendered_result)); + to_render.push('\n') + } + to_render + } + pub fn render_selection_control_area(&self) -> String { + let rows_for_results = self.rows_for_results(); + if !self.displayed_search_results.1.is_empty() { + format!( + "{}\n", + SelectionControlsArea::new(rows_for_results, self.display_columns) + .render(self.number_of_lines_in_displayed_search_results()) + ) + } else { + format!( + "{}\n", + SelectionControlsArea::new(rows_for_results, self.display_columns) + .render_empty_lines() + ) + } + } + pub fn render_controls_line(&self) -> String { + let has_results = !self.displayed_search_results.1.is_empty(); + let tiled_floating_control = + Control::new_floating_control("Ctrl f", self.should_open_floating); + let names_contents_control = Control::new_filter_control("Ctrl r", &self.search_filter); + if self.loading { + ControlsLine::new( + vec![tiled_floating_control, names_contents_control], + Some(vec!["Scanning folder", "Scanning", "S"]), + ) + .with_animation_offset(self.loading_animation_offset) + .render(self.display_columns, has_results) + } else { + ControlsLine::new(vec![tiled_floating_control, names_contents_control], None) + .render(self.display_columns, has_results) + } + } +} + +pub fn bold(text: &str) -> String { + format!("\u{1b}[1m{}\u{1b}[m", text) +} + +pub fn underline(text: &str) -> String { + format!("\u{1b}[4m{}\u{1b}[m", text) +} + +pub fn styled_text(foreground_color: u8, background_color: u8, text: &str) -> String { + format!( + "\u{1b}[38;5;{};48;5;{}m{}\u{1b}[m", + foreground_color, background_color, text + ) +} + +pub fn styled_text_foreground(foreground_color: u8, text: &str) -> String { + format!("\u{1b}[38;5;{}m{}\u{1b}[m", foreground_color, text) +} + +pub fn styled_text_background(background_color: u8, text: &str) -> String { + format!("\u{1b}[48;5;{}m{}\u{1b}[m", background_color, text) +} + +pub fn color_line_to_end(background_color: u8) -> String { + format!("\u{1b}[48;5;{}m\u{1b}[0K", background_color) +} + +pub fn arrow(foreground: u8, background: u8) -> String { + format!("\u{1b}[38;5;{}m\u{1b}[48;5;{}m", foreground, background) +} + +pub fn dot(foreground: u8, background: u8) -> String { + format!("\u{1b}[38;5;{};48;5;{}m•", foreground, background) +} diff --git a/default-plugins/strider/src/state.rs b/default-plugins/strider/src/state.rs index 7ced4932cc..8dd09653a5 100644 --- a/default-plugins/strider/src/state.rs +++ b/default-plugins/strider/src/state.rs @@ -1,4 +1,6 @@ -use crate::search::SearchResult; +use crate::search::search_results::SearchResult; +use crate::search::search_state::SearchState; +use crate::search::search_state::SearchType; use pretty_bytes::converter as pb; use std::{ collections::{HashMap, VecDeque}, @@ -18,65 +20,29 @@ pub struct State { pub cursor_hist: HashMap, pub hide_hidden_files: bool, pub ev_history: VecDeque<(Event, Instant)>, // stores last event, can be expanded in future + pub search_state: SearchState, pub search_paths: Vec, pub search_term: Option, - pub search_results: Vec, + pub file_name_search_results: Vec, + pub file_contents_search_results: Vec, pub loading: bool, pub loading_animation_offset: u8, pub typing_search_term: bool, - pub exploring_search_results: bool, pub selected_search_result: usize, + pub processed_search_index: usize, + pub should_open_floating: bool, + pub search_filter: SearchType, } impl State { - pub fn append_to_search_term(&mut self, key: Key) { - match key { - Key::Char(character) => { - if let Some(search_term) = self.search_term.as_mut() { - search_term.push(character); - } - }, - Key::Backspace => { - if let Some(search_term) = self.search_term.as_mut() { - search_term.pop(); - if search_term.len() == 0 { - self.search_term = None; - self.typing_search_term = false; - } - } - }, - _ => {}, - } - } - pub fn accept_search_term(&mut self) { - self.typing_search_term = false; - self.exploring_search_results = true; - } pub fn typing_search_term(&self) -> bool { self.typing_search_term } - pub fn exploring_search_results(&self) -> bool { - self.exploring_search_results - } - pub fn stop_exploring_search_results(&mut self) { - self.exploring_search_results = false; - } pub fn start_typing_search_term(&mut self) { - if self.search_term.is_none() { - self.search_term = Some(String::new()); - } self.typing_search_term = true; } pub fn stop_typing_search_term(&mut self) { - self.typing_search_term = true; - } - pub fn move_search_selection_up(&mut self) { - self.selected_search_result = self.selected_search_result.saturating_sub(1); - } - pub fn move_search_selection_down(&mut self) { - if self.selected_search_result < self.search_results.len() { - self.selected_search_result = self.selected_search_result.saturating_add(1); - } + self.typing_search_term = false; } pub fn selected_mut(&mut self) -> &mut usize { &mut self.cursor_hist.entry(self.path.clone()).or_default().0 @@ -104,32 +70,6 @@ impl State { } } } - pub fn open_search_result(&mut self) { - match self.search_results.get(self.selected_search_result) { - Some(SearchResult::File { - path, - score, - indices, - }) => { - let file_path = PathBuf::from(path); - open_file(file_path.strip_prefix(ROOT).unwrap()); - }, - Some(SearchResult::LineInFile { - path, - score, - indices, - line, - line_number, - }) => { - let file_path = PathBuf::from(path); - open_file_with_line(file_path.strip_prefix(ROOT).unwrap(), *line_number); - // open_file_with_line(&file_path, *line_number); // TODO: no!! - }, - None => { - eprintln!("Search result not found"); - }, - } - } } #[derive(PartialEq, Eq, PartialOrd, Ord, Clone)] diff --git a/zellij-server/src/lib.rs b/zellij-server/src/lib.rs index 166a60c5fe..4e4911429d 100644 --- a/zellij-server/src/lib.rs +++ b/zellij-server/src/lib.rs @@ -32,7 +32,7 @@ use wasmer::Store; use crate::{ os_input_output::ServerOsApi, plugins::{plugin_thread_main, PluginInstruction}, - pty::{pty_thread_main, Pty, PtyInstruction}, + pty::{get_default_shell, pty_thread_main, Pty, PtyInstruction}, screen::{screen_thread_main, ScreenInstruction}, thread_bus::{Bus, ThreadSenders}, }; @@ -705,6 +705,10 @@ fn init_session( ..Default::default() }) }); + let path_to_default_shell = config_options + .default_shell + .clone() + .unwrap_or_else(|| get_default_shell()); let pty_thread = thread::Builder::new() .name("pty".to_string()) @@ -757,6 +761,7 @@ fn init_session( }) .unwrap(); + let zellij_cwd = std::env::current_dir().unwrap_or_else(|_| std::path::PathBuf::from(".")); let plugin_thread = thread::Builder::new() .name("wasm".to_string()) .spawn({ @@ -780,6 +785,8 @@ fn init_session( data_dir, plugins.unwrap_or_default(), layout, + path_to_default_shell, + zellij_cwd, ) .fatal() } diff --git a/zellij-server/src/panes/floating_panes/mod.rs b/zellij-server/src/panes/floating_panes/mod.rs index a85cf80fd8..071f6e7c27 100644 --- a/zellij-server/src/panes/floating_panes/mod.rs +++ b/zellij-server/src/panes/floating_panes/mod.rs @@ -25,7 +25,7 @@ use zellij_utils::{ data::{ModeInfo, Style}, errors::prelude::*, input::command::RunCommand, - input::layout::FloatingPaneLayout, + input::layout::{FloatingPaneLayout, Run, RunPlugin}, pane_size::{Dimension, Offset, PaneGeom, Size, SizeInPixels, Viewport}, }; @@ -870,4 +870,19 @@ impl FloatingPanes { self.focus_pane_for_all_clients(active_pane_id); } } + pub fn get_plugin_pane_id(&self, run_plugin: &RunPlugin) -> Option { + let run = Some(Run::Plugin(run_plugin.clone())); + self.panes + .iter() + .find(|(_id, s_p)| s_p.invoked_with() == &run) + .map(|(id, _)| *id) + } + pub fn focus_pane_if_exists(&mut self, pane_id: PaneId, client_id: ClientId) -> Result<()> { + if self.panes.get(&pane_id).is_some() { + self.focus_pane(pane_id, client_id); + Ok(()) + } else { + Err(anyhow!("Pane not found")) + } + } } diff --git a/zellij-server/src/panes/plugin_pane.rs b/zellij-server/src/panes/plugin_pane.rs index 7915069c56..28fdb3d10c 100644 --- a/zellij-server/src/panes/plugin_pane.rs +++ b/zellij-server/src/panes/plugin_pane.rs @@ -537,7 +537,7 @@ impl Pane for PluginPane { self.pane_title = title; } fn update_loading_indication(&mut self, loading_indication: LoadingIndication) { - if self.loading_indication.ended { + if self.loading_indication.ended && !loading_indication.is_error() { return; } self.loading_indication.merge(loading_indication); diff --git a/zellij-server/src/panes/tiled_panes/mod.rs b/zellij-server/src/panes/tiled_panes/mod.rs index 08a633b0a5..3734214af3 100644 --- a/zellij-server/src/panes/tiled_panes/mod.rs +++ b/zellij-server/src/panes/tiled_panes/mod.rs @@ -20,7 +20,10 @@ use stacked_panes::StackedPanes; use zellij_utils::{ data::{Direction, ModeInfo, ResizeStrategy, Style}, errors::prelude::*, - input::{command::RunCommand, layout::SplitDirection}, + input::{ + command::RunCommand, + layout::{Run, RunPlugin, SplitDirection}, + }, pane_size::{Offset, PaneGeom, Size, SizeInPixels, Viewport}, }; @@ -529,6 +532,14 @@ impl TiledPanes { } self.reset_boundaries(); } + pub fn focus_pane_if_exists(&mut self, pane_id: PaneId, client_id: ClientId) -> Result<()> { + if self.panes.get(&pane_id).is_some() { + self.focus_pane(pane_id, client_id); + Ok(()) + } else { + Err(anyhow!("Pane not found")) + } + } pub fn focus_pane_at_position(&mut self, position_and_size: PaneGeom, client_id: ClientId) { if let Some(pane_id) = self .panes @@ -1691,6 +1702,13 @@ impl TiledPanes { fn reset_boundaries(&mut self) { self.client_id_to_boundaries.clear(); } + pub fn get_plugin_pane_id(&self, run_plugin: &RunPlugin) -> Option { + let run = Some(Run::Plugin(run_plugin.clone())); + self.panes + .iter() + .find(|(_id, s_p)| s_p.invoked_with() == &run) + .map(|(id, _)| *id) + } } #[allow(clippy::borrowed_box)] diff --git a/zellij-server/src/plugins/mod.rs b/zellij-server/src/plugins/mod.rs index 9384b3f621..7b322b054c 100644 --- a/zellij-server/src/plugins/mod.rs +++ b/zellij-server/src/plugins/mod.rs @@ -1,6 +1,8 @@ mod plugin_loader; mod plugin_map; +mod plugin_worker; mod wasm_bridge; +mod watch_filesystem; mod zellij_exports; use log::info; use std::{collections::HashMap, fs, path::PathBuf}; @@ -104,13 +106,22 @@ pub(crate) fn plugin_thread_main( data_dir: PathBuf, plugins: PluginsConfig, layout: Box, + path_to_default_shell: PathBuf, + zellij_cwd: PathBuf, ) -> Result<()> { info!("Wasm main thread starts"); let plugin_dir = data_dir.join("plugins/"); let plugin_global_data_dir = plugin_dir.join("data"); - let mut wasm_bridge = WasmBridge::new(plugins, bus.senders.clone(), store, plugin_dir); + let mut wasm_bridge = WasmBridge::new( + plugins, + bus.senders.clone(), + store, + plugin_dir, + path_to_default_shell, + zellij_cwd, + ); loop { let (event, mut err_ctx) = bus.recv().expect("failed to receive event on channel"); diff --git a/zellij-server/src/plugins/plugin_loader.rs b/zellij-server/src/plugins/plugin_loader.rs index 91eb6c93e9..4d77bd9e9e 100644 --- a/zellij-server/src/plugins/plugin_loader.rs +++ b/zellij-server/src/plugins/plugin_loader.rs @@ -1,6 +1,5 @@ -use crate::plugins::plugin_map::{ - PluginEnv, PluginMap, RunningPlugin, RunningWorker, Subscriptions, -}; +use crate::plugins::plugin_map::{PluginEnv, PluginMap, RunningPlugin, Subscriptions}; +use crate::plugins::plugin_worker::{plugin_worker, RunningWorker}; use crate::plugins::zellij_exports::{wasi_read_string, zellij_exports}; use crate::plugins::PluginId; use highway::{HighwayHash, PortableHash}; @@ -164,6 +163,8 @@ pub struct PluginLoader<'a> { plugin_own_data_dir: PathBuf, size: Size, wasm_blob_on_hd: Option<(Vec, PathBuf)>, + path_to_default_shell: PathBuf, + zellij_cwd: PathBuf, } impl<'a> PluginLoader<'a> { @@ -176,6 +177,8 @@ impl<'a> PluginLoader<'a> { plugin_map: Arc>, connected_clients: Arc>>, loading_indication: &mut LoadingIndication, + path_to_default_shell: PathBuf, + zellij_cwd: PathBuf, ) -> Result<()> { let err_context = || format!("failed to reload plugin {plugin_id} from memory"); let mut connected_clients: Vec = @@ -194,6 +197,8 @@ impl<'a> PluginLoader<'a> { first_client_id, &store, &plugin_dir, + path_to_default_shell, + zellij_cwd, )?; plugin_loader .load_module_from_memory() @@ -227,6 +232,8 @@ impl<'a> PluginLoader<'a> { size: Size, connected_clients: Arc>>, loading_indication: &mut LoadingIndication, + path_to_default_shell: PathBuf, + zellij_cwd: PathBuf, ) -> Result<()> { let err_context = || format!("failed to start plugin {plugin_id} for client {client_id}"); let mut plugin_loader = PluginLoader::new( @@ -240,6 +247,8 @@ impl<'a> PluginLoader<'a> { &plugin_dir, tab_index, size, + path_to_default_shell, + zellij_cwd, )?; plugin_loader .load_module_from_memory() @@ -273,6 +282,8 @@ impl<'a> PluginLoader<'a> { plugin_map: Arc>, connected_clients: Arc>>, loading_indication: &mut LoadingIndication, + path_to_default_shell: PathBuf, + zellij_cwd: PathBuf, ) -> Result<()> { let mut new_plugins = HashSet::new(); for plugin_id in plugin_map.lock().unwrap().plugin_ids() { @@ -288,6 +299,8 @@ impl<'a> PluginLoader<'a> { existing_client_id, &store, &plugin_dir, + path_to_default_shell.clone(), + zellij_cwd.clone(), )?; plugin_loader .load_module_from_memory() @@ -314,6 +327,8 @@ impl<'a> PluginLoader<'a> { plugin_map: Arc>, connected_clients: Arc>>, loading_indication: &mut LoadingIndication, + path_to_default_shell: PathBuf, + zellij_cwd: PathBuf, ) -> Result<()> { let err_context = || format!("failed to reload plugin id {plugin_id}"); @@ -333,6 +348,8 @@ impl<'a> PluginLoader<'a> { first_client_id, &store, &plugin_dir, + path_to_default_shell, + zellij_cwd, )?; plugin_loader .compile_module() @@ -363,6 +380,8 @@ impl<'a> PluginLoader<'a> { plugin_dir: &'a PathBuf, tab_index: usize, size: Size, + path_to_default_shell: PathBuf, + zellij_cwd: PathBuf, ) -> Result { let plugin_own_data_dir = ZELLIJ_SESSION_CACHE_DIR .join(Url::from(&plugin.location).to_string()) @@ -383,6 +402,8 @@ impl<'a> PluginLoader<'a> { plugin_own_data_dir, size, wasm_blob_on_hd: None, + path_to_default_shell, + zellij_cwd, }) } pub fn new_from_existing_plugin_attributes( @@ -394,6 +415,8 @@ impl<'a> PluginLoader<'a> { client_id: ClientId, store: &Store, plugin_dir: &'a PathBuf, + path_to_default_shell: PathBuf, + zellij_cwd: PathBuf, ) -> Result { let err_context = || "Failed to find existing plugin"; let (running_plugin, _subscriptions, _workers) = { @@ -421,6 +444,8 @@ impl<'a> PluginLoader<'a> { plugin_dir, tab_index, size, + path_to_default_shell, + zellij_cwd, ) } pub fn new_from_different_client_id( @@ -432,6 +457,8 @@ impl<'a> PluginLoader<'a> { client_id: ClientId, store: &Store, plugin_dir: &'a PathBuf, + path_to_default_shell: PathBuf, + zellij_cwd: PathBuf, ) -> Result { let err_context = || "Failed to find existing plugin"; let running_plugin = { @@ -460,6 +487,8 @@ impl<'a> PluginLoader<'a> { plugin_dir, tab_index, size, + path_to_default_shell, + zellij_cwd, ) } pub fn load_module_from_memory(&mut self) -> Result { @@ -625,7 +654,8 @@ impl<'a> PluginLoader<'a> { let worker = RunningWorker::new(instance, &function_name, plugin_config, plugin_env); - workers.insert(function_name.into(), Arc::new(Mutex::new(worker))); + let worker_sender = plugin_worker(worker); + workers.insert(function_name.into(), worker_sender); } } start_function.call(&[]).with_context(err_context)?; @@ -689,6 +719,8 @@ impl<'a> PluginLoader<'a> { *client_id, &self.store, &self.plugin_dir, + self.path_to_default_shell.clone(), + self.zellij_cwd.clone(), )?; plugin_loader_for_client .load_module_from_memory() @@ -746,7 +778,7 @@ impl<'a> PluginLoader<'a> { }; let mut wasi_env = WasiState::new("Zellij") .env("CLICOLOR_FORCE", "1") - .map_dir("/host", ".") + .map_dir("/host", self.zellij_cwd.clone()) .and_then(|wasi| wasi.map_dir("/data", &self.plugin_own_data_dir)) .and_then(|wasi| wasi.map_dir("/tmp", ZELLIJ_TMP_DIR.as_path())) .and_then(|wasi| { @@ -771,6 +803,7 @@ impl<'a> PluginLoader<'a> { wasi_env, plugin_own_data_dir: self.plugin_own_data_dir.clone(), tab_index: self.tab_index, + path_to_default_shell: self.path_to_default_shell.clone(), }; let subscriptions = Arc::new(Mutex::new(HashSet::new())); diff --git a/zellij-server/src/plugins/plugin_map.rs b/zellij-server/src/plugins/plugin_map.rs index 0c3df931e2..040c9f9fda 100644 --- a/zellij-server/src/plugins/plugin_map.rs +++ b/zellij-server/src/plugins/plugin_map.rs @@ -1,5 +1,4 @@ -use crate::plugins::plugin_loader::{PluginLoader, VersionMismatchError}; -use crate::plugins::zellij_exports::wasi_write_object; +use crate::plugins::plugin_worker::MessageToWorker; use crate::plugins::PluginId; use std::{ collections::{HashMap, HashSet}, @@ -11,10 +10,10 @@ use wasmer_wasi::WasiEnv; use crate::{thread_bus::ThreadSenders, ClientId}; +use zellij_utils::async_channel::Sender; use zellij_utils::errors::prelude::*; use zellij_utils::{ - consts::VERSION, data::EventType, input::layout::RunPluginLocation, - input::plugins::PluginConfig, + data::EventType, input::layout::RunPluginLocation, input::plugins::PluginConfig, }; // the idea here is to provide atomicity when adding/removing plugins from the map (eg. when a new @@ -29,7 +28,7 @@ pub struct PluginMap { ( Arc>, Arc>, - HashMap>>, + HashMap>, ), >, } @@ -41,7 +40,7 @@ impl PluginMap { ) -> Vec<( Arc>, Arc>, - HashMap>>, + HashMap>, )> { let mut removed = vec![]; let ids_in_plugin_map: Vec<(PluginId, ClientId)> = @@ -62,7 +61,7 @@ impl PluginMap { ) -> Option<( Arc>, Arc>, - HashMap>>, + HashMap>, )> { self.plugin_assets.remove(&(plugin_id, client_id)) } @@ -132,12 +131,12 @@ impl PluginMap { .and_then(|(_, (running_plugin, _, _))| Some(running_plugin.clone())), } } - pub fn clone_worker( + pub fn worker_sender( &self, plugin_id: PluginId, client_id: ClientId, worker_name: &str, - ) -> Option>> { + ) -> Option> { self.plugin_assets .iter() .find(|((p_id, c_id), _)| p_id == &plugin_id && c_id == &client_id) @@ -174,7 +173,7 @@ impl PluginMap { client_id: ClientId, running_plugin: Arc>, subscriptions: Arc>, - running_workers: HashMap>>, + running_workers: HashMap>, ) { self.plugin_assets.insert( (plugin_id, client_id), @@ -195,6 +194,7 @@ pub struct PluginEnv { pub client_id: ClientId, #[allow(dead_code)] pub plugin_own_data_dir: PathBuf, + pub path_to_default_shell: PathBuf, } impl PluginEnv { @@ -256,53 +256,3 @@ impl RunningPlugin { } } } - -pub struct RunningWorker { - pub instance: Instance, - pub name: String, - pub plugin_config: PluginConfig, - pub plugin_env: PluginEnv, -} - -impl RunningWorker { - pub fn new( - instance: Instance, - name: &str, - plugin_config: PluginConfig, - plugin_env: PluginEnv, - ) -> Self { - RunningWorker { - instance, - name: name.into(), - plugin_config, - plugin_env, - } - } - pub fn send_message(&self, message: String, payload: String) -> Result<()> { - let err_context = || format!("Failed to send message to worker"); - - let work_function = self - .instance - .exports - .get_function(&self.name) - .with_context(err_context)?; - wasi_write_object(&self.plugin_env.wasi_env, &(message, payload)) - .with_context(err_context)?; - work_function.call(&[]).or_else::(|e| { - match e.downcast::() { - Ok(_) => panic!( - "{}", - anyError::new(VersionMismatchError::new( - VERSION, - "Unavailable", - &self.plugin_config.path, - self.plugin_config.is_builtin(), - )) - ), - Err(e) => Err(e).with_context(err_context), - } - })?; - - Ok(()) - } -} diff --git a/zellij-server/src/plugins/plugin_worker.rs b/zellij-server/src/plugins/plugin_worker.rs new file mode 100644 index 0000000000..bc7303c7c1 --- /dev/null +++ b/zellij-server/src/plugins/plugin_worker.rs @@ -0,0 +1,89 @@ +use crate::plugins::plugin_loader::VersionMismatchError; +use crate::plugins::plugin_map::PluginEnv; +use crate::plugins::zellij_exports::wasi_write_object; +use wasmer::Instance; + +use zellij_utils::async_channel::{unbounded, Receiver, Sender}; +use zellij_utils::async_std::task; +use zellij_utils::errors::prelude::*; +use zellij_utils::{consts::VERSION, input::plugins::PluginConfig}; + +pub struct RunningWorker { + pub instance: Instance, + pub name: String, + pub plugin_config: PluginConfig, + pub plugin_env: PluginEnv, +} + +impl RunningWorker { + pub fn new( + instance: Instance, + name: &str, + plugin_config: PluginConfig, + plugin_env: PluginEnv, + ) -> Self { + RunningWorker { + instance, + name: name.into(), + plugin_config, + plugin_env, + } + } + pub fn send_message(&self, message: String, payload: String) -> Result<()> { + let err_context = || format!("Failed to send message to worker"); + + let work_function = self + .instance + .exports + .get_function(&self.name) + .with_context(err_context)?; + wasi_write_object(&self.plugin_env.wasi_env, &(message, payload)) + .with_context(err_context)?; + work_function.call(&[]).or_else::(|e| { + match e.downcast::() { + Ok(_) => panic!( + "{}", + anyError::new(VersionMismatchError::new( + VERSION, + "Unavailable", + &self.plugin_config.path, + self.plugin_config.is_builtin(), + )) + ), + Err(e) => Err(e).with_context(err_context), + } + })?; + + Ok(()) + } +} + +pub enum MessageToWorker { + Message(String, String), // message, payload + Exit, +} + +pub fn plugin_worker(worker: RunningWorker) -> Sender { + let (sender, receiver): (Sender, Receiver) = unbounded(); + task::spawn({ + async move { + loop { + match receiver.recv().await { + Ok(MessageToWorker::Message(message, payload)) => { + if let Err(e) = worker.send_message(message, payload) { + log::error!("Failed to send message to worker: {:?}", e); + } + }, + Ok(MessageToWorker::Exit) => { + break; + }, + Err(e) => { + log::error!("Failed to receive worker message on channel: {:?}", e); + break; + }, + } + } + } + }); + sender +} diff --git a/zellij-server/src/plugins/unit/plugin_tests.rs b/zellij-server/src/plugins/unit/plugin_tests.rs index d1e79a5fed..d41a477b72 100644 --- a/zellij-server/src/plugins/unit/plugin_tests.rs +++ b/zellij-server/src/plugins/unit/plugin_tests.rs @@ -50,11 +50,14 @@ macro_rules! log_actions_in_thread { }; } -fn create_plugin_thread() -> ( +fn create_plugin_thread( + zellij_cwd: Option, +) -> ( SenderWithContext, Receiver<(ScreenInstruction, ErrorContext)>, Box, ) { + let zellij_cwd = zellij_cwd.unwrap_or_else(|| PathBuf::from(".")); let (to_server, _server_receiver): ChannelWithContext = channels::bounded(50); let to_server = SenderWithContext::new(to_server); @@ -88,6 +91,7 @@ fn create_plugin_thread() -> ( .should_silently_fail(); let store = Store::new(&wasmer::Universal::new(wasmer::Singlepass::default()).engine()); let data_dir = PathBuf::from(tempdir().unwrap().path()); + let default_shell = PathBuf::from("."); let _plugin_thread = std::thread::Builder::new() .name("plugin_thread".to_string()) .spawn(move || { @@ -98,6 +102,8 @@ fn create_plugin_thread() -> ( data_dir, PluginsConfig::default(), Box::new(Layout::default()), + default_shell, + zellij_cwd, ) .expect("TEST") }) @@ -134,7 +140,7 @@ pub fn load_new_plugin_from_hd() { // message (this is what the fixture plugin does) // we then listen on our mock screen receiver to make sure we got a PluginBytes instruction // that contains said render, and assert against it - let (plugin_thread_sender, screen_receiver, mut teardown) = create_plugin_thread(); + let (plugin_thread_sender, screen_receiver, mut teardown) = create_plugin_thread(None); let plugin_should_float = Some(false); let plugin_title = Some("test_plugin".to_owned()); let run_plugin = RunPlugin { @@ -192,7 +198,7 @@ pub fn load_new_plugin_from_hd() { #[test] #[ignore] pub fn plugin_workers() { - let (plugin_thread_sender, screen_receiver, mut teardown) = create_plugin_thread(); + let (plugin_thread_sender, screen_receiver, mut teardown) = create_plugin_thread(None); let plugin_should_float = Some(false); let plugin_title = Some("test_plugin".to_owned()); let run_plugin = RunPlugin { @@ -253,7 +259,7 @@ pub fn plugin_workers() { #[test] #[ignore] pub fn plugin_workers_persist_state() { - let (plugin_thread_sender, screen_receiver, mut teardown) = create_plugin_thread(); + let (plugin_thread_sender, screen_receiver, mut teardown) = create_plugin_thread(None); let plugin_should_float = Some(false); let plugin_title = Some("test_plugin".to_owned()); let run_plugin = RunPlugin { @@ -318,3 +324,71 @@ pub fn plugin_workers_persist_state() { }); assert_snapshot!(format!("{:#?}", plugin_bytes_event)); } + +#[test] +#[ignore] +pub fn can_subscribe_to_hd_events() { + 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::PluginBytes, + screen_receiver, + 3 + ); + + 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::InputReceived, + )])); // will be cached and sent to the plugin once it's loaded + std::thread::sleep(std::time::Duration::from_millis(100)); + std::fs::OpenOptions::new() + .create(true) + .write(true) + .open(PathBuf::from(temp_folder.path()).join("test1")) + .unwrap(); + screen_thread.join().unwrap(); // this might take a while if the cache is cold + teardown(); + let plugin_bytes_event = received_screen_instructions + .lock() + .unwrap() + .iter() + .find_map(|i| { + if let ScreenInstruction::PluginBytes(plugin_bytes) = i { + for (plugin_id, client_id, plugin_bytes) in plugin_bytes { + let plugin_bytes = String::from_utf8_lossy(plugin_bytes).to_string(); + if plugin_bytes.contains("FileSystem") { + return Some((*plugin_id, *client_id, plugin_bytes)); + } + } + } + None + }); + assert_snapshot!(format!("{:#?}", plugin_bytes_event)); +} diff --git a/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__can_subscribe_to_hd_events.snap b/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__can_subscribe_to_hd_events.snap new file mode 100644 index 0000000000..57e31f15e0 --- /dev/null +++ b/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__can_subscribe_to_hd_events.snap @@ -0,0 +1,12 @@ +--- +source: zellij-server/src/plugins/./unit/plugin_tests.rs +assertion_line: 387 +expression: "format!(\"{:#?}\", plugin_bytes_event)" +--- +Some( + ( + 0, + 1, + "Rows: 20, Cols: 121, Received events: [InputReceived, FileSystemRead([\"/host/test1\"])]\n\r", + ), +) diff --git a/zellij-server/src/plugins/wasm_bridge.rs b/zellij-server/src/plugins/wasm_bridge.rs index 0015552c1b..1ede55e231 100644 --- a/zellij-server/src/plugins/wasm_bridge.rs +++ b/zellij-server/src/plugins/wasm_bridge.rs @@ -1,24 +1,24 @@ use super::{PluginId, PluginInstruction}; use crate::plugins::plugin_loader::{PluginLoader, VersionMismatchError}; -use crate::plugins::plugin_map::{ - AtomicEvent, PluginEnv, PluginMap, RunningPlugin, RunningWorker, Subscriptions, -}; +use crate::plugins::plugin_map::{AtomicEvent, PluginEnv, PluginMap, RunningPlugin, Subscriptions}; +use crate::plugins::plugin_worker::MessageToWorker; +use crate::plugins::watch_filesystem::watch_filesystem; use crate::plugins::zellij_exports::{wasi_read_string, wasi_write_object}; use log::info; use std::{ collections::{HashMap, HashSet}, path::PathBuf, str::FromStr, - sync::{Arc, Mutex, TryLockError}, + sync::{Arc, Mutex}, }; use wasmer::{Instance, Module, Store, Value}; use zellij_utils::async_std::task::{self, JoinHandle}; +use zellij_utils::notify::{RecommendedWatcher, Watcher}; use crate::{ background_jobs::BackgroundJob, screen::ScreenInstruction, thread_bus::ThreadSenders, ui::loading_indication::LoadingIndication, ClientId, }; - use zellij_utils::{ consts::VERSION, data::{Event, EventType}, @@ -30,8 +30,6 @@ use zellij_utils::{ pane_size::Size, }; -const RETRY_INTERVAL_MS: u64 = 100; - pub struct WasmBridge { connected_clients: Arc>>, plugins: PluginsConfig, @@ -49,6 +47,9 @@ pub struct WasmBridge { // payload> loading_plugins: HashMap<(PluginId, RunPlugin), JoinHandle<()>>, // plugin_id to join-handle pending_plugin_reloads: HashSet, + path_to_default_shell: PathBuf, + watcher: Option, + zellij_cwd: PathBuf, } impl WasmBridge { @@ -57,11 +58,20 @@ impl WasmBridge { senders: ThreadSenders, store: Store, plugin_dir: PathBuf, + path_to_default_shell: PathBuf, + zellij_cwd: PathBuf, ) -> Self { let plugin_map = Arc::new(Mutex::new(PluginMap::default())); let connected_clients: Arc>> = Arc::new(Mutex::new(vec![])); let plugin_cache: Arc>> = Arc::new(Mutex::new(HashMap::new())); + let watcher = match watch_filesystem(senders.clone(), &zellij_cwd) { + Ok(watcher) => Some(watcher), + Err(e) => { + log::error!("Failed to watch filesystem: {:?}", e); + None + }, + }; WasmBridge { connected_clients, plugins, @@ -70,12 +80,15 @@ impl WasmBridge { plugin_dir, plugin_cache, plugin_map, + path_to_default_shell, + watcher, next_plugin_id: 0, cached_events_for_pending_plugins: HashMap::new(), cached_resizes_for_pending_plugins: HashMap::new(), cached_worker_messages: HashMap::new(), loading_plugins: HashMap::new(), pending_plugin_reloads: HashSet::new(), + zellij_cwd, } } pub fn load_plugin( @@ -122,6 +135,8 @@ impl WasmBridge { let store = self.store.clone(); let plugin_map = self.plugin_map.clone(); let connected_clients = self.connected_clients.clone(); + let path_to_default_shell = self.path_to_default_shell.clone(); + let zellij_cwd = self.zellij_cwd.clone(); async move { let _ = senders.send_to_background_jobs(BackgroundJob::AnimatePluginLoading(plugin_id)); @@ -139,6 +154,8 @@ impl WasmBridge { size, connected_clients.clone(), &mut loading_indication, + path_to_default_shell, + zellij_cwd.clone(), ) { Ok(_) => handle_plugin_successful_loading(&senders, plugin_id), Err(e) => handle_plugin_loading_failure( @@ -160,7 +177,10 @@ impl WasmBridge { pub fn unload_plugin(&mut self, pid: PluginId) -> Result<()> { info!("Bye from plugin {}", &pid); let mut plugin_map = self.plugin_map.lock().unwrap(); - for (running_plugin, _, _) in plugin_map.remove_plugins(pid) { + for (running_plugin, _, workers) in plugin_map.remove_plugins(pid) { + for (_worker_name, worker_sender) in workers { + drop(worker_sender.send(MessageToWorker::Exit)); + } let running_plugin = running_plugin.lock().unwrap(); let cache_dir = running_plugin.plugin_env.plugin_own_data_dir.clone(); if let Err(e) = std::fs::remove_dir_all(cache_dir) { @@ -195,6 +215,8 @@ impl WasmBridge { let store = self.store.clone(); let plugin_map = self.plugin_map.clone(); let connected_clients = self.connected_clients.clone(); + let path_to_default_shell = self.path_to_default_shell.clone(); + let zellij_cwd = self.zellij_cwd.clone(); async move { match PluginLoader::reload_plugin( first_plugin_id, @@ -205,6 +227,8 @@ impl WasmBridge { plugin_map.clone(), connected_clients.clone(), &mut loading_indication, + path_to_default_shell.clone(), + zellij_cwd.clone(), ) { Ok(_) => { handle_plugin_successful_loading(&senders, first_plugin_id); @@ -223,6 +247,8 @@ impl WasmBridge { plugin_map.clone(), connected_clients.clone(), &mut loading_indication, + path_to_default_shell.clone(), + zellij_cwd.clone(), ) { Ok(_) => handle_plugin_successful_loading(&senders, *plugin_id), Err(e) => handle_plugin_loading_failure( @@ -263,6 +289,8 @@ impl WasmBridge { self.plugin_map.clone(), self.connected_clients.clone(), &mut loading_indication, + self.path_to_default_shell.clone(), + self.zellij_cwd.clone(), ) { Ok(_) => { let _ = self @@ -414,7 +442,17 @@ impl WasmBridge { )); }, Err(e) => { - log::error!("{}", e); + log::error!("{:?}", e); + + // https://stackoverflow.com/questions/66450942/in-rust-is-there-a-way-to-make-literal-newlines-in-r-using-windows-c + let stringified_error = + format!("{:?}", e).replace("\n", "\n\r"); + + handle_plugin_crash( + plugin_id, + stringified_error, + senders.clone(), + ); }, } } @@ -460,6 +498,7 @@ impl WasmBridge { for plugin_id in &plugin_ids { drop(self.unload_plugin(*plugin_id)); } + drop(self.watcher.as_mut().map(|w| w.unwatch(&self.zellij_cwd))); } fn run_plugin_of_plugin_id(&self, plugin_id: PluginId) -> Option<&RunPlugin> { self.loading_plugins @@ -599,36 +638,23 @@ impl WasmBridge { self.plugin_map .lock() .unwrap() - .clone_worker(plugin_id, client_id, &worker_name); - let mut cache_messages = || { - for (message, payload) in messages.drain(..) { - self.cached_worker_messages - .entry(plugin_id) - .or_default() - .push((client_id, worker_name.clone(), message, payload)); - } - }; + .worker_sender(plugin_id, client_id, &worker_name); match worker { Some(worker) => { - let worker_is_busy = { worker.try_lock().is_err() }; - if worker_is_busy { - // most messages will be caught here, we do this once before the async task to - // bulk most messages together and prevent them from cascading - cache_messages(); - } else { - async_send_messages_to_worker( - self.senders.clone(), - messages, - worker, - plugin_id, - client_id, - worker_name, - ); + for (message, payload) in messages.drain(..) { + if let Err(e) = worker.try_send(MessageToWorker::Message(message, payload)) { + log::error!("Failed to send message to worker: {:?}", e); + } } }, None => { - log::warn!("Worker {worker_name} not found, placing message in cache"); - cache_messages(); + log::warn!("Worker {worker_name} not found, caching messages"); + for (message, payload) in messages.drain(..) { + self.cached_worker_messages + .entry(plugin_id) + .or_default() + .push((client_id, worker_name.clone(), message, payload)); + } }, } Ok(()) @@ -708,52 +734,12 @@ pub fn apply_event_to_plugin( Ok(()) } -fn async_send_messages_to_worker( - senders: ThreadSenders, - mut messages: Vec<(String, String)>, - worker: Arc>, - plugin_id: PluginId, - client_id: ClientId, - worker_name: String, -) { - task::spawn({ - async move { - match worker.try_lock() { - Ok(worker) => { - for (message, payload) in messages.drain(..) { - worker.send_message(message, payload).ok(); - } - let _ = senders - .send_to_plugin(PluginInstruction::ApplyCachedWorkerMessages(plugin_id)); - }, - Err(TryLockError::WouldBlock) => { - task::spawn({ - async move { - log::warn!( - "Worker {} busy, retrying sending message after: {}ms", - worker_name, - RETRY_INTERVAL_MS - ); - task::sleep(std::time::Duration::from_millis(RETRY_INTERVAL_MS)).await; - let _ = senders.send_to_plugin( - PluginInstruction::PostMessagesToPluginWorker( - plugin_id, - client_id, - worker_name, - messages, - ), - ); - } - }); - }, - Err(e) => { - log::error!( - "Failed to send message to worker \"{}\": {:?}", - worker_name, - e - ); - }, - } - } - }); +pub fn handle_plugin_crash(plugin_id: PluginId, message: String, senders: ThreadSenders) { + let mut loading_indication = LoadingIndication::new("Panic!".to_owned()); + loading_indication.indicate_loading_error(message); + let _ = senders.send_to_screen(ScreenInstruction::UpdatePluginLoadingStage( + plugin_id, + loading_indication, + )); + let _ = senders.send_to_plugin(PluginInstruction::Unload(plugin_id)); } diff --git a/zellij-server/src/plugins/watch_filesystem.rs b/zellij-server/src/plugins/watch_filesystem.rs new file mode 100644 index 0000000000..864bd45f98 --- /dev/null +++ b/zellij-server/src/plugins/watch_filesystem.rs @@ -0,0 +1,63 @@ +use super::PluginInstruction; +use std::path::PathBuf; + +use crate::thread_bus::ThreadSenders; +use std::path::Path; + +use zellij_utils::{data::Event, errors::prelude::*}; + +use zellij_utils::notify::{self, EventKind, RecommendedWatcher, RecursiveMode, Watcher}; +pub fn watch_filesystem(senders: ThreadSenders, zellij_cwd: &Path) -> Result { + let path_prefix_in_plugins = PathBuf::from("/host"); + let current_dir = PathBuf::from(zellij_cwd); + let mut watcher = notify::recommended_watcher({ + move |res: notify::Result| match res { + Ok(event) => { + let paths: Vec = event + .paths + .iter() + .map(|p| { + let stripped_prefix_path = + p.strip_prefix(¤t_dir).unwrap_or_else(|_| p); + path_prefix_in_plugins.join(stripped_prefix_path) + }) + .collect(); + match event.kind { + EventKind::Access(_) => { + let _ = senders.send_to_plugin(PluginInstruction::Update(vec![( + None, + None, + Event::FileSystemRead(paths), + )])); + }, + EventKind::Create(_) => { + let _ = senders.send_to_plugin(PluginInstruction::Update(vec![( + None, + None, + Event::FileSystemCreate(paths), + )])); + }, + EventKind::Modify(_) => { + let _ = senders.send_to_plugin(PluginInstruction::Update(vec![( + None, + None, + Event::FileSystemUpdate(paths), + )])); + }, + EventKind::Remove(_) => { + let _ = senders.send_to_plugin(PluginInstruction::Update(vec![( + None, + None, + Event::FileSystemDelete(paths), + )])); + }, + _ => {}, + } + }, + Err(e) => log::error!("watch error: {:?}", e), + } + })?; + + watcher.watch(zellij_cwd, RecursiveMode::Recursive)?; + Ok(watcher) +} diff --git a/zellij-server/src/plugins/zellij_exports.rs b/zellij-server/src/plugins/zellij_exports.rs index 3515cb3485..d64d867b70 100644 --- a/zellij-server/src/plugins/zellij_exports.rs +++ b/zellij-server/src/plugins/zellij_exports.rs @@ -1,5 +1,6 @@ use super::PluginInstruction; use crate::plugins::plugin_map::{PluginEnv, Subscriptions}; +use crate::plugins::wasm_bridge::handle_plugin_crash; use log::{debug, warn}; use serde::{de::DeserializeOwned, Serialize}; use std::{ @@ -23,7 +24,10 @@ use zellij_utils::{ consts::VERSION, data::{Event, EventType, PluginIds}, errors::prelude::*, - input::{command::TerminalAction, plugins::PluginType}, + input::{ + command::{RunCommand, TerminalAction}, + plugins::PluginType, + }, serde, }; @@ -50,13 +54,18 @@ pub fn zellij_exports( host_get_plugin_ids, host_get_zellij_version, host_open_file, + host_open_file_floating, host_open_file_with_line, + host_open_file_with_line_floating, + host_open_terminal, + host_open_terminal_floating, host_switch_tab_to, host_set_timeout, host_exec_cmd, host_report_panic, host_post_message_to, host_post_message_to_plugin, + host_hide_self, } } @@ -160,9 +169,30 @@ fn host_open_file(env: &ForeignFunctionEnv) { .senders .send_to_pty(PtyInstruction::SpawnTerminal( Some(TerminalAction::OpenFile(path, None, None)), + Some(false), None, + ClientOrTabIndex::ClientId(env.plugin_env.client_id), + )) + }) + .with_context(|| { + format!( + "failed to open file on host from plugin {}", + env.plugin_env.name() + ) + }) + .non_fatal(); +} + +fn host_open_file_floating(env: &ForeignFunctionEnv) { + wasi_read_object::(&env.plugin_env.wasi_env) + .and_then(|path| { + env.plugin_env + .senders + .send_to_pty(PtyInstruction::SpawnTerminal( + Some(TerminalAction::OpenFile(path, None, None)), + Some(true), None, - ClientOrTabIndex::TabIndex(env.plugin_env.tab_index), + ClientOrTabIndex::ClientId(env.plugin_env.client_id), )) }) .with_context(|| { @@ -181,9 +211,30 @@ fn host_open_file_with_line(env: &ForeignFunctionEnv) { .senders .send_to_pty(PtyInstruction::SpawnTerminal( Some(TerminalAction::OpenFile(path, Some(line), None)), // TODO: add cwd + Some(false), None, + ClientOrTabIndex::ClientId(env.plugin_env.client_id), + )) + }) + .with_context(|| { + format!( + "failed to open file on host from plugin {}", + env.plugin_env.name() + ) + }) + .non_fatal(); +} + +fn host_open_file_with_line_floating(env: &ForeignFunctionEnv) { + wasi_read_object::<(PathBuf, usize)>(&env.plugin_env.wasi_env) + .and_then(|(path, line)| { + env.plugin_env + .senders + .send_to_pty(PtyInstruction::SpawnTerminal( + Some(TerminalAction::OpenFile(path, Some(line), None)), // TODO: add cwd + Some(true), None, - ClientOrTabIndex::TabIndex(env.plugin_env.tab_index), + ClientOrTabIndex::ClientId(env.plugin_env.client_id), )) }) .with_context(|| { @@ -195,6 +246,54 @@ fn host_open_file_with_line(env: &ForeignFunctionEnv) { .non_fatal(); } +fn host_open_terminal(env: &ForeignFunctionEnv) { + wasi_read_object::(&env.plugin_env.wasi_env) + .and_then(|path| { + env.plugin_env + .senders + .send_to_pty(PtyInstruction::SpawnTerminal( + Some(TerminalAction::RunCommand( + RunCommand::new(env.plugin_env.path_to_default_shell.clone()) + .with_cwd(path), + )), + Some(false), + None, + ClientOrTabIndex::ClientId(env.plugin_env.client_id), + )) + }) + .with_context(|| { + format!( + "failed to open terminal on host from plugin {}", + env.plugin_env.name() + ) + }) + .non_fatal(); +} + +fn host_open_terminal_floating(env: &ForeignFunctionEnv) { + wasi_read_object::(&env.plugin_env.wasi_env) + .and_then(|path| { + env.plugin_env + .senders + .send_to_pty(PtyInstruction::SpawnTerminal( + Some(TerminalAction::RunCommand( + RunCommand::new(env.plugin_env.path_to_default_shell.clone()) + .with_cwd(path), + )), + Some(true), + None, + ClientOrTabIndex::ClientId(env.plugin_env.client_id), + )) + }) + .with_context(|| { + format!( + "failed to open terminal on host from plugin {}", + env.plugin_env.name() + ) + }) + .non_fatal(); +} + fn host_switch_tab_to(env: &ForeignFunctionEnv, tab_idx: u32) { env.plugin_env .senders @@ -314,6 +413,17 @@ fn host_post_message_to_plugin(env: &ForeignFunctionEnv) { .fatal(); } +fn host_hide_self(env: &ForeignFunctionEnv) { + env.plugin_env + .senders + .send_to_screen(ScreenInstruction::SuppressPane( + PaneId::Plugin(env.plugin_env.plugin_id), + env.plugin_env.client_id, + )) + .with_context(|| format!("failed to hide self")) + .fatal(); +} + // Custom panic handler for plugins. // // This is called when a panic occurs in a plugin. Since most panics will likely originate in the @@ -328,7 +438,12 @@ fn host_report_panic(env: &ForeignFunctionEnv) { ) }) .fatal(); - panic!("{}", msg); + log::error!("PANIC IN PLUGIN! {}", msg); + handle_plugin_crash( + env.plugin_env.plugin_id, + msg, + env.plugin_env.senders.clone(), + ); } // Helper Functions --------------------------------------------------------------------------------------------------- diff --git a/zellij-server/src/pty.rs b/zellij-server/src/pty.rs index fb58aef8bd..84f282f8de 100644 --- a/zellij-server/src/pty.rs +++ b/zellij-server/src/pty.rs @@ -7,7 +7,7 @@ use crate::{ ClientId, ServerInstruction, }; use async_std::task::{self, JoinHandle}; -use std::{collections::HashMap, env, os::unix::io::RawFd, path::PathBuf}; +use std::{collections::HashMap, os::unix::io::RawFd, path::PathBuf}; use zellij_utils::nix::unistd::Pid; use zellij_utils::{ async_std, @@ -468,10 +468,7 @@ impl Pty { default_shell }, None => { - let shell = PathBuf::from(env::var("SHELL").unwrap_or_else(|_| { - log::warn!("Cannot read SHELL env, falling back to use /bin/sh"); - "/bin/sh".to_string() - })); + let shell = get_default_shell(); TerminalAction::RunCommand(RunCommand { args: vec![], command: shell, @@ -1048,3 +1045,10 @@ fn send_command_not_found_to_screen( .with_context(err_context)?; Ok(()) } + +pub fn get_default_shell() -> PathBuf { + PathBuf::from(std::env::var("SHELL").unwrap_or_else(|_| { + log::warn!("Cannot read SHELL env, falling back to use /bin/sh"); + "/bin/sh".to_string() + })) +} diff --git a/zellij-server/src/route.rs b/zellij-server/src/route.rs index f34553796d..c43e99f170 100644 --- a/zellij-server/src/route.rs +++ b/zellij-server/src/route.rs @@ -702,6 +702,16 @@ pub(crate) fn route_action( )) .with_context(err_context)?; }, + Action::LaunchOrFocusPlugin(run_plugin, should_float) => { + session + .senders + .send_to_screen(ScreenInstruction::LaunchOrFocusPlugin( + run_plugin, + should_float, + client_id, + )) + .with_context(err_context)?; + }, } Ok(should_break) } diff --git a/zellij-server/src/screen.rs b/zellij-server/src/screen.rs index 030a63a57f..e88baf29e8 100644 --- a/zellij-server/src/screen.rs +++ b/zellij-server/src/screen.rs @@ -273,6 +273,8 @@ pub enum ScreenInstruction { StartPluginLoadingIndication(u32, LoadingIndication), // u32 - plugin_id ProgressPluginLoadingOffset(u32), // u32 - plugin id RequestStateUpdateForPlugins, + LaunchOrFocusPlugin(RunPlugin, bool, ClientId), // bool is should_float + SuppressPane(PaneId, ClientId), } impl From<&ScreenInstruction> for ScreenContext { @@ -435,6 +437,8 @@ impl From<&ScreenInstruction> for ScreenContext { ScreenInstruction::RequestStateUpdateForPlugins => { ScreenContext::RequestStateUpdateForPlugins }, + ScreenInstruction::LaunchOrFocusPlugin(..) => ScreenContext::LaunchOrFocusPlugin, + ScreenInstruction::SuppressPane(..) => ScreenContext::SuppressPane, } } } @@ -1462,6 +1466,24 @@ impl Screen { self.render() } + pub fn focus_plugin_pane( + &mut self, + run_plugin: &RunPlugin, + should_float: bool, + client_id: ClientId, + ) -> Result { + // true => found and focused, false => not + let all_tabs = self.get_tabs_mut(); + for tab in all_tabs.values_mut() { + if let Some(plugin_pane_id) = tab.find_plugin(&run_plugin) { + tab.focus_pane_with_id(plugin_pane_id, should_float, client_id) + .context("failed to focus plugin pane")?; + return Ok(true); + } + } + Ok(false) + } + fn unblock_input(&self) -> Result<()> { self.bus .senders @@ -1557,6 +1579,7 @@ pub(crate) fn screen_thread_main( client_id: ClientId| tab .new_pane(pid, initial_pane_title, should_float, + None, Some(client_id)), ?); if let Some(hold_for_command) = hold_for_command { @@ -1575,7 +1598,13 @@ pub(crate) fn screen_thread_main( }, ClientOrTabIndex::TabIndex(tab_index) => { if let Some(active_tab) = screen.tabs.get_mut(&tab_index) { - active_tab.new_pane(pid, initial_pane_title, should_float, None)?; + active_tab.new_pane( + pid, + initial_pane_title, + should_float, + None, + None, + )?; if let Some(hold_for_command) = hold_for_command { let is_first_run = true; active_tab.hold_pane(pid, None, is_first_run, hold_for_command); @@ -2558,11 +2587,11 @@ pub(crate) fn screen_thread_main( pane_title.unwrap_or_else(|| run_plugin_location.location.to_string()); let run_plugin = Run::Plugin(run_plugin_location); if let Some(active_tab) = screen.tabs.get_mut(&tab_index) { - active_tab.new_plugin_pane( + active_tab.new_pane( PaneId::Plugin(plugin_id), - pane_title, + Some(pane_title), should_float, - run_plugin, + Some(run_plugin), None, )?; } else { @@ -2608,6 +2637,46 @@ pub(crate) fn screen_thread_main( screen.update_tabs()?; screen.render()?; }, + ScreenInstruction::LaunchOrFocusPlugin(run_plugin, should_float, client_id) => { + let client_id = if screen.active_tab_indices.contains_key(&client_id) { + Some(client_id) + } else { + screen.get_first_client_id() + }; + let client_id_and_focused_tab = client_id.and_then(|client_id| { + screen + .active_tab_indices + .get(&client_id) + .map(|tab_index| (*tab_index, client_id)) + }); + match client_id_and_focused_tab { + Some((tab_index, client_id)) => { + if screen.focus_plugin_pane(&run_plugin, should_float, client_id)? { + screen.render()?; + } else { + screen.bus.senders.send_to_plugin(PluginInstruction::Load( + Some(should_float), + None, + run_plugin, + tab_index, + client_id, + Size::default(), + ))?; + } + }, + None => log::error!("No connected clients found - cannot load or focus plugin"), + } + }, + ScreenInstruction::SuppressPane(pane_id, client_id) => { + let all_tabs = screen.get_tabs_mut(); + for tab in all_tabs.values_mut() { + if tab.has_pane_with_pid(&pane_id) { + tab.suppress_pane(pane_id, client_id); + drop(screen.render()); + break; + } + } + }, } } Ok(()) diff --git a/zellij-server/src/tab/mod.rs b/zellij-server/src/tab/mod.rs index 4a37a0903a..e695117e21 100644 --- a/zellij-server/src/tab/mod.rs +++ b/zellij-server/src/tab/mod.rs @@ -47,8 +47,8 @@ use zellij_utils::{ input::{ command::TerminalAction, layout::{ - FloatingPaneLayout, Run, RunPluginLocation, SwapFloatingLayout, SwapTiledLayout, - TiledPaneLayout, + FloatingPaneLayout, Run, RunPlugin, RunPluginLocation, SwapFloatingLayout, + SwapTiledLayout, TiledPaneLayout, }, parse_keys, }, @@ -901,7 +901,6 @@ impl Tab { pub fn toggle_pane_embed_or_floating(&mut self, client_id: ClientId) -> Result<()> { let err_context = || format!("failed to toggle embedded/floating pane for client {client_id}"); - if self.tiled_panes.fullscreen_is_active() { self.tiled_panes.unset_fullscreen(); } @@ -914,55 +913,24 @@ impl Tab { "failed to find floating pane (ID: {focused_floating_pane_id:?}) to embed for client {client_id}", )) .with_context(err_context)?; - self.tiled_panes - .insert_pane(focused_floating_pane_id, floating_pane_to_embed); - self.should_clear_display_before_rendering = true; - self.tiled_panes - .focus_pane(focused_floating_pane_id, client_id); self.hide_floating_panes(); - if self.auto_layout && !self.swap_layouts.is_tiled_damaged() { - // only do this if we're already in this layout, otherwise it might be - // confusing and not what the user intends - self.swap_layouts.set_is_tiled_damaged(); // we do this so that we won't skip to the - // next layout - self.next_swap_layout(Some(client_id), true)?; - } + self.add_tiled_pane( + floating_pane_to_embed, + focused_floating_pane_id, + Some(client_id), + )?; } } } else if let Some(focused_pane_id) = self.tiled_panes.focused_pane_id(client_id) { - if let Some(new_pane_geom) = self.floating_panes.find_room_for_new_pane() { - if self.get_selectable_tiled_panes().count() <= 1 { - // don't close the only pane on screen... - return Ok(()); - } - if let Some(mut embedded_pane_to_float) = - self.close_pane(focused_pane_id, true, Some(client_id)) - { - if !embedded_pane_to_float.borderless() { - // floating panes always have a frame unless they're explicitly borderless - embedded_pane_to_float.set_content_offset(Offset::frame(1)); - } - embedded_pane_to_float.set_geom(new_pane_geom); - resize_pty!( - embedded_pane_to_float, - self.os_api, - self.senders, - self.character_cell_size - ) - .with_context(err_context)?; - embedded_pane_to_float.set_active_at(Instant::now()); - self.floating_panes - .add_pane(focused_pane_id, embedded_pane_to_float); - self.floating_panes.focus_pane(focused_pane_id, client_id); - self.show_floating_panes(); - if self.auto_layout && !self.swap_layouts.is_floating_damaged() { - // only do this if we're already in this layout, otherwise it might be - // confusing and not what the user intends - self.swap_layouts.set_is_floating_damaged(); // we do this so that we won't skip to the - // next layout - self.next_swap_layout(Some(client_id), true)?; - } - } + if self.get_selectable_tiled_panes().count() <= 1 { + // don't close the only pane on screen... + return Ok(()); + } + if let Some(embedded_pane_to_float) = + self.close_pane(focused_pane_id, true, Some(client_id)) + { + self.show_floating_panes(); + self.add_floating_pane(embedded_pane_to_float, focused_pane_id, Some(client_id))?; } } Ok(()) @@ -1018,10 +986,10 @@ impl Tab { pid: PaneId, initial_pane_title: Option, should_float: Option, + run_plugin: Option, // only relevant if this is a plugin pane client_id: Option, ) -> Result<()> { let err_context = || format!("failed to create new pane with id {pid:?}"); - match should_float { Some(true) => self.show_floating_panes(), Some(false) => self.hide_floating_panes(), @@ -1029,198 +997,51 @@ impl Tab { }; self.close_down_to_max_terminals() .with_context(err_context)?; - if self.floating_panes.panes_are_visible() { - if let Some(new_pane_geom) = self.floating_panes.find_room_for_new_pane() { + let new_pane = match pid { + PaneId::Terminal(term_pid) => { let next_terminal_position = self.get_next_terminal_position(); - if let PaneId::Terminal(term_pid) = pid { - let mut new_pane = TerminalPane::new( - term_pid, - new_pane_geom, - self.style, - next_terminal_position, - String::new(), - self.link_handler.clone(), - self.character_cell_size.clone(), - self.sixel_image_store.clone(), - self.terminal_emulator_colors.clone(), - self.terminal_emulator_color_codes.clone(), - initial_pane_title, - None, - ); - new_pane.set_active_at(Instant::now()); - new_pane.set_content_offset(Offset::frame(1)); // floating panes always have a frame - resize_pty!( - new_pane, - self.os_api, - self.senders, - self.character_cell_size - ) - .with_context(err_context)?; - self.floating_panes.add_pane(pid, Box::new(new_pane)); - self.floating_panes.focus_pane_for_all_clients(pid); - } - if self.auto_layout && !self.swap_layouts.is_floating_damaged() { - // only do this if we're already in this layout, otherwise it might be - // confusing and not what the user intends - self.swap_layouts.set_is_floating_damaged(); // we do this so that we won't skip to the - // next layout - self.next_swap_layout(client_id, true)?; - } - } - } else { - if self.tiled_panes.fullscreen_is_active() { - self.tiled_panes.unset_fullscreen(); - } - let should_auto_layout = self.auto_layout && !self.swap_layouts.is_tiled_damaged(); - if self.tiled_panes.has_room_for_new_pane() { - if let PaneId::Terminal(term_pid) = pid { - let next_terminal_position = self.get_next_terminal_position(); - let mut new_terminal = TerminalPane::new( - term_pid, - PaneGeom::default(), // the initial size will be set later - self.style, - next_terminal_position, - String::new(), - self.link_handler.clone(), - self.character_cell_size.clone(), - self.sixel_image_store.clone(), - self.terminal_emulator_colors.clone(), - self.terminal_emulator_color_codes.clone(), - initial_pane_title, - None, - ); - new_terminal.set_active_at(Instant::now()); - if should_auto_layout { - // no need to relayout here, we'll do it when reapplying the swap layout - // below - self.tiled_panes - .insert_pane_without_relayout(pid, Box::new(new_terminal)); - } else { - self.tiled_panes.insert_pane(pid, Box::new(new_terminal)); - } - self.should_clear_display_before_rendering = true; - if let Some(client_id) = client_id { - self.tiled_panes.focus_pane(pid, client_id); - } - } - } - if should_auto_layout { - // only do this if we're already in this layout, otherwise it might be - // confusing and not what the user intends - self.swap_layouts.set_is_tiled_damaged(); // we do this so that we won't skip to the - // next layout - self.next_swap_layout(client_id, true)?; - } - } - Ok(()) - } - pub fn new_plugin_pane( - &mut self, - pid: PaneId, - initial_pane_title: String, - should_float: Option, - run_plugin: Run, - client_id: Option, - ) -> Result<()> { - let err_context = || format!("failed to create new pane with id {pid:?}"); - - match should_float { - Some(true) => self.show_floating_panes(), - Some(false) => self.hide_floating_panes(), - None => {}, + Box::new(TerminalPane::new( + term_pid, + PaneGeom::default(), // this will be filled out later + self.style, + next_terminal_position, + String::new(), + self.link_handler.clone(), + self.character_cell_size.clone(), + self.sixel_image_store.clone(), + self.terminal_emulator_colors.clone(), + self.terminal_emulator_color_codes.clone(), + initial_pane_title, + None, + )) as Box + }, + PaneId::Plugin(plugin_pid) => { + Box::new(PluginPane::new( + plugin_pid, + PaneGeom::default(), // this will be filled out later + self.senders + .to_plugin + .as_ref() + .with_context(err_context)? + .clone(), + initial_pane_title.unwrap_or("".to_owned()), + String::new(), + self.sixel_image_store.clone(), + self.terminal_emulator_colors.clone(), + self.terminal_emulator_color_codes.clone(), + self.link_handler.clone(), + self.character_cell_size.clone(), + self.connected_clients.borrow().iter().copied().collect(), + self.style, + run_plugin, + )) as Box + }, }; if self.floating_panes.panes_are_visible() { - if let Some(new_pane_geom) = self.floating_panes.find_room_for_new_pane() { - if let PaneId::Plugin(plugin_pid) = pid { - let mut new_pane = PluginPane::new( - plugin_pid, - new_pane_geom, - self.senders - .to_plugin - .as_ref() - .with_context(err_context)? - .clone(), - initial_pane_title, - String::new(), - self.sixel_image_store.clone(), - self.terminal_emulator_colors.clone(), - self.terminal_emulator_color_codes.clone(), - self.link_handler.clone(), - self.character_cell_size.clone(), - self.connected_clients.borrow().iter().copied().collect(), - self.style, - Some(run_plugin), - ); - new_pane.set_active_at(Instant::now()); - new_pane.set_content_offset(Offset::frame(1)); // floating panes always have a frame - resize_pty!( - new_pane, - self.os_api, - self.senders, - self.character_cell_size - ) - .with_context(err_context)?; - self.floating_panes.add_pane(pid, Box::new(new_pane)); - self.floating_panes.focus_pane_for_all_clients(pid); - } - if self.auto_layout && !self.swap_layouts.is_floating_damaged() { - // only do this if we're already in this layout, otherwise it might be - // confusing and not what the user intends - self.swap_layouts.set_is_floating_damaged(); // we do this so that we won't skip to the - // next layout - self.next_swap_layout(client_id, true)?; - } - } + self.add_floating_pane(new_pane, pid, client_id) } else { - if self.tiled_panes.fullscreen_is_active() { - self.tiled_panes.unset_fullscreen(); - } - let should_auto_layout = self.auto_layout && !self.swap_layouts.is_tiled_damaged(); - if self.tiled_panes.has_room_for_new_pane() { - if let PaneId::Plugin(plugin_pid) = pid { - let mut new_pane = PluginPane::new( - plugin_pid, - PaneGeom::default(), // the initial size will be set later - self.senders - .to_plugin - .as_ref() - .with_context(err_context)? - .clone(), - initial_pane_title, - String::new(), - self.sixel_image_store.clone(), - self.terminal_emulator_colors.clone(), - self.terminal_emulator_color_codes.clone(), - self.link_handler.clone(), - self.character_cell_size.clone(), - self.connected_clients.borrow().iter().copied().collect(), - self.style, - Some(run_plugin), - ); - new_pane.set_active_at(Instant::now()); - if should_auto_layout { - // no need to relayout here, we'll do it when reapplying the swap layout - // below - self.tiled_panes - .insert_pane_without_relayout(pid, Box::new(new_pane)); - } else { - self.tiled_panes.insert_pane(pid, Box::new(new_pane)); - } - self.should_clear_display_before_rendering = true; - if let Some(client_id) = client_id { - self.tiled_panes.focus_pane(pid, client_id); - } - } - } - if should_auto_layout { - // only do this if we're already in this layout, otherwise it might be - // confusing and not what the user intends - self.swap_layouts.set_is_tiled_damaged(); // we do this so that we won't skip to the - // next layout - self.next_swap_layout(client_id, true)?; - } + self.add_tiled_pane(new_pane, pid, client_id) } - Ok(()) } pub fn suppress_active_pane(&mut self, pid: PaneId, client_id: ClientId) -> Result<()> { // this method creates a new pane from pid and replaces it with the active pane @@ -3426,6 +3247,112 @@ impl Tab { self.floating_panes.toggle_show_panes(false); self.tiled_panes.focus_all_panes(); } + + pub fn find_plugin(&self, run_plugin: &RunPlugin) -> Option { + self.tiled_panes + .get_plugin_pane_id(run_plugin) + .or_else(|| self.floating_panes.get_plugin_pane_id(run_plugin)) + .or_else(|| { + let run = Some(Run::Plugin(run_plugin.clone())); + self.suppressed_panes + .iter() + .find(|(_id, s_p)| s_p.invoked_with() == &run) + .map(|(id, _)| *id) + }) + } + + pub fn focus_pane_with_id( + &mut self, + pane_id: PaneId, + should_float: bool, + client_id: ClientId, + ) -> Result<()> { + self.tiled_panes + .focus_pane_if_exists(pane_id, client_id) + .or_else(|_| { + let focused_floating_pane = + self.floating_panes.focus_pane_if_exists(pane_id, client_id); + if focused_floating_pane.is_ok() { + self.show_floating_panes() + }; + focused_floating_pane + }) + .or_else(|_| match self.suppressed_panes.remove(&pane_id) { + Some(pane) => { + if should_float { + self.show_floating_panes(); + self.add_floating_pane(pane, pane_id, Some(client_id)) + } else { + self.hide_floating_panes(); + self.add_tiled_pane(pane, pane_id, Some(client_id)) + } + }, + None => Ok(()), + }) + } + pub fn suppress_pane(&mut self, pane_id: PaneId, client_id: ClientId) { + if let Some(pane) = self.close_pane(pane_id, true, Some(client_id)) { + self.suppressed_panes.insert(pane_id, pane); + } + } + fn add_floating_pane( + &mut self, + mut pane: Box, + pane_id: PaneId, + client_id: Option, + ) -> Result<()> { + let err_context = || format!("failed to add floating pane"); + if let Some(new_pane_geom) = self.floating_panes.find_room_for_new_pane() { + pane.set_active_at(Instant::now()); + pane.set_geom(new_pane_geom); + pane.set_content_offset(Offset::frame(1)); // floating panes always have a frame + resize_pty!(pane, self.os_api, self.senders, self.character_cell_size) + .with_context(err_context)?; + self.floating_panes.add_pane(pane_id, pane); + self.floating_panes.focus_pane_for_all_clients(pane_id); + } + if self.auto_layout && !self.swap_layouts.is_floating_damaged() { + // only do this if we're already in this layout, otherwise it might be + // confusing and not what the user intends + self.swap_layouts.set_is_floating_damaged(); // we do this so that we won't skip to the + // next layout + self.next_swap_layout(client_id, true)?; + } + Ok(()) + } + fn add_tiled_pane( + &mut self, + mut pane: Box, + pane_id: PaneId, + client_id: Option, + ) -> Result<()> { + if self.tiled_panes.fullscreen_is_active() { + self.tiled_panes.unset_fullscreen(); + } + let should_auto_layout = self.auto_layout && !self.swap_layouts.is_tiled_damaged(); + if self.tiled_panes.has_room_for_new_pane() { + pane.set_active_at(Instant::now()); + if should_auto_layout { + // no need to relayout here, we'll do it when reapplying the swap layout + // below + self.tiled_panes.insert_pane_without_relayout(pane_id, pane); + } else { + self.tiled_panes.insert_pane(pane_id, pane); + } + self.should_clear_display_before_rendering = true; + if let Some(client_id) = client_id { + self.tiled_panes.focus_pane(pane_id, client_id); + } + } + if should_auto_layout { + // only do this if we're already in this layout, otherwise it might be + // confusing and not what the user intends + self.swap_layouts.set_is_tiled_damaged(); // we do this so that we won't skip to the + // next layout + self.next_swap_layout(client_id, true)?; + } + Ok(()) + } } #[cfg(test)] diff --git a/zellij-server/src/tab/unit/tab_integration_tests.rs b/zellij-server/src/tab/unit/tab_integration_tests.rs index 48d841a5d8..7cd32c0f5e 100644 --- a/zellij-server/src/tab/unit/tab_integration_tests.rs +++ b/zellij-server/src/tab/unit/tab_integration_tests.rs @@ -696,7 +696,7 @@ fn dump_screen() { ..Default::default() }); let new_pane_id = PaneId::Terminal(2); - tab.new_pane(new_pane_id, None, None, Some(client_id)) + tab.new_pane(new_pane_id, None, None, None, Some(client_id)) .unwrap(); tab.handle_pty_bytes(2, Vec::from("scratch".as_bytes())) .unwrap(); @@ -724,7 +724,7 @@ fn clear_screen() { ..Default::default() }); let new_pane_id = PaneId::Terminal(2); - tab.new_pane(new_pane_id, None, None, Some(client_id)) + tab.new_pane(new_pane_id, None, None, None, Some(client_id)) .unwrap(); tab.handle_pty_bytes(2, Vec::from("scratch".as_bytes())) .unwrap(); @@ -750,7 +750,7 @@ fn new_floating_pane() { let new_pane_id = PaneId::Terminal(2); let mut output = Output::default(); tab.toggle_floating_panes(Some(client_id), None).unwrap(); - tab.new_pane(new_pane_id, None, None, Some(client_id)) + tab.new_pane(new_pane_id, None, None, None, Some(client_id)) .unwrap(); tab.handle_pty_bytes( 2, @@ -778,7 +778,7 @@ fn floating_panes_persist_across_toggles() { let new_pane_id = PaneId::Terminal(2); let mut output = Output::default(); tab.toggle_floating_panes(Some(client_id), None).unwrap(); - tab.new_pane(new_pane_id, None, None, Some(client_id)) + tab.new_pane(new_pane_id, None, None, None, Some(client_id)) .unwrap(); tab.toggle_floating_panes(Some(client_id), None).unwrap(); // here we send bytes to the pane when it's not visible to make sure they're still handled and @@ -810,7 +810,7 @@ fn toggle_floating_panes_off() { let new_pane_id = PaneId::Terminal(2); let mut output = Output::default(); tab.toggle_floating_panes(Some(client_id), None).unwrap(); - tab.new_pane(new_pane_id, None, None, Some(client_id)) + tab.new_pane(new_pane_id, None, None, None, Some(client_id)) .unwrap(); tab.handle_pty_bytes( 2, @@ -839,7 +839,7 @@ fn toggle_floating_panes_on() { let new_pane_id = PaneId::Terminal(2); let mut output = Output::default(); tab.toggle_floating_panes(Some(client_id), None).unwrap(); - tab.new_pane(new_pane_id, None, None, Some(client_id)) + tab.new_pane(new_pane_id, None, None, None, Some(client_id)) .unwrap(); tab.handle_pty_bytes( 2, @@ -873,15 +873,15 @@ fn five_new_floating_panes() { let new_pane_id_5 = PaneId::Terminal(6); let mut output = Output::default(); tab.toggle_floating_panes(Some(client_id), None).unwrap(); - tab.new_pane(new_pane_id_1, None, None, Some(client_id)) + tab.new_pane(new_pane_id_1, None, None, None, Some(client_id)) .unwrap(); - tab.new_pane(new_pane_id_2, None, None, Some(client_id)) + tab.new_pane(new_pane_id_2, None, None, None, Some(client_id)) .unwrap(); - tab.new_pane(new_pane_id_3, None, None, Some(client_id)) + tab.new_pane(new_pane_id_3, None, None, None, Some(client_id)) .unwrap(); - tab.new_pane(new_pane_id_4, None, None, Some(client_id)) + tab.new_pane(new_pane_id_4, None, None, None, Some(client_id)) .unwrap(); - tab.new_pane(new_pane_id_5, None, None, Some(client_id)) + tab.new_pane(new_pane_id_5, None, None, None, Some(client_id)) .unwrap(); tab.handle_pty_bytes( 2, @@ -917,7 +917,7 @@ fn increase_floating_pane_size() { let new_pane_id_1 = PaneId::Terminal(2); let mut output = Output::default(); tab.toggle_floating_panes(Some(client_id), None).unwrap(); - tab.new_pane(new_pane_id_1, None, None, Some(client_id)) + tab.new_pane(new_pane_id_1, None, None, None, Some(client_id)) .unwrap(); tab.handle_pty_bytes( 2, @@ -947,7 +947,7 @@ fn decrease_floating_pane_size() { let new_pane_id_1 = PaneId::Terminal(2); let mut output = Output::default(); tab.toggle_floating_panes(Some(client_id), None).unwrap(); - tab.new_pane(new_pane_id_1, None, None, Some(client_id)) + tab.new_pane(new_pane_id_1, None, None, None, Some(client_id)) .unwrap(); tab.handle_pty_bytes( 2, @@ -977,7 +977,7 @@ fn resize_floating_pane_left() { let new_pane_id_1 = PaneId::Terminal(2); let mut output = Output::default(); tab.toggle_floating_panes(Some(client_id), None).unwrap(); - tab.new_pane(new_pane_id_1, None, None, Some(client_id)) + tab.new_pane(new_pane_id_1, None, None, None, Some(client_id)) .unwrap(); tab.handle_pty_bytes( 2, @@ -1010,7 +1010,7 @@ fn resize_floating_pane_right() { let new_pane_id_1 = PaneId::Terminal(2); let mut output = Output::default(); tab.toggle_floating_panes(Some(client_id), None).unwrap(); - tab.new_pane(new_pane_id_1, None, None, Some(client_id)) + tab.new_pane(new_pane_id_1, None, None, None, Some(client_id)) .unwrap(); tab.handle_pty_bytes( 2, @@ -1043,7 +1043,7 @@ fn resize_floating_pane_up() { let new_pane_id_1 = PaneId::Terminal(2); let mut output = Output::default(); tab.toggle_floating_panes(Some(client_id), None).unwrap(); - tab.new_pane(new_pane_id_1, None, None, Some(client_id)) + tab.new_pane(new_pane_id_1, None, None, None, Some(client_id)) .unwrap(); tab.handle_pty_bytes( 2, @@ -1076,7 +1076,7 @@ fn resize_floating_pane_down() { let new_pane_id_1 = PaneId::Terminal(2); let mut output = Output::default(); tab.toggle_floating_panes(Some(client_id), None).unwrap(); - tab.new_pane(new_pane_id_1, None, None, Some(client_id)) + tab.new_pane(new_pane_id_1, None, None, None, Some(client_id)) .unwrap(); tab.handle_pty_bytes( 2, @@ -1113,15 +1113,15 @@ fn move_floating_pane_focus_left() { let new_pane_id_5 = PaneId::Terminal(6); let mut output = Output::default(); tab.toggle_floating_panes(Some(client_id), None).unwrap(); - tab.new_pane(new_pane_id_1, None, None, Some(client_id)) + tab.new_pane(new_pane_id_1, None, None, None, Some(client_id)) .unwrap(); - tab.new_pane(new_pane_id_2, None, None, Some(client_id)) + tab.new_pane(new_pane_id_2, None, None, None, Some(client_id)) .unwrap(); - tab.new_pane(new_pane_id_3, None, None, Some(client_id)) + tab.new_pane(new_pane_id_3, None, None, None, Some(client_id)) .unwrap(); - tab.new_pane(new_pane_id_4, None, None, Some(client_id)) + tab.new_pane(new_pane_id_4, None, None, None, Some(client_id)) .unwrap(); - tab.new_pane(new_pane_id_5, None, None, Some(client_id)) + tab.new_pane(new_pane_id_5, None, None, None, Some(client_id)) .unwrap(); tab.handle_pty_bytes( 2, @@ -1168,15 +1168,15 @@ fn move_floating_pane_focus_right() { let new_pane_id_5 = PaneId::Terminal(6); let mut output = Output::default(); tab.toggle_floating_panes(Some(client_id), None).unwrap(); - tab.new_pane(new_pane_id_1, None, None, Some(client_id)) + tab.new_pane(new_pane_id_1, None, None, None, Some(client_id)) .unwrap(); - tab.new_pane(new_pane_id_2, None, None, Some(client_id)) + tab.new_pane(new_pane_id_2, None, None, None, Some(client_id)) .unwrap(); - tab.new_pane(new_pane_id_3, None, None, Some(client_id)) + tab.new_pane(new_pane_id_3, None, None, None, Some(client_id)) .unwrap(); - tab.new_pane(new_pane_id_4, None, None, Some(client_id)) + tab.new_pane(new_pane_id_4, None, None, None, Some(client_id)) .unwrap(); - tab.new_pane(new_pane_id_5, None, None, Some(client_id)) + tab.new_pane(new_pane_id_5, None, None, None, Some(client_id)) .unwrap(); tab.handle_pty_bytes( 2, @@ -1224,15 +1224,15 @@ fn move_floating_pane_focus_up() { let new_pane_id_5 = PaneId::Terminal(6); let mut output = Output::default(); tab.toggle_floating_panes(Some(client_id), None).unwrap(); - tab.new_pane(new_pane_id_1, None, None, Some(client_id)) + tab.new_pane(new_pane_id_1, None, None, None, Some(client_id)) .unwrap(); - tab.new_pane(new_pane_id_2, None, None, Some(client_id)) + tab.new_pane(new_pane_id_2, None, None, None, Some(client_id)) .unwrap(); - tab.new_pane(new_pane_id_3, None, None, Some(client_id)) + tab.new_pane(new_pane_id_3, None, None, None, Some(client_id)) .unwrap(); - tab.new_pane(new_pane_id_4, None, None, Some(client_id)) + tab.new_pane(new_pane_id_4, None, None, None, Some(client_id)) .unwrap(); - tab.new_pane(new_pane_id_5, None, None, Some(client_id)) + tab.new_pane(new_pane_id_5, None, None, None, Some(client_id)) .unwrap(); tab.handle_pty_bytes( 2, @@ -1279,15 +1279,15 @@ fn move_floating_pane_focus_down() { let new_pane_id_5 = PaneId::Terminal(6); let mut output = Output::default(); tab.toggle_floating_panes(Some(client_id), None).unwrap(); - tab.new_pane(new_pane_id_1, None, None, Some(client_id)) + tab.new_pane(new_pane_id_1, None, None, None, Some(client_id)) .unwrap(); - tab.new_pane(new_pane_id_2, None, None, Some(client_id)) + tab.new_pane(new_pane_id_2, None, None, None, Some(client_id)) .unwrap(); - tab.new_pane(new_pane_id_3, None, None, Some(client_id)) + tab.new_pane(new_pane_id_3, None, None, None, Some(client_id)) .unwrap(); - tab.new_pane(new_pane_id_4, None, None, Some(client_id)) + tab.new_pane(new_pane_id_4, None, None, None, Some(client_id)) .unwrap(); - tab.new_pane(new_pane_id_5, None, None, Some(client_id)) + tab.new_pane(new_pane_id_5, None, None, None, Some(client_id)) .unwrap(); tab.handle_pty_bytes( 2, @@ -1335,15 +1335,15 @@ fn move_floating_pane_focus_with_mouse() { let new_pane_id_5 = PaneId::Terminal(6); let mut output = Output::default(); tab.toggle_floating_panes(Some(client_id), None).unwrap(); - tab.new_pane(new_pane_id_1, None, None, Some(client_id)) + tab.new_pane(new_pane_id_1, None, None, None, Some(client_id)) .unwrap(); - tab.new_pane(new_pane_id_2, None, None, Some(client_id)) + tab.new_pane(new_pane_id_2, None, None, None, Some(client_id)) .unwrap(); - tab.new_pane(new_pane_id_3, None, None, Some(client_id)) + tab.new_pane(new_pane_id_3, None, None, None, Some(client_id)) .unwrap(); - tab.new_pane(new_pane_id_4, None, None, Some(client_id)) + tab.new_pane(new_pane_id_4, None, None, None, Some(client_id)) .unwrap(); - tab.new_pane(new_pane_id_5, None, None, Some(client_id)) + tab.new_pane(new_pane_id_5, None, None, None, Some(client_id)) .unwrap(); tab.handle_pty_bytes( 2, @@ -1393,15 +1393,15 @@ fn move_pane_focus_with_mouse_to_non_floating_pane() { let new_pane_id_5 = PaneId::Terminal(6); let mut output = Output::default(); tab.toggle_floating_panes(Some(client_id), None).unwrap(); - tab.new_pane(new_pane_id_1, None, None, Some(client_id)) + tab.new_pane(new_pane_id_1, None, None, None, Some(client_id)) .unwrap(); - tab.new_pane(new_pane_id_2, None, None, Some(client_id)) + tab.new_pane(new_pane_id_2, None, None, None, Some(client_id)) .unwrap(); - tab.new_pane(new_pane_id_3, None, None, Some(client_id)) + tab.new_pane(new_pane_id_3, None, None, None, Some(client_id)) .unwrap(); - tab.new_pane(new_pane_id_4, None, None, Some(client_id)) + tab.new_pane(new_pane_id_4, None, None, None, Some(client_id)) .unwrap(); - tab.new_pane(new_pane_id_5, None, None, Some(client_id)) + tab.new_pane(new_pane_id_5, None, None, None, Some(client_id)) .unwrap(); tab.handle_pty_bytes( 2, @@ -1451,15 +1451,15 @@ fn drag_pane_with_mouse() { let new_pane_id_5 = PaneId::Terminal(6); let mut output = Output::default(); tab.toggle_floating_panes(Some(client_id), None).unwrap(); - tab.new_pane(new_pane_id_1, None, None, Some(client_id)) + tab.new_pane(new_pane_id_1, None, None, None, Some(client_id)) .unwrap(); - tab.new_pane(new_pane_id_2, None, None, Some(client_id)) + tab.new_pane(new_pane_id_2, None, None, None, Some(client_id)) .unwrap(); - tab.new_pane(new_pane_id_3, None, None, Some(client_id)) + tab.new_pane(new_pane_id_3, None, None, None, Some(client_id)) .unwrap(); - tab.new_pane(new_pane_id_4, None, None, Some(client_id)) + tab.new_pane(new_pane_id_4, None, None, None, Some(client_id)) .unwrap(); - tab.new_pane(new_pane_id_5, None, None, Some(client_id)) + tab.new_pane(new_pane_id_5, None, None, None, Some(client_id)) .unwrap(); tab.handle_pty_bytes( 2, @@ -1509,15 +1509,15 @@ fn mark_text_inside_floating_pane() { let new_pane_id_5 = PaneId::Terminal(6); let mut output = Output::default(); tab.toggle_floating_panes(Some(client_id), None).unwrap(); - tab.new_pane(new_pane_id_1, None, None, Some(client_id)) + tab.new_pane(new_pane_id_1, None, None, None, Some(client_id)) .unwrap(); - tab.new_pane(new_pane_id_2, None, None, Some(client_id)) + tab.new_pane(new_pane_id_2, None, None, None, Some(client_id)) .unwrap(); - tab.new_pane(new_pane_id_3, None, None, Some(client_id)) + tab.new_pane(new_pane_id_3, None, None, None, Some(client_id)) .unwrap(); - tab.new_pane(new_pane_id_4, None, None, Some(client_id)) + tab.new_pane(new_pane_id_4, None, None, None, Some(client_id)) .unwrap(); - tab.new_pane(new_pane_id_5, None, None, Some(client_id)) + tab.new_pane(new_pane_id_5, None, None, None, Some(client_id)) .unwrap(); tab.handle_pty_bytes( 2, @@ -1575,15 +1575,15 @@ fn resize_tab_with_floating_panes() { let new_pane_id_5 = PaneId::Terminal(6); let mut output = Output::default(); tab.toggle_floating_panes(Some(client_id), None).unwrap(); - tab.new_pane(new_pane_id_1, None, None, Some(client_id)) + tab.new_pane(new_pane_id_1, None, None, None, Some(client_id)) .unwrap(); - tab.new_pane(new_pane_id_2, None, None, Some(client_id)) + tab.new_pane(new_pane_id_2, None, None, None, Some(client_id)) .unwrap(); - tab.new_pane(new_pane_id_3, None, None, Some(client_id)) + tab.new_pane(new_pane_id_3, None, None, None, Some(client_id)) .unwrap(); - tab.new_pane(new_pane_id_4, None, None, Some(client_id)) + tab.new_pane(new_pane_id_4, None, None, None, Some(client_id)) .unwrap(); - tab.new_pane(new_pane_id_5, None, None, Some(client_id)) + tab.new_pane(new_pane_id_5, None, None, None, Some(client_id)) .unwrap(); tab.handle_pty_bytes( 2, @@ -1629,15 +1629,15 @@ fn shrink_whole_tab_with_floating_panes_horizontally_and_vertically() { let new_pane_id_5 = PaneId::Terminal(6); let mut output = Output::default(); tab.toggle_floating_panes(Some(client_id), None).unwrap(); - tab.new_pane(new_pane_id_1, None, None, Some(client_id)) + tab.new_pane(new_pane_id_1, None, None, None, Some(client_id)) .unwrap(); - tab.new_pane(new_pane_id_2, None, None, Some(client_id)) + tab.new_pane(new_pane_id_2, None, None, None, Some(client_id)) .unwrap(); - tab.new_pane(new_pane_id_3, None, None, Some(client_id)) + tab.new_pane(new_pane_id_3, None, None, None, Some(client_id)) .unwrap(); - tab.new_pane(new_pane_id_4, None, None, Some(client_id)) + tab.new_pane(new_pane_id_4, None, None, None, Some(client_id)) .unwrap(); - tab.new_pane(new_pane_id_5, None, None, Some(client_id)) + tab.new_pane(new_pane_id_5, None, None, None, Some(client_id)) .unwrap(); tab.handle_pty_bytes( 2, @@ -1679,15 +1679,15 @@ fn shrink_whole_tab_with_floating_panes_horizontally_and_vertically_and_expand_b let new_pane_id_5 = PaneId::Terminal(6); let mut output = Output::default(); tab.toggle_floating_panes(Some(client_id), None).unwrap(); - tab.new_pane(new_pane_id_1, None, None, Some(client_id)) + tab.new_pane(new_pane_id_1, None, None, None, Some(client_id)) .unwrap(); - tab.new_pane(new_pane_id_2, None, None, Some(client_id)) + tab.new_pane(new_pane_id_2, None, None, None, Some(client_id)) .unwrap(); - tab.new_pane(new_pane_id_3, None, None, Some(client_id)) + tab.new_pane(new_pane_id_3, None, None, None, Some(client_id)) .unwrap(); - tab.new_pane(new_pane_id_4, None, None, Some(client_id)) + tab.new_pane(new_pane_id_4, None, None, None, Some(client_id)) .unwrap(); - tab.new_pane(new_pane_id_5, None, None, Some(client_id)) + tab.new_pane(new_pane_id_5, None, None, None, Some(client_id)) .unwrap(); tab.handle_pty_bytes( 2, @@ -1730,7 +1730,7 @@ fn embed_floating_pane() { let new_pane_id = PaneId::Terminal(2); let mut output = Output::default(); tab.toggle_floating_panes(Some(client_id), None).unwrap(); - tab.new_pane(new_pane_id, None, None, Some(client_id)) + tab.new_pane(new_pane_id, None, None, None, Some(client_id)) .unwrap(); tab.handle_pty_bytes( 2, @@ -1758,7 +1758,7 @@ fn float_embedded_pane() { let mut tab = create_new_tab(size, ModeInfo::default()); let new_pane_id = PaneId::Terminal(2); let mut output = Output::default(); - tab.new_pane(new_pane_id, None, None, Some(client_id)) + tab.new_pane(new_pane_id, None, None, None, Some(client_id)) .unwrap(); tab.handle_pty_bytes( 2, @@ -1788,7 +1788,7 @@ fn embed_floating_pane_without_pane_frames() { let mut output = Output::default(); tab.set_pane_frames(false); tab.toggle_floating_panes(Some(client_id), None).unwrap(); - tab.new_pane(new_pane_id, None, None, Some(client_id)) + tab.new_pane(new_pane_id, None, None, None, Some(client_id)) .unwrap(); tab.handle_pty_bytes( 2, @@ -1817,7 +1817,7 @@ fn float_embedded_pane_without_pane_frames() { let new_pane_id = PaneId::Terminal(2); let mut output = Output::default(); tab.set_pane_frames(false); - tab.new_pane(new_pane_id, None, None, Some(client_id)) + tab.new_pane(new_pane_id, None, None, None, Some(client_id)) .unwrap(); tab.handle_pty_bytes( 2, @@ -1920,7 +1920,7 @@ fn rename_floating_pane() { let mut tab = create_new_tab(size, ModeInfo::default()); let new_pane_id = PaneId::Terminal(2); let mut output = Output::default(); - tab.new_pane(new_pane_id, None, None, Some(client_id)) + tab.new_pane(new_pane_id, None, None, None, Some(client_id)) .unwrap(); tab.handle_pty_bytes( 2, @@ -2006,7 +2006,7 @@ fn move_floating_pane_with_sixel_image() { let mut output = Output::new(sixel_image_store.clone(), character_cell_size); tab.toggle_floating_panes(Some(client_id), None).unwrap(); - tab.new_pane(new_pane_id, None, None, Some(client_id)) + tab.new_pane(new_pane_id, None, None, None, Some(client_id)) .unwrap(); let fixture = read_fixture("sixel-image-500px.six"); tab.handle_pty_bytes(2, fixture).unwrap(); @@ -2044,7 +2044,7 @@ fn floating_pane_above_sixel_image() { let mut output = Output::new(sixel_image_store.clone(), character_cell_size); tab.toggle_floating_panes(Some(client_id), None).unwrap(); - tab.new_pane(new_pane_id, None, None, Some(client_id)) + tab.new_pane(new_pane_id, None, None, None, Some(client_id)) .unwrap(); let fixture = read_fixture("sixel-image-500px.six"); tab.handle_pty_bytes(1, fixture).unwrap(); @@ -2101,7 +2101,7 @@ fn suppress_floating_pane() { let mut output = Output::default(); tab.toggle_floating_panes(Some(client_id), None).unwrap(); - tab.new_pane(new_pane_id, None, None, Some(client_id)) + tab.new_pane(new_pane_id, None, None, None, Some(client_id)) .unwrap(); tab.suppress_active_pane(editor_pane_id, client_id).unwrap(); tab.handle_pty_bytes(3, Vec::from("\n\n\nI am an editor pane".as_bytes())) @@ -2155,7 +2155,7 @@ fn close_suppressing_floating_pane() { let mut output = Output::default(); tab.toggle_floating_panes(Some(client_id), None).unwrap(); - tab.new_pane(new_pane_id, None, None, Some(client_id)) + tab.new_pane(new_pane_id, None, None, None, Some(client_id)) .unwrap(); tab.suppress_active_pane(editor_pane_id, client_id).unwrap(); tab.handle_pty_bytes(3, Vec::from("\n\n\nI am an editor pane".as_bytes())) @@ -2213,7 +2213,7 @@ fn suppress_floating_pane_embed_it_and_close_it() { let mut output = Output::default(); tab.toggle_floating_panes(Some(client_id), None).unwrap(); - tab.new_pane(new_pane_id, None, None, Some(client_id)) + tab.new_pane(new_pane_id, None, None, None, Some(client_id)) .unwrap(); tab.suppress_active_pane(editor_pane_id, client_id).unwrap(); tab.handle_pty_bytes(3, Vec::from("\n\n\nI am an editor pane".as_bytes())) @@ -2273,7 +2273,7 @@ fn resize_whole_tab_while_floting_pane_is_suppressed() { let mut output = Output::default(); tab.toggle_floating_panes(Some(client_id), None).unwrap(); - tab.new_pane(new_pane_id, None, None, Some(client_id)) + tab.new_pane(new_pane_id, None, None, None, Some(client_id)) .unwrap(); tab.suppress_active_pane(editor_pane_id, client_id).unwrap(); tab.handle_pty_bytes(3, Vec::from("\n\n\nI am an editor pane".as_bytes())) @@ -2374,7 +2374,7 @@ fn enter_search_floating_pane() { let new_pane_id = PaneId::Terminal(2); let mut output = Output::default(); tab.toggle_floating_panes(Some(client_id), None).unwrap(); - tab.new_pane(new_pane_id, None, None, Some(client_id)) + tab.new_pane(new_pane_id, None, None, None, Some(client_id)) .unwrap(); let pane_content = read_fixture("grid_copy"); @@ -2879,7 +2879,7 @@ fn move_pane_focus_sends_tty_csi_event() { }); let mut tab = create_new_tab_with_os_api(size, ModeInfo::default(), &os_api); let new_pane_id_1 = PaneId::Terminal(2); - tab.new_pane(new_pane_id_1, None, None, Some(client_id)) + tab.new_pane(new_pane_id_1, None, None, None, Some(client_id)) .unwrap(); tab.handle_pty_bytes( 1, @@ -2914,9 +2914,9 @@ fn move_floating_pane_focus_sends_tty_csi_event() { let new_pane_id_2 = PaneId::Terminal(3); tab.toggle_floating_panes(Some(client_id), None).unwrap(); - tab.new_pane(new_pane_id_1, None, None, Some(client_id)) + tab.new_pane(new_pane_id_1, None, None, None, Some(client_id)) .unwrap(); - tab.new_pane(new_pane_id_2, None, None, Some(client_id)) + tab.new_pane(new_pane_id_2, None, None, None, Some(client_id)) .unwrap(); tab.handle_pty_bytes( 1, @@ -2957,9 +2957,9 @@ fn toggle_floating_panes_on_sends_tty_csi_event() { let new_pane_id_2 = PaneId::Terminal(3); tab.toggle_floating_panes(Some(client_id), None).unwrap(); - tab.new_pane(new_pane_id_1, None, None, Some(client_id)) + tab.new_pane(new_pane_id_1, None, None, None, Some(client_id)) .unwrap(); - tab.new_pane(new_pane_id_2, None, None, Some(client_id)) + tab.new_pane(new_pane_id_2, None, None, None, Some(client_id)) .unwrap(); tab.toggle_floating_panes(Some(client_id), None).unwrap(); tab.handle_pty_bytes( @@ -3001,9 +3001,9 @@ fn toggle_floating_panes_off_sends_tty_csi_event() { let new_pane_id_2 = PaneId::Terminal(3); tab.toggle_floating_panes(Some(client_id), None).unwrap(); - tab.new_pane(new_pane_id_1, None, None, Some(client_id)) + tab.new_pane(new_pane_id_1, None, None, None, Some(client_id)) .unwrap(); - tab.new_pane(new_pane_id_2, None, None, Some(client_id)) + tab.new_pane(new_pane_id_2, None, None, None, Some(client_id)) .unwrap(); tab.handle_pty_bytes( 1, @@ -3063,7 +3063,7 @@ fn can_swap_tiled_layout_at_runtime() { ); let new_pane_id_1 = PaneId::Terminal(2); - tab.new_pane(new_pane_id_1, None, None, Some(client_id)) + tab.new_pane(new_pane_id_1, None, None, None, Some(client_id)) .unwrap(); tab.next_swap_layout(Some(client_id), false).unwrap(); tab.render(&mut output, None).unwrap(); @@ -3118,9 +3118,9 @@ fn can_swap_floating_layout_at_runtime() { let new_pane_id_2 = PaneId::Terminal(3); tab.toggle_floating_panes(Some(client_id), None).unwrap(); - tab.new_pane(new_pane_id_1, None, None, Some(client_id)) + tab.new_pane(new_pane_id_1, None, None, None, Some(client_id)) .unwrap(); - tab.new_pane(new_pane_id_2, None, None, Some(client_id)) + tab.new_pane(new_pane_id_2, None, None, None, Some(client_id)) .unwrap(); tab.next_swap_layout(Some(client_id), false).unwrap(); tab.render(&mut output, None).unwrap(); @@ -3171,7 +3171,7 @@ fn swapping_layouts_after_resize_snaps_to_current_layout() { ); let new_pane_id_1 = PaneId::Terminal(2); - tab.new_pane(new_pane_id_1, None, None, Some(client_id)) + tab.new_pane(new_pane_id_1, None, None, None, Some(client_id)) .unwrap(); tab.next_swap_layout(Some(client_id), false).unwrap(); tab.resize(client_id, ResizeStrategy::new(Resize::Increase, None)) @@ -3221,11 +3221,11 @@ fn swap_tiled_layout_with_stacked_children() { let new_pane_id_2 = PaneId::Terminal(3); let new_pane_id_3 = PaneId::Terminal(4); - tab.new_pane(new_pane_id_1, None, None, Some(client_id)) + tab.new_pane(new_pane_id_1, None, None, None, Some(client_id)) .unwrap(); - tab.new_pane(new_pane_id_2, None, None, Some(client_id)) + tab.new_pane(new_pane_id_2, None, None, None, Some(client_id)) .unwrap(); - tab.new_pane(new_pane_id_3, None, None, Some(client_id)) + tab.new_pane(new_pane_id_3, None, None, None, Some(client_id)) .unwrap(); tab.render(&mut output, None).unwrap(); let snapshot = take_snapshot( @@ -3268,11 +3268,11 @@ fn swap_tiled_layout_with_only_stacked_children() { let new_pane_id_2 = PaneId::Terminal(3); let new_pane_id_3 = PaneId::Terminal(4); - tab.new_pane(new_pane_id_1, None, None, Some(client_id)) + tab.new_pane(new_pane_id_1, None, None, None, Some(client_id)) .unwrap(); - tab.new_pane(new_pane_id_2, None, None, Some(client_id)) + tab.new_pane(new_pane_id_2, None, None, None, Some(client_id)) .unwrap(); - tab.new_pane(new_pane_id_3, None, None, Some(client_id)) + tab.new_pane(new_pane_id_3, None, None, None, Some(client_id)) .unwrap(); tab.render(&mut output, None).unwrap(); let snapshot = take_snapshot( @@ -3318,11 +3318,11 @@ fn swap_tiled_layout_with_stacked_children_and_no_pane_frames() { let new_pane_id_2 = PaneId::Terminal(3); let new_pane_id_3 = PaneId::Terminal(4); - tab.new_pane(new_pane_id_1, None, None, Some(client_id)) + tab.new_pane(new_pane_id_1, None, None, None, Some(client_id)) .unwrap(); - tab.new_pane(new_pane_id_2, None, None, Some(client_id)) + tab.new_pane(new_pane_id_2, None, None, None, Some(client_id)) .unwrap(); - tab.new_pane(new_pane_id_3, None, None, Some(client_id)) + tab.new_pane(new_pane_id_3, None, None, None, Some(client_id)) .unwrap(); tab.render(&mut output, None).unwrap(); let snapshot = take_snapshot( @@ -3368,11 +3368,11 @@ fn move_focus_up_with_stacked_panes() { let new_pane_id_2 = PaneId::Terminal(3); let new_pane_id_3 = PaneId::Terminal(4); - tab.new_pane(new_pane_id_1, None, None, Some(client_id)) + tab.new_pane(new_pane_id_1, None, None, None, Some(client_id)) .unwrap(); - tab.new_pane(new_pane_id_2, None, None, Some(client_id)) + tab.new_pane(new_pane_id_2, None, None, None, Some(client_id)) .unwrap(); - tab.new_pane(new_pane_id_3, None, None, Some(client_id)) + tab.new_pane(new_pane_id_3, None, None, None, Some(client_id)) .unwrap(); tab.move_focus_right(client_id); tab.move_focus_up(client_id); @@ -3420,11 +3420,11 @@ fn move_focus_down_with_stacked_panes() { let new_pane_id_2 = PaneId::Terminal(3); let new_pane_id_3 = PaneId::Terminal(4); - tab.new_pane(new_pane_id_1, None, None, Some(client_id)) + tab.new_pane(new_pane_id_1, None, None, None, Some(client_id)) .unwrap(); - tab.new_pane(new_pane_id_2, None, None, Some(client_id)) + tab.new_pane(new_pane_id_2, None, None, None, Some(client_id)) .unwrap(); - tab.new_pane(new_pane_id_3, None, None, Some(client_id)) + tab.new_pane(new_pane_id_3, None, None, None, Some(client_id)) .unwrap(); tab.move_focus_right(client_id); tab.move_focus_up(client_id); @@ -3474,8 +3474,14 @@ fn move_focus_right_into_stacked_panes() { ); for i in 0..12 { let new_pane_id = i + 2; - tab.new_pane(PaneId::Terminal(new_pane_id), None, None, Some(client_id)) - .unwrap(); + tab.new_pane( + PaneId::Terminal(new_pane_id), + None, + None, + None, + Some(client_id), + ) + .unwrap(); } tab.move_focus_left(client_id); tab.horizontal_split(PaneId::Terminal(16), None, client_id) @@ -3535,8 +3541,14 @@ fn move_focus_left_into_stacked_panes() { ); for i in 0..13 { let new_pane_id = i + 2; - tab.new_pane(PaneId::Terminal(new_pane_id), None, None, Some(client_id)) - .unwrap(); + tab.new_pane( + PaneId::Terminal(new_pane_id), + None, + None, + None, + Some(client_id), + ) + .unwrap(); } tab.move_focus_right(client_id); tab.horizontal_split(PaneId::Terminal(1), None, client_id) @@ -3598,8 +3610,14 @@ fn move_focus_up_into_stacked_panes() { ); for i in 0..4 { let new_pane_id = i + 3; - tab.new_pane(PaneId::Terminal(new_pane_id), None, None, Some(client_id)) - .unwrap(); + tab.new_pane( + PaneId::Terminal(new_pane_id), + None, + None, + None, + Some(client_id), + ) + .unwrap(); } tab.move_focus_right(client_id); tab.move_focus_up(client_id); @@ -3662,8 +3680,14 @@ fn move_focus_down_into_stacked_panes() { ); for i in 0..4 { let new_pane_id = i + 3; - tab.new_pane(PaneId::Terminal(new_pane_id), None, None, Some(client_id)) - .unwrap(); + tab.new_pane( + PaneId::Terminal(new_pane_id), + None, + None, + None, + Some(client_id), + ) + .unwrap(); } tab.move_focus_left(client_id); tab.move_focus_up(client_id); @@ -3722,11 +3746,11 @@ fn close_main_stacked_pane() { let new_pane_id_2 = PaneId::Terminal(3); let new_pane_id_3 = PaneId::Terminal(4); - tab.new_pane(new_pane_id_1, None, None, Some(client_id)) + tab.new_pane(new_pane_id_1, None, None, None, Some(client_id)) .unwrap(); - tab.new_pane(new_pane_id_2, None, None, Some(client_id)) + tab.new_pane(new_pane_id_2, None, None, None, Some(client_id)) .unwrap(); - tab.new_pane(new_pane_id_3, None, None, Some(client_id)) + tab.new_pane(new_pane_id_3, None, None, None, Some(client_id)) .unwrap(); tab.close_pane(new_pane_id_2, false, None); tab.render(&mut output, None).unwrap(); @@ -3775,15 +3799,15 @@ fn close_main_stacked_pane_in_mid_stack() { let new_pane_id_4 = PaneId::Terminal(5); let new_pane_id_5 = PaneId::Terminal(6); - tab.new_pane(new_pane_id_1, None, None, Some(client_id)) + tab.new_pane(new_pane_id_1, None, None, None, Some(client_id)) .unwrap(); - tab.new_pane(new_pane_id_2, None, None, Some(client_id)) + tab.new_pane(new_pane_id_2, None, None, None, Some(client_id)) .unwrap(); - tab.new_pane(new_pane_id_3, None, None, Some(client_id)) + tab.new_pane(new_pane_id_3, None, None, None, Some(client_id)) .unwrap(); - tab.new_pane(new_pane_id_4, None, None, Some(client_id)) + tab.new_pane(new_pane_id_4, None, None, None, Some(client_id)) .unwrap(); - tab.new_pane(new_pane_id_5, None, None, Some(client_id)) + tab.new_pane(new_pane_id_5, None, None, None, Some(client_id)) .unwrap(); tab.move_focus_right(client_id); tab.move_focus_up(client_id); @@ -3835,15 +3859,15 @@ fn close_one_liner_stacked_pane_below_main_pane() { let new_pane_id_4 = PaneId::Terminal(5); let new_pane_id_5 = PaneId::Terminal(6); - tab.new_pane(new_pane_id_1, None, None, Some(client_id)) + tab.new_pane(new_pane_id_1, None, None, None, Some(client_id)) .unwrap(); - tab.new_pane(new_pane_id_2, None, None, Some(client_id)) + tab.new_pane(new_pane_id_2, None, None, None, Some(client_id)) .unwrap(); - tab.new_pane(new_pane_id_3, None, None, Some(client_id)) + tab.new_pane(new_pane_id_3, None, None, None, Some(client_id)) .unwrap(); - tab.new_pane(new_pane_id_4, None, None, Some(client_id)) + tab.new_pane(new_pane_id_4, None, None, None, Some(client_id)) .unwrap(); - tab.new_pane(new_pane_id_5, None, None, Some(client_id)) + tab.new_pane(new_pane_id_5, None, None, None, Some(client_id)) .unwrap(); tab.move_focus_left(client_id); tab.move_focus_right(client_id); @@ -3896,15 +3920,15 @@ fn close_one_liner_stacked_pane_above_main_pane() { let new_pane_id_4 = PaneId::Terminal(5); let new_pane_id_5 = PaneId::Terminal(6); - tab.new_pane(new_pane_id_1, None, None, Some(client_id)) + tab.new_pane(new_pane_id_1, None, None, None, Some(client_id)) .unwrap(); - tab.new_pane(new_pane_id_2, None, None, Some(client_id)) + tab.new_pane(new_pane_id_2, None, None, None, Some(client_id)) .unwrap(); - tab.new_pane(new_pane_id_3, None, None, Some(client_id)) + tab.new_pane(new_pane_id_3, None, None, None, Some(client_id)) .unwrap(); - tab.new_pane(new_pane_id_4, None, None, Some(client_id)) + tab.new_pane(new_pane_id_4, None, None, None, Some(client_id)) .unwrap(); - tab.new_pane(new_pane_id_5, None, None, Some(client_id)) + tab.new_pane(new_pane_id_5, None, None, None, Some(client_id)) .unwrap(); tab.move_focus_right(client_id); tab.move_focus_up(client_id); @@ -3956,15 +3980,15 @@ fn can_increase_size_of_main_pane_in_stack_horizontally() { let new_pane_id_4 = PaneId::Terminal(5); let new_pane_id_5 = PaneId::Terminal(6); - tab.new_pane(new_pane_id_1, None, None, Some(client_id)) + tab.new_pane(new_pane_id_1, None, None, None, Some(client_id)) .unwrap(); - tab.new_pane(new_pane_id_2, None, None, Some(client_id)) + tab.new_pane(new_pane_id_2, None, None, None, Some(client_id)) .unwrap(); - tab.new_pane(new_pane_id_3, None, None, Some(client_id)) + tab.new_pane(new_pane_id_3, None, None, None, Some(client_id)) .unwrap(); - tab.new_pane(new_pane_id_4, None, None, Some(client_id)) + tab.new_pane(new_pane_id_4, None, None, None, Some(client_id)) .unwrap(); - tab.new_pane(new_pane_id_5, None, None, Some(client_id)) + tab.new_pane(new_pane_id_5, None, None, None, Some(client_id)) .unwrap(); tab.move_focus_right(client_id); tab.resize( @@ -4020,15 +4044,15 @@ fn can_increase_size_of_main_pane_in_stack_vertically() { let new_pane_id_4 = PaneId::Terminal(5); let new_pane_id_5 = PaneId::Terminal(6); - tab.new_pane(new_pane_id_1, None, None, Some(client_id)) + tab.new_pane(new_pane_id_1, None, None, None, Some(client_id)) .unwrap(); - tab.new_pane(new_pane_id_2, None, None, Some(client_id)) + tab.new_pane(new_pane_id_2, None, None, None, Some(client_id)) .unwrap(); - tab.new_pane(new_pane_id_3, None, None, Some(client_id)) + tab.new_pane(new_pane_id_3, None, None, None, Some(client_id)) .unwrap(); - tab.new_pane(new_pane_id_4, None, None, Some(client_id)) + tab.new_pane(new_pane_id_4, None, None, None, Some(client_id)) .unwrap(); - tab.new_pane(new_pane_id_5, None, None, Some(client_id)) + tab.new_pane(new_pane_id_5, None, None, None, Some(client_id)) .unwrap(); tab.move_focus_right(client_id); tab.resize( @@ -4084,15 +4108,15 @@ fn can_increase_size_of_main_pane_in_stack_non_directionally() { let new_pane_id_4 = PaneId::Terminal(5); let new_pane_id_5 = PaneId::Terminal(6); - tab.new_pane(new_pane_id_1, None, None, Some(client_id)) + tab.new_pane(new_pane_id_1, None, None, None, Some(client_id)) .unwrap(); - tab.new_pane(new_pane_id_2, None, None, Some(client_id)) + tab.new_pane(new_pane_id_2, None, None, None, Some(client_id)) .unwrap(); - tab.new_pane(new_pane_id_3, None, None, Some(client_id)) + tab.new_pane(new_pane_id_3, None, None, None, Some(client_id)) .unwrap(); - tab.new_pane(new_pane_id_4, None, None, Some(client_id)) + tab.new_pane(new_pane_id_4, None, None, None, Some(client_id)) .unwrap(); - tab.new_pane(new_pane_id_5, None, None, Some(client_id)) + tab.new_pane(new_pane_id_5, None, None, None, Some(client_id)) .unwrap(); let _ = tab.move_focus_up(client_id); let _ = tab.move_focus_right(client_id); @@ -4144,15 +4168,15 @@ fn can_increase_size_into_pane_stack_horizontally() { let new_pane_id_4 = PaneId::Terminal(5); let new_pane_id_5 = PaneId::Terminal(6); - tab.new_pane(new_pane_id_1, None, None, Some(client_id)) + tab.new_pane(new_pane_id_1, None, None, None, Some(client_id)) .unwrap(); - tab.new_pane(new_pane_id_2, None, None, Some(client_id)) + tab.new_pane(new_pane_id_2, None, None, None, Some(client_id)) .unwrap(); - tab.new_pane(new_pane_id_3, None, None, Some(client_id)) + tab.new_pane(new_pane_id_3, None, None, None, Some(client_id)) .unwrap(); - tab.new_pane(new_pane_id_4, None, None, Some(client_id)) + tab.new_pane(new_pane_id_4, None, None, None, Some(client_id)) .unwrap(); - tab.new_pane(new_pane_id_5, None, None, Some(client_id)) + tab.new_pane(new_pane_id_5, None, None, None, Some(client_id)) .unwrap(); tab.resize( client_id, @@ -4207,15 +4231,15 @@ fn can_increase_size_into_pane_stack_vertically() { let new_pane_id_4 = PaneId::Terminal(5); let new_pane_id_5 = PaneId::Terminal(6); - tab.new_pane(new_pane_id_1, None, None, Some(client_id)) + tab.new_pane(new_pane_id_1, None, None, None, Some(client_id)) .unwrap(); - tab.new_pane(new_pane_id_2, None, None, Some(client_id)) + tab.new_pane(new_pane_id_2, None, None, None, Some(client_id)) .unwrap(); - tab.new_pane(new_pane_id_3, None, None, Some(client_id)) + tab.new_pane(new_pane_id_3, None, None, None, Some(client_id)) .unwrap(); - tab.new_pane(new_pane_id_4, None, None, Some(client_id)) + tab.new_pane(new_pane_id_4, None, None, None, Some(client_id)) .unwrap(); - tab.new_pane(new_pane_id_5, None, None, Some(client_id)) + tab.new_pane(new_pane_id_5, None, None, None, Some(client_id)) .unwrap(); tab.move_focus_right(client_id); tab.move_focus_down(client_id); @@ -4272,15 +4296,15 @@ fn can_increase_size_into_pane_stack_non_directionally() { let new_pane_id_4 = PaneId::Terminal(5); let new_pane_id_5 = PaneId::Terminal(6); - tab.new_pane(new_pane_id_1, None, None, Some(client_id)) + tab.new_pane(new_pane_id_1, None, None, None, Some(client_id)) .unwrap(); - tab.new_pane(new_pane_id_2, None, None, Some(client_id)) + tab.new_pane(new_pane_id_2, None, None, None, Some(client_id)) .unwrap(); - tab.new_pane(new_pane_id_3, None, None, Some(client_id)) + tab.new_pane(new_pane_id_3, None, None, None, Some(client_id)) .unwrap(); - tab.new_pane(new_pane_id_4, None, None, Some(client_id)) + tab.new_pane(new_pane_id_4, None, None, None, Some(client_id)) .unwrap(); - tab.new_pane(new_pane_id_5, None, None, Some(client_id)) + tab.new_pane(new_pane_id_5, None, None, None, Some(client_id)) .unwrap(); let _ = tab.move_focus_up(client_id); tab.resize(client_id, ResizeStrategy::new(Resize::Increase, None)) @@ -4331,15 +4355,15 @@ fn decreasing_size_of_whole_tab_treats_stacked_panes_properly() { let new_pane_id_4 = PaneId::Terminal(5); let new_pane_id_5 = PaneId::Terminal(6); - tab.new_pane(new_pane_id_1, None, None, Some(client_id)) + tab.new_pane(new_pane_id_1, None, None, None, Some(client_id)) .unwrap(); - tab.new_pane(new_pane_id_2, None, None, Some(client_id)) + tab.new_pane(new_pane_id_2, None, None, None, Some(client_id)) .unwrap(); - tab.new_pane(new_pane_id_3, None, None, Some(client_id)) + tab.new_pane(new_pane_id_3, None, None, None, Some(client_id)) .unwrap(); - tab.new_pane(new_pane_id_4, None, None, Some(client_id)) + tab.new_pane(new_pane_id_4, None, None, None, Some(client_id)) .unwrap(); - tab.new_pane(new_pane_id_5, None, None, Some(client_id)) + tab.new_pane(new_pane_id_5, None, None, None, Some(client_id)) .unwrap(); tab.resize_whole_tab(Size { cols: 100, @@ -4391,15 +4415,15 @@ fn increasing_size_of_whole_tab_treats_stacked_panes_properly() { let new_pane_id_4 = PaneId::Terminal(5); let new_pane_id_5 = PaneId::Terminal(6); - tab.new_pane(new_pane_id_1, None, None, Some(client_id)) + tab.new_pane(new_pane_id_1, None, None, None, Some(client_id)) .unwrap(); - tab.new_pane(new_pane_id_2, None, None, Some(client_id)) + tab.new_pane(new_pane_id_2, None, None, None, Some(client_id)) .unwrap(); - tab.new_pane(new_pane_id_3, None, None, Some(client_id)) + tab.new_pane(new_pane_id_3, None, None, None, Some(client_id)) .unwrap(); - tab.new_pane(new_pane_id_4, None, None, Some(client_id)) + tab.new_pane(new_pane_id_4, None, None, None, Some(client_id)) .unwrap(); - tab.new_pane(new_pane_id_5, None, None, Some(client_id)) + tab.new_pane(new_pane_id_5, None, None, None, Some(client_id)) .unwrap(); tab.resize_whole_tab(Size { cols: 100, @@ -4456,15 +4480,15 @@ fn cannot_decrease_stack_size_beyond_minimum_height() { let new_pane_id_4 = PaneId::Terminal(5); let new_pane_id_5 = PaneId::Terminal(6); - tab.new_pane(new_pane_id_1, None, None, Some(client_id)) + tab.new_pane(new_pane_id_1, None, None, None, Some(client_id)) .unwrap(); - tab.new_pane(new_pane_id_2, None, None, Some(client_id)) + tab.new_pane(new_pane_id_2, None, None, None, Some(client_id)) .unwrap(); - tab.new_pane(new_pane_id_3, None, None, Some(client_id)) + tab.new_pane(new_pane_id_3, None, None, None, Some(client_id)) .unwrap(); - tab.new_pane(new_pane_id_4, None, None, Some(client_id)) + tab.new_pane(new_pane_id_4, None, None, None, Some(client_id)) .unwrap(); - tab.new_pane(new_pane_id_5, None, None, Some(client_id)) + tab.new_pane(new_pane_id_5, None, None, None, Some(client_id)) .unwrap(); tab.move_focus_down(client_id); for _ in 0..6 { @@ -4521,15 +4545,15 @@ fn focus_stacked_pane_over_flexible_pane_with_the_mouse() { let new_pane_id_4 = PaneId::Terminal(5); let new_pane_id_5 = PaneId::Terminal(6); - tab.new_pane(new_pane_id_1, None, None, Some(client_id)) + tab.new_pane(new_pane_id_1, None, None, None, Some(client_id)) .unwrap(); - tab.new_pane(new_pane_id_2, None, None, Some(client_id)) + tab.new_pane(new_pane_id_2, None, None, None, Some(client_id)) .unwrap(); - tab.new_pane(new_pane_id_3, None, None, Some(client_id)) + tab.new_pane(new_pane_id_3, None, None, None, Some(client_id)) .unwrap(); - tab.new_pane(new_pane_id_4, None, None, Some(client_id)) + tab.new_pane(new_pane_id_4, None, None, None, Some(client_id)) .unwrap(); - tab.new_pane(new_pane_id_5, None, None, Some(client_id)) + tab.new_pane(new_pane_id_5, None, None, None, Some(client_id)) .unwrap(); tab.handle_left_click(&Position::new(1, 71), client_id) .unwrap(); @@ -4580,15 +4604,15 @@ fn focus_stacked_pane_under_flexible_pane_with_the_mouse() { let new_pane_id_4 = PaneId::Terminal(5); let new_pane_id_5 = PaneId::Terminal(6); - tab.new_pane(new_pane_id_1, None, None, Some(client_id)) + tab.new_pane(new_pane_id_1, None, None, None, Some(client_id)) .unwrap(); - tab.new_pane(new_pane_id_2, None, None, Some(client_id)) + tab.new_pane(new_pane_id_2, None, None, None, Some(client_id)) .unwrap(); - tab.new_pane(new_pane_id_3, None, None, Some(client_id)) + tab.new_pane(new_pane_id_3, None, None, None, Some(client_id)) .unwrap(); - tab.new_pane(new_pane_id_4, None, None, Some(client_id)) + tab.new_pane(new_pane_id_4, None, None, None, Some(client_id)) .unwrap(); - tab.new_pane(new_pane_id_5, None, None, Some(client_id)) + tab.new_pane(new_pane_id_5, None, None, None, Some(client_id)) .unwrap(); tab.handle_left_click(&Position::new(1, 71), client_id) .unwrap(); @@ -4641,15 +4665,15 @@ fn close_stacked_pane_with_previously_focused_other_pane() { let new_pane_id_4 = PaneId::Terminal(5); let new_pane_id_5 = PaneId::Terminal(6); - tab.new_pane(new_pane_id_1, None, None, Some(client_id)) + tab.new_pane(new_pane_id_1, None, None, None, Some(client_id)) .unwrap(); - tab.new_pane(new_pane_id_2, None, None, Some(client_id)) + tab.new_pane(new_pane_id_2, None, None, None, Some(client_id)) .unwrap(); - tab.new_pane(new_pane_id_3, None, None, Some(client_id)) + tab.new_pane(new_pane_id_3, None, None, None, Some(client_id)) .unwrap(); - tab.new_pane(new_pane_id_4, None, None, Some(client_id)) + tab.new_pane(new_pane_id_4, None, None, None, Some(client_id)) .unwrap(); - tab.new_pane(new_pane_id_5, None, None, Some(client_id)) + tab.new_pane(new_pane_id_5, None, None, None, Some(client_id)) .unwrap(); tab.handle_left_click(&Position::new(2, 71), client_id) .unwrap(); @@ -4708,15 +4732,15 @@ fn close_pane_near_stacked_panes() { let new_pane_id_4 = PaneId::Terminal(5); let new_pane_id_5 = PaneId::Terminal(6); - tab.new_pane(new_pane_id_1, None, None, Some(client_id)) + tab.new_pane(new_pane_id_1, None, None, None, Some(client_id)) .unwrap(); - tab.new_pane(new_pane_id_2, None, None, Some(client_id)) + tab.new_pane(new_pane_id_2, None, None, None, Some(client_id)) .unwrap(); - tab.new_pane(new_pane_id_3, None, None, Some(client_id)) + tab.new_pane(new_pane_id_3, None, None, None, Some(client_id)) .unwrap(); - tab.new_pane(new_pane_id_4, None, None, Some(client_id)) + tab.new_pane(new_pane_id_4, None, None, None, Some(client_id)) .unwrap(); - tab.new_pane(new_pane_id_5, None, None, Some(client_id)) + tab.new_pane(new_pane_id_5, None, None, None, Some(client_id)) .unwrap(); tab.close_pane(PaneId::Terminal(6), false, None); tab.render(&mut output, None).unwrap(); @@ -4772,15 +4796,15 @@ fn focus_next_pane_expands_stacked_panes() { let new_pane_id_4 = PaneId::Terminal(5); let new_pane_id_5 = PaneId::Terminal(6); - tab.new_pane(new_pane_id_1, None, None, Some(client_id)) + tab.new_pane(new_pane_id_1, None, None, None, Some(client_id)) .unwrap(); - tab.new_pane(new_pane_id_2, None, None, Some(client_id)) + tab.new_pane(new_pane_id_2, None, None, None, Some(client_id)) .unwrap(); - tab.new_pane(new_pane_id_3, None, None, Some(client_id)) + tab.new_pane(new_pane_id_3, None, None, None, Some(client_id)) .unwrap(); - tab.new_pane(new_pane_id_4, None, None, Some(client_id)) + tab.new_pane(new_pane_id_4, None, None, None, Some(client_id)) .unwrap(); - tab.new_pane(new_pane_id_5, None, None, Some(client_id)) + tab.new_pane(new_pane_id_5, None, None, None, Some(client_id)) .unwrap(); tab.move_focus_left(client_id); tab.focus_next_pane(client_id); @@ -4832,15 +4856,15 @@ fn stacked_panes_can_become_fullscreen() { let new_pane_id_4 = PaneId::Terminal(5); let new_pane_id_5 = PaneId::Terminal(6); - tab.new_pane(new_pane_id_1, None, None, Some(client_id)) + tab.new_pane(new_pane_id_1, None, None, None, Some(client_id)) .unwrap(); - tab.new_pane(new_pane_id_2, None, None, Some(client_id)) + tab.new_pane(new_pane_id_2, None, None, None, Some(client_id)) .unwrap(); - tab.new_pane(new_pane_id_3, None, None, Some(client_id)) + tab.new_pane(new_pane_id_3, None, None, None, Some(client_id)) .unwrap(); - tab.new_pane(new_pane_id_4, None, None, Some(client_id)) + tab.new_pane(new_pane_id_4, None, None, None, Some(client_id)) .unwrap(); - tab.new_pane(new_pane_id_5, None, None, Some(client_id)) + tab.new_pane(new_pane_id_5, None, None, None, Some(client_id)) .unwrap(); tab.move_focus_up(client_id); tab.toggle_active_pane_fullscreen(client_id); @@ -5467,8 +5491,14 @@ fn new_pane_in_auto_layout() { ]; for i in 0..7 { let new_pane_id = i + 2; - tab.new_pane(PaneId::Terminal(new_pane_id), None, None, Some(client_id)) - .unwrap(); + tab.new_pane( + PaneId::Terminal(new_pane_id), + None, + None, + None, + Some(client_id), + ) + .unwrap(); tab.render(&mut output, None).unwrap(); let (snapshot, cursor_coordinates) = take_snapshot_and_cursor_position( @@ -6379,6 +6409,7 @@ fn new_floating_pane_in_auto_layout() { PaneId::Terminal(new_pane_id), None, Some(should_float), + None, Some(client_id), ) .unwrap(); diff --git a/zellij-server/src/tab/unit/tab_tests.rs b/zellij-server/src/tab/unit/tab_tests.rs index bcf02202c8..5374ed7043 100644 --- a/zellij-server/src/tab/unit/tab_tests.rs +++ b/zellij-server/src/tab/unit/tab_tests.rs @@ -537,7 +537,8 @@ fn split_largest_pane() { let mut tab = create_new_tab(size); for i in 2..5 { let new_pane_id = PaneId::Terminal(i); - tab.new_pane(new_pane_id, None, None, Some(1)).unwrap(); + tab.new_pane(new_pane_id, None, None, None, Some(1)) + .unwrap(); } assert_eq!(tab.tiled_panes.panes.len(), 4, "The tab has four panes"); @@ -742,7 +743,7 @@ pub fn cannot_split_panes_horizontally_when_active_pane_is_too_small() { pub fn cannot_split_largest_pane_when_there_is_no_room() { let size = Size { cols: 8, rows: 4 }; let mut tab = create_new_tab(size); - tab.new_pane(PaneId::Terminal(2), None, None, Some(1)) + tab.new_pane(PaneId::Terminal(2), None, None, None, Some(1)) .unwrap(); assert_eq!( tab.tiled_panes.panes.len(), @@ -786,7 +787,8 @@ pub fn toggle_focused_pane_fullscreen() { let mut tab = create_new_tab(size); for i in 2..5 { let new_pane_id = PaneId::Terminal(i); - tab.new_pane(new_pane_id, None, None, Some(1)).unwrap(); + tab.new_pane(new_pane_id, None, None, None, Some(1)) + .unwrap(); } tab.toggle_active_pane_fullscreen(1); assert_eq!( @@ -860,16 +862,16 @@ fn switch_to_next_pane_fullscreen() { let mut active_tab = create_new_tab(size); active_tab - .new_pane(PaneId::Terminal(1), None, None, Some(1)) + .new_pane(PaneId::Terminal(1), None, None, None, Some(1)) .unwrap(); active_tab - .new_pane(PaneId::Terminal(2), None, None, Some(1)) + .new_pane(PaneId::Terminal(2), None, None, None, Some(1)) .unwrap(); active_tab - .new_pane(PaneId::Terminal(3), None, None, Some(1)) + .new_pane(PaneId::Terminal(3), None, None, None, Some(1)) .unwrap(); active_tab - .new_pane(PaneId::Terminal(4), None, None, Some(1)) + .new_pane(PaneId::Terminal(4), None, None, None, Some(1)) .unwrap(); active_tab.toggle_active_pane_fullscreen(1); @@ -900,16 +902,16 @@ fn switch_to_prev_pane_fullscreen() { //testing four consecutive switches in fullscreen mode active_tab - .new_pane(PaneId::Terminal(1), None, None, Some(1)) + .new_pane(PaneId::Terminal(1), None, None, None, Some(1)) .unwrap(); active_tab - .new_pane(PaneId::Terminal(2), None, None, Some(1)) + .new_pane(PaneId::Terminal(2), None, None, None, Some(1)) .unwrap(); active_tab - .new_pane(PaneId::Terminal(3), None, None, Some(1)) + .new_pane(PaneId::Terminal(3), None, None, None, Some(1)) .unwrap(); active_tab - .new_pane(PaneId::Terminal(4), None, None, Some(1)) + .new_pane(PaneId::Terminal(4), None, None, None, Some(1)) .unwrap(); active_tab.toggle_active_pane_fullscreen(1); // order is now 1 2 3 4 @@ -14391,7 +14393,7 @@ fn correctly_resize_frameless_panes_on_pane_close() { let content_size = (pane.get_content_columns(), pane.get_content_rows()); assert_eq!(content_size, (cols, rows)); - tab.new_pane(PaneId::Terminal(2), None, None, Some(1)) + tab.new_pane(PaneId::Terminal(2), None, None, None, Some(1)) .unwrap(); tab.close_pane(PaneId::Terminal(2), true, None); diff --git a/zellij-server/src/ui/loading_indication.rs b/zellij-server/src/ui/loading_indication.rs index 78af2c1037..1aa514ec5d 100644 --- a/zellij-server/src/ui/loading_indication.rs +++ b/zellij-server/src/ui/loading_indication.rs @@ -45,9 +45,15 @@ impl LoadingIndication { pub fn merge(&mut self, other: LoadingIndication) { let current_animation_offset = self.animation_offset; let current_terminal_emulator_colors = self.terminal_emulator_colors.take(); + let mut current_error = self.error.take(); drop(std::mem::replace(self, other)); self.animation_offset = current_animation_offset; self.terminal_emulator_colors = current_terminal_emulator_colors; + if let Some(current_error) = current_error.take() { + // we do this so that only the first error (usually the root cause) will be shown + // when plugins support scrolling, we might want to do an append here + self.error = Some(current_error); + } } pub fn indicate_loading_plugin_from_memory(&mut self) { self.loading_from_memory = Some(LoadingStatus::InProgress); @@ -104,6 +110,9 @@ impl LoadingIndication { pub fn indicate_loading_error(&mut self, error_text: String) { self.error = Some(error_text); } + pub fn is_error(&self) -> bool { + self.error.is_some() + } fn started_loading(&self) -> bool { self.loading_from_memory.is_some() || self.loading_from_hd_cache.is_some() @@ -257,6 +266,13 @@ impl Display for LoadingIndication { } if let Some(error_text) = &self.error { stringified.push_str(&format!("\n\r{} {error_text}", red.bold().paint("ERROR:"))); + // we add this additional line explicitly to make it easier to realize when something + // is wrong in very small plugins (eg. the tab-bar and status-bar) + stringified.push_str(&format!( + "\n\r{}", + red.bold() + .paint("ERROR IN PLUGIN - check logs for more info") + )); } write!(f, "{}", stringified) } diff --git a/zellij-server/src/unit/screen_tests.rs b/zellij-server/src/unit/screen_tests.rs index d4cd8c1e76..8639970b49 100644 --- a/zellij-server/src/unit/screen_tests.rs +++ b/zellij-server/src/unit/screen_tests.rs @@ -14,7 +14,9 @@ use zellij_utils::data::Resize; use zellij_utils::errors::{prelude::*, ErrorContext}; use zellij_utils::input::actions::Action; use zellij_utils::input::command::{RunCommand, TerminalAction}; -use zellij_utils::input::layout::{Layout, SplitDirection, TiledPaneLayout}; +use zellij_utils::input::layout::{ + Layout, Run, RunPlugin, RunPluginLocation, SplitDirection, TiledPaneLayout, +}; use zellij_utils::input::options::Options; use zellij_utils::ipc::IpcReceiverWithContext; use zellij_utils::pane_size::{Size, SizeInPixels}; @@ -291,7 +293,11 @@ impl MockScreen { let pane_layout = initial_layout.unwrap_or_default(); let pane_count = pane_layout.extract_run_instructions().len(); let mut pane_ids = vec![]; - let plugin_ids = HashMap::new(); + let mut plugin_ids = HashMap::new(); + plugin_ids.insert( + RunPluginLocation::File(PathBuf::from("/path/to/fake/plugin")), + vec![1], + ); for i in 0..pane_count { pane_ids.push((i as u32, None)); } @@ -864,7 +870,7 @@ fn switch_to_tab_with_fullscreen() { { let active_tab = screen.get_active_tab_mut(1).unwrap(); active_tab - .new_pane(PaneId::Terminal(2), None, None, Some(1)) + .new_pane(PaneId::Terminal(2), None, None, None, Some(1)) .unwrap(); active_tab.toggle_active_pane_fullscreen(1); } @@ -979,7 +985,7 @@ fn attach_after_first_tab_closed() { { let active_tab = screen.get_active_tab_mut(1).unwrap(); active_tab - .new_pane(PaneId::Terminal(2), None, None, Some(1)) + .new_pane(PaneId::Terminal(2), None, None, None, Some(1)) .unwrap(); active_tab.toggle_active_pane_fullscreen(1); } @@ -2715,3 +2721,145 @@ pub fn send_cli_query_tab_names_action() { .cloned(); assert_snapshot!(format!("{:#?}", log_tab_names_instruction)); } + +#[test] +pub fn send_cli_launch_or_focus_plugin_action() { + let size = Size { + cols: 121, + rows: 20, + }; + let client_id = 10; // fake client id should not appear in the screen's state + let mut mock_screen = MockScreen::new(size); + let plugin_receiver = mock_screen.plugin_receiver.take().unwrap(); + let session_metadata = mock_screen.clone_session_metadata(); + let screen_thread = mock_screen.run(None); + let received_plugin_instructions = Arc::new(Mutex::new(vec![])); + let plugin_thread = log_actions_in_thread!( + received_plugin_instructions, + PluginInstruction::Exit, + plugin_receiver + ); + let cli_action = CliAction::LaunchOrFocusPlugin { + floating: true, + url: url::Url::parse("file:/path/to/fake/plugin").unwrap(), + }; + send_cli_action_to_server(&session_metadata, cli_action, &mut mock_screen, client_id); + std::thread::sleep(std::time::Duration::from_millis(100)); // give time for actions to be + mock_screen.teardown(vec![plugin_thread, screen_thread]); + + let plugin_load_instruction = received_plugin_instructions + .lock() + .unwrap() + .iter() + .find(|instruction| match instruction { + PluginInstruction::Load(..) => true, + _ => false, + }) + .cloned(); + + assert_snapshot!(format!("{:#?}", plugin_load_instruction)); +} + +#[test] +pub fn send_cli_launch_or_focus_plugin_action_when_plugin_is_already_loaded() { + let size = Size { + cols: 121, + rows: 20, + }; + let client_id = 10; // fake client id should not appear in the screen's state + let mut mock_screen = MockScreen::new(size); + let plugin_receiver = mock_screen.plugin_receiver.take().unwrap(); + let session_metadata = mock_screen.clone_session_metadata(); + let mut initial_layout = TiledPaneLayout::default(); + let existing_plugin_pane = TiledPaneLayout { + run: Some(Run::Plugin(RunPlugin { + _allow_exec_host_cmd: false, + location: RunPluginLocation::File(PathBuf::from("/path/to/fake/plugin")), + })), + ..Default::default() + }; + initial_layout.children_split_direction = SplitDirection::Vertical; + initial_layout.children = vec![TiledPaneLayout::default(), existing_plugin_pane]; + let screen_thread = mock_screen.run(Some(initial_layout)); + let received_plugin_instructions = Arc::new(Mutex::new(vec![])); + let plugin_thread = log_actions_in_thread!( + received_plugin_instructions, + PluginInstruction::Exit, + plugin_receiver + ); + let received_server_instructions = Arc::new(Mutex::new(vec![])); + let server_receiver = mock_screen.server_receiver.take().unwrap(); + let server_thread = log_actions_in_thread!( + received_server_instructions, + ServerInstruction::KillSession, + server_receiver + ); + let cli_action = CliAction::LaunchOrFocusPlugin { + floating: true, + url: url::Url::parse("file:/path/to/fake/plugin").unwrap(), + }; + send_cli_action_to_server(&session_metadata, cli_action, &mut mock_screen, client_id); + std::thread::sleep(std::time::Duration::from_millis(100)); // give time for actions to be + mock_screen.teardown(vec![plugin_thread, server_thread, screen_thread]); + + let plugin_load_instruction_sent = received_plugin_instructions + .lock() + .unwrap() + .iter() + .find(|instruction| match instruction { + PluginInstruction::Load(..) => true, + _ => false, + }) + .is_some(); + assert!( + !plugin_load_instruction_sent, + "Plugin Load instruction should not be sent for an already loaded plugin" + ); + let snapshots = take_snapshots_and_cursor_coordinates_from_render_events( + received_server_instructions.lock().unwrap().iter(), + size, + ); + let snapshot_count = snapshots.len(); + assert_eq!( + snapshot_count, 2, + "Another render was sent for focusing the already loaded plugin" + ); + for (cursor_coordinates, _snapshot) in snapshots.iter().skip(1) { + assert!( + cursor_coordinates.is_none(), + "Cursor moved to existing plugin in final snapshot indicating focus changed" + ); + } +} + +#[test] +pub fn screen_can_suppress_pane() { + let size = Size { cols: 80, rows: 20 }; + let mut initial_layout = TiledPaneLayout::default(); + initial_layout.children_split_direction = SplitDirection::Vertical; + initial_layout.children = vec![TiledPaneLayout::default(), TiledPaneLayout::default()]; + let mut mock_screen = MockScreen::new(size); + let screen_thread = mock_screen.run(Some(initial_layout)); + let received_server_instructions = Arc::new(Mutex::new(vec![])); + let server_receiver = mock_screen.server_receiver.take().unwrap(); + let server_thread = log_actions_in_thread!( + received_server_instructions, + ServerInstruction::KillSession, + server_receiver + ); + let _ = mock_screen + .to_screen + .send(ScreenInstruction::SuppressPane(PaneId::Terminal(1), 1)); + std::thread::sleep(std::time::Duration::from_millis(100)); + mock_screen.teardown(vec![server_thread, screen_thread]); + + let snapshots = take_snapshots_and_cursor_coordinates_from_render_events( + received_server_instructions.lock().unwrap().iter(), + size, + ); + let snapshot_count = snapshots.len(); + for (_cursor_coordinates, snapshot) in snapshots { + assert_snapshot!(format!("{}", snapshot)); + } + assert_snapshot!(format!("{}", snapshot_count)); +} diff --git a/zellij-server/src/unit/snapshots/zellij_server__screen__screen_tests__screen_can_suppress_pane-2.snap b/zellij-server/src/unit/snapshots/zellij_server__screen__screen_tests__screen_can_suppress_pane-2.snap new file mode 100644 index 0000000000..e4fbe94276 --- /dev/null +++ b/zellij-server/src/unit/snapshots/zellij_server__screen__screen_tests__screen_can_suppress_pane-2.snap @@ -0,0 +1,26 @@ +--- +source: zellij-server/src/./unit/screen_tests.rs +assertion_line: 2852 +expression: "format!(\"{}\", snapshot)" +--- +00 (C): ┌ Pane #1 ─────────────────────────────────────────────────────────────────────┐ +01 (C): │ │ +02 (C): │ │ +03 (C): │ │ +04 (C): │ │ +05 (C): │ │ +06 (C): │ │ +07 (C): │ │ +08 (C): │ │ +09 (C): │ │ +10 (C): │ │ +11 (C): │ │ +12 (C): │ │ +13 (C): │ │ +14 (C): │ │ +15 (C): │ │ +16 (C): │ │ +17 (C): │ │ +18 (C): │ │ +19 (C): └──────────────────────────────────────────────────────────────────────────────┘ + diff --git a/zellij-server/src/unit/snapshots/zellij_server__screen__screen_tests__screen_can_suppress_pane-3.snap b/zellij-server/src/unit/snapshots/zellij_server__screen__screen_tests__screen_can_suppress_pane-3.snap new file mode 100644 index 0000000000..ea3ddf3431 --- /dev/null +++ b/zellij-server/src/unit/snapshots/zellij_server__screen__screen_tests__screen_can_suppress_pane-3.snap @@ -0,0 +1,6 @@ +--- +source: zellij-server/src/./unit/screen_tests.rs +assertion_line: 2854 +expression: "format!(\"{}\", snapshot_count)" +--- +2 diff --git a/zellij-server/src/unit/snapshots/zellij_server__screen__screen_tests__screen_can_suppress_pane.snap b/zellij-server/src/unit/snapshots/zellij_server__screen__screen_tests__screen_can_suppress_pane.snap new file mode 100644 index 0000000000..3a14dc5513 --- /dev/null +++ b/zellij-server/src/unit/snapshots/zellij_server__screen__screen_tests__screen_can_suppress_pane.snap @@ -0,0 +1,26 @@ +--- +source: zellij-server/src/./unit/screen_tests.rs +assertion_line: 2852 +expression: "format!(\"{}\", snapshot)" +--- +00 (C): ┌ Pane #1 ─────────────────────────────┐┌ Pane #2 ─────────────────────────────┐ +01 (C): │ ││ │ +02 (C): │ ││ │ +03 (C): │ ││ │ +04 (C): │ ││ │ +05 (C): │ ││ │ +06 (C): │ ││ │ +07 (C): │ ││ │ +08 (C): │ ││ │ +09 (C): │ ││ │ +10 (C): │ ││ │ +11 (C): │ ││ │ +12 (C): │ ││ │ +13 (C): │ ││ │ +14 (C): │ ││ │ +15 (C): │ ││ │ +16 (C): │ ││ │ +17 (C): │ ││ │ +18 (C): │ ││ │ +19 (C): └──────────────────────────────────────┘└──────────────────────────────────────┘ + diff --git a/zellij-server/src/unit/snapshots/zellij_server__screen__screen_tests__send_cli_launch_or_focus_plugin_action.snap b/zellij-server/src/unit/snapshots/zellij_server__screen__screen_tests__send_cli_launch_or_focus_plugin_action.snap new file mode 100644 index 0000000000..0751fa5f1c --- /dev/null +++ b/zellij-server/src/unit/snapshots/zellij_server__screen__screen_tests__send_cli_launch_or_focus_plugin_action.snap @@ -0,0 +1,25 @@ +--- +source: zellij-server/src/./unit/screen_tests.rs +assertion_line: 2758 +expression: "format!(\"{:#?}\", plugin_load_instruction)" +--- +Some( + Load( + Some( + true, + ), + None, + RunPlugin { + _allow_exec_host_cmd: false, + location: File( + "/path/to/fake/plugin", + ), + }, + 0, + 1, + Size { + rows: 0, + cols: 0, + }, + ), +) diff --git a/zellij-tile/src/lib.rs b/zellij-tile/src/lib.rs index 685de4afec..7968ced3d3 100644 --- a/zellij-tile/src/lib.rs +++ b/zellij-tile/src/lib.rs @@ -14,7 +14,6 @@ pub trait ZellijPlugin { } #[allow(unused_variables)] -// TODO: can we get rid of the lifetime? maybe with generics? pub trait ZellijWorker<'de>: Default + Serialize + Deserialize<'de> { fn on_message(&mut self, message: String, payload: String) {} } @@ -54,12 +53,13 @@ macro_rules! register_plugin { #[no_mangle] pub fn update() -> bool { - let object = $crate::shim::object_from_stdin() - .context($crate::PLUGIN_MISMATCH) - .to_stdout() - .unwrap(); - - STATE.with(|state| state.borrow_mut().update(object)) + STATE.with(|state| { + let object = $crate::shim::object_from_stdin() + .context($crate::PLUGIN_MISMATCH) + .to_stdout() + .unwrap(); + state.borrow_mut().update(object) + }) } #[no_mangle] @@ -78,10 +78,14 @@ macro_rules! register_plugin { #[macro_export] macro_rules! register_worker { - ($worker:ty, $worker_name:ident) => { + ($worker:ty, $worker_name:ident, $worker_static_name:ident) => { + // persist worker state in memory in a static variable + thread_local! { + static $worker_static_name: std::cell::RefCell<$worker> = std::cell::RefCell::new(Default::default()); + } #[no_mangle] pub fn $worker_name() { - use serde_json::*; + let worker_display_name = std::stringify!($worker_name); // read message from STDIN @@ -93,43 +97,10 @@ macro_rules! register_worker { ); Default::default() }); - - // read previous worker state from HD if it exists - let mut worker_instance = match std::fs::read(&format!("/data/{}", worker_display_name)) - .map_err(|e| format!("Failed to read file: {:?}", e)) - .and_then(|s| { - serde_json::from_str::<$worker>(&String::from_utf8_lossy(&s)) - .map_err(|e| format!("Failed to deserialize: {:?}", e)) - }) { - Ok(s) => s, - Err(e) => { - eprintln!( - "Failed to read existing state ({:?}), creating new state for worker", - e - ); - <$worker>::default() - }, - }; - - // invoke worker - worker_instance.on_message(message, payload); - - // persist worker state to HD for next run - match serde_json::to_string(&worker_instance) - .map_err(|e| format!("Failed to serialize worker state")) - .and_then(|serialized_state| { - std::fs::write( - &format!("/data/{}", worker_display_name), - serialized_state.as_bytes(), - ) - .map_err(|e| format!("Failed to persist state to HD: {:?}", e)) - }) { - Ok(()) => {}, - Err(e) => eprintln!( - "Failed to serialize and persist worker state to hd: {:?}", - e - ), - } - } + $worker_static_name.with(|worker_instance| { + let mut worker_instance = worker_instance.borrow_mut(); + worker_instance.on_message(message, payload); + }); + } }; } diff --git a/zellij-tile/src/shim.rs b/zellij-tile/src/shim.rs index f1c1fb54d8..c43fc8db75 100644 --- a/zellij-tile/src/shim.rs +++ b/zellij-tile/src/shim.rs @@ -39,11 +39,31 @@ pub fn open_file(path: &Path) { unsafe { host_open_file() }; } +pub fn open_file_floating(path: &Path) { + object_to_stdout(&path); + unsafe { host_open_file_floating() }; +} + pub fn open_file_with_line(path: &Path, line: usize) { object_to_stdout(&(path, line)); unsafe { host_open_file_with_line() }; } +pub fn open_file_with_line_floating(path: &Path, line: usize) { + object_to_stdout(&(path, line)); + unsafe { host_open_file_with_line_floating() }; +} + +pub fn open_terminal(path: &Path) { + object_to_stdout(&path); + unsafe { host_open_terminal() }; +} + +pub fn open_terminal_floating(path: &Path) { + object_to_stdout(&path); + unsafe { host_open_terminal_floating() }; +} + pub fn switch_tab_to(tab_idx: u32) { unsafe { host_switch_tab_to(tab_idx) }; } @@ -56,6 +76,10 @@ pub fn exec_cmd(cmd: &[&str]) { unsafe { host_exec_cmd() }; } +pub fn hide_self() { + unsafe { host_hide_self() }; +} + pub fn report_panic(info: &std::panic::PanicInfo) { println!(""); println!("A panic occured in a plugin"); @@ -105,11 +129,16 @@ extern "C" { fn host_get_plugin_ids(); fn host_get_zellij_version(); fn host_open_file(); + fn host_open_file_floating(); fn host_open_file_with_line(); + fn host_open_file_with_line_floating(); + fn host_open_terminal(); + fn host_open_terminal_floating(); fn host_switch_tab_to(tab_idx: u32); fn host_set_timeout(secs: f64); fn host_exec_cmd(); fn host_report_panic(); fn host_post_message_to(); fn host_post_message_to_plugin(); + fn host_hide_self(); } diff --git a/zellij-utils/Cargo.toml b/zellij-utils/Cargo.toml index f12d8c78e9..2469715f62 100644 --- a/zellij-utils/Cargo.toml +++ b/zellij-utils/Cargo.toml @@ -40,6 +40,8 @@ tempfile = "3.2.0" kdl = { version = "4.5.0", features = ["span"] } shellexpand = "3.0.0" uuid = { version = "0.8.2", features = ["serde", "v4"] } +notify = "6.0.0" +async-channel = "1.8.0" #[cfg(not(target_family = "wasm"))] [target.'cfg(not(target_family = "wasm"))'.dependencies] diff --git a/zellij-utils/assets/plugins/compact-bar.wasm b/zellij-utils/assets/plugins/compact-bar.wasm index 3145fe1daa..e033de69a6 100755 Binary files a/zellij-utils/assets/plugins/compact-bar.wasm and b/zellij-utils/assets/plugins/compact-bar.wasm differ diff --git a/zellij-utils/assets/plugins/fixture-plugin-for-tests.wasm b/zellij-utils/assets/plugins/fixture-plugin-for-tests.wasm index a5421d7209..cf68f60a00 100755 Binary files a/zellij-utils/assets/plugins/fixture-plugin-for-tests.wasm and b/zellij-utils/assets/plugins/fixture-plugin-for-tests.wasm differ diff --git a/zellij-utils/assets/plugins/status-bar.wasm b/zellij-utils/assets/plugins/status-bar.wasm index 8f7ed7826a..317c839b02 100755 Binary files a/zellij-utils/assets/plugins/status-bar.wasm and b/zellij-utils/assets/plugins/status-bar.wasm differ diff --git a/zellij-utils/assets/plugins/strider.wasm b/zellij-utils/assets/plugins/strider.wasm index 929f5e8c74..51fcf72d78 100755 Binary files a/zellij-utils/assets/plugins/strider.wasm and b/zellij-utils/assets/plugins/strider.wasm differ diff --git a/zellij-utils/assets/plugins/tab-bar.wasm b/zellij-utils/assets/plugins/tab-bar.wasm index a8742f6766..f58671b9b5 100755 Binary files a/zellij-utils/assets/plugins/tab-bar.wasm and b/zellij-utils/assets/plugins/tab-bar.wasm differ diff --git a/zellij-utils/src/cli.rs b/zellij-utils/src/cli.rs index 14bcfae7a8..b222af0049 100644 --- a/zellij-utils/src/cli.rs +++ b/zellij-utils/src/cli.rs @@ -377,4 +377,9 @@ pub enum CliAction { StartOrReloadPlugin { url: Url, }, + LaunchOrFocusPlugin { + #[clap(short, long, value_parser)] + floating: bool, + url: Url, + }, } diff --git a/zellij-utils/src/data.rs b/zellij-utils/src/data.rs index 285532cc3c..d470c24890 100644 --- a/zellij-utils/src/data.rs +++ b/zellij-utils/src/data.rs @@ -3,6 +3,7 @@ use crate::input::config::ConversionError; use clap::ArgEnum; use serde::{Deserialize, Serialize}; use std::fmt; +use std::path::PathBuf; use std::str::FromStr; use strum_macros::{EnumDiscriminants, EnumIter, EnumString, ToString}; @@ -473,6 +474,10 @@ pub enum Event { String, // message String, // payload ), + FileSystemCreate(Vec), + FileSystemRead(Vec), + FileSystemUpdate(Vec), + FileSystemDelete(Vec), } /// Describes the different input modes, which change the way that keystrokes will be interpreted. diff --git a/zellij-utils/src/errors.rs b/zellij-utils/src/errors.rs index 74395e57a2..239cf5ab8c 100644 --- a/zellij-utils/src/errors.rs +++ b/zellij-utils/src/errors.rs @@ -333,6 +333,8 @@ pub enum ScreenContext { ProgressPluginLoadingOffset, StartPluginLoadingIndication, RequestStateUpdateForPlugins, + LaunchOrFocusPlugin, + SuppressPane, } /// 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 c288b8d2b4..36d59dde65 100644 --- a/zellij-utils/src/input/actions.rs +++ b/zellij-utils/src/input/actions.rs @@ -2,7 +2,7 @@ use super::command::RunCommandAction; use super::layout::{ - FloatingPaneLayout, Layout, RunPluginLocation, SwapFloatingLayout, SwapTiledLayout, + FloatingPaneLayout, Layout, RunPlugin, RunPluginLocation, SwapFloatingLayout, SwapTiledLayout, TiledPaneLayout, }; use crate::cli::CliAction; @@ -205,6 +205,7 @@ pub enum Action { LeftClick(Position), RightClick(Position), MiddleClick(Position), + LaunchOrFocusPlugin(RunPlugin, bool), // bool => should float LeftMouseRelease(Position), RightMouseRelease(Position), MiddleMouseRelease(Position), @@ -474,6 +475,15 @@ impl Action { CliAction::NextSwapLayout => Ok(vec![Action::NextSwapLayout]), CliAction::QueryTabNames => Ok(vec![Action::QueryTabNames]), CliAction::StartOrReloadPlugin { url } => Ok(vec![Action::StartOrReloadPlugin(url)]), + CliAction::LaunchOrFocusPlugin { url, floating } => { + let run_plugin_location = RunPluginLocation::parse(url.as_str()) + .map_err(|e| format!("Failed to parse plugin location: {}", e))?; + let run_plugin = RunPlugin { + location: run_plugin_location, + _allow_exec_host_cmd: false, + }; + Ok(vec![Action::LaunchOrFocusPlugin(run_plugin, floating)]) + }, } } } diff --git a/zellij-utils/src/input/command.rs b/zellij-utils/src/input/command.rs index 3d2a87ab3d..3212d26ec6 100644 --- a/zellij-utils/src/input/command.rs +++ b/zellij-utils/src/input/command.rs @@ -68,3 +68,16 @@ impl From for RunCommand { } } } + +impl RunCommand { + pub fn new(command: PathBuf) -> Self { + RunCommand { + command, + ..Default::default() + } + } + pub fn with_cwd(mut self, cwd: PathBuf) -> Self { + self.cwd = Some(cwd); + self + } +} diff --git a/zellij-utils/src/kdl/mod.rs b/zellij-utils/src/kdl/mod.rs index dceaaf36cb..58dda9c866 100644 --- a/zellij-utils/src/kdl/mod.rs +++ b/zellij-utils/src/kdl/mod.rs @@ -876,6 +876,29 @@ impl TryFrom<(&KdlNode, &Options)> for Action { }; Ok(Action::Run(run_command_action)) }, + "LaunchOrFocusPlugin" => { + let arguments = action_arguments.iter().copied(); + let mut args = kdl_arguments_that_are_strings(arguments)?; + if args.is_empty() { + return Err(ConfigError::new_kdl_error( + "No plugin found to launch in LaunchOrFocusPlugin".into(), + kdl_action.span().offset(), + kdl_action.span().len(), + )); + } + let plugin_path = args.remove(0); + + let command_metadata = action_children.iter().next(); + let should_float = command_metadata + .and_then(|c_m| kdl_child_bool_value_for_entry(c_m, "floating")) + .unwrap_or(false); + let location = RunPluginLocation::parse(&plugin_path)?; + let run_plugin = RunPlugin { + location, + _allow_exec_host_cmd: false, + }; + Ok(Action::LaunchOrFocusPlugin(run_plugin, should_float)) + }, "PreviousSwapLayout" => Ok(Action::PreviousSwapLayout), "NextSwapLayout" => Ok(Action::NextSwapLayout), _ => Err(ConfigError::new_kdl_error( diff --git a/zellij-utils/src/lib.rs b/zellij-utils/src/lib.rs index 2ee248060e..87637de553 100644 --- a/zellij-utils/src/lib.rs +++ b/zellij-utils/src/lib.rs @@ -20,6 +20,6 @@ pub mod logging; // Requires log4rs #[cfg(not(target_family = "wasm"))] pub use ::{ - anyhow, async_std, clap, interprocess, lazy_static, libc, miette, nix, regex, serde, - signal_hook, tempfile, termwiz, vte, + anyhow, async_channel, async_std, clap, interprocess, lazy_static, libc, miette, nix, notify, + regex, serde, signal_hook, tempfile, termwiz, vte, };