aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorAram Drevekenin <aram@poor.dev>2024-03-18 09:19:58 +0100
committerGitHub <noreply@github.com>2024-03-18 09:19:58 +0100
commitee16a4b8c347a81878389fd4f90144c2c6357f41 (patch)
treebf8675d57499b6278e8c89b84c8141969cc2ce3f
parentdocs(changelog): switch session cwd (diff)
downloadsl0ppij-ee16a4b8c347a81878389fd4f90144c2c6357f41.tar.gz
sl0ppij-ee16a4b8c347a81878389fd4f90144c2c6357f41.tar.xz
sl0ppij-ee16a4b8c347a81878389fd4f90144c2c6357f41.zip
feat(plugins): session manager cwd and new filepicker (#3200)
* prototype * folder selection ui in session manager * overhaul strider * scan folder host command * get strider to work from the cli and some cli pipe fixes * some ux improvements to strider * improve strider's ui * make strider ui responsive * make session-manager new ui parts responsive * fix tests * style(fmt): rustfmt
-rw-r--r--Cargo.lock5
-rw-r--r--default-plugins/fixture-plugin-for-tests/src/main.rs1
-rw-r--r--default-plugins/session-manager/Cargo.toml1
-rw-r--r--default-plugins/session-manager/src/main.rs56
-rw-r--r--default-plugins/session-manager/src/new_session_info.rs5
-rw-r--r--default-plugins/session-manager/src/ui/components.rs178
-rw-r--r--default-plugins/strider/.cargo/config.toml2
-rw-r--r--default-plugins/strider/src/file_list_view.rs191
-rw-r--r--default-plugins/strider/src/main.rs224
-rw-r--r--default-plugins/strider/src/search_view.rs134
-rw-r--r--default-plugins/strider/src/shared.rs156
-rw-r--r--default-plugins/strider/src/state.rs243
-rw-r--r--src/main.rs2
-rw-r--r--zellij-client/src/cli_client.rs52
-rw-r--r--zellij-client/src/os_input_output.rs17
-rw-r--r--zellij-server/src/plugins/mod.rs16
-rw-r--r--zellij-server/src/plugins/wasm_bridge.rs14
-rw-r--r--zellij-server/src/plugins/watch_filesystem.rs33
-rw-r--r--zellij-server/src/plugins/zellij_exports.rs81
-rw-r--r--zellij-tile/src/shim.rs18
-rw-r--r--zellij-utils/assets/prost/api.event.rs17
-rw-r--r--zellij-utils/assets/prost/api.plugin_command.rs10
-rw-r--r--zellij-utils/assets/prost/api.plugin_ids.rs2
-rw-r--r--zellij-utils/src/data.rs33
-rw-r--r--zellij-utils/src/errors.rs1
-rw-r--r--zellij-utils/src/input/actions.rs18
-rw-r--r--zellij-utils/src/plugin_api/event.proto9
-rw-r--r--zellij-utils/src/plugin_api/event.rs126
-rw-r--r--zellij-utils/src/plugin_api/plugin_command.proto3
-rw-r--r--zellij-utils/src/plugin_api/plugin_command.rs22
-rw-r--r--zellij-utils/src/plugin_api/plugin_ids.proto1
-rw-r--r--zellij-utils/src/plugin_api/plugin_ids.rs3
32 files changed, 1390 insertions, 284 deletions
diff --git a/Cargo.lock b/Cargo.lock
index b109ca64..279ee4c0 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -3338,6 +3338,7 @@ dependencies = [
"fuzzy-matcher",
"humantime",
"unicode-width",
+ "uuid",
"zellij-tile",
]
@@ -4191,9 +4192,9 @@ checksum = "936e4b492acfd135421d8dca4b1aa80a7bfc26e702ef3af710e0752684df5372"
[[package]]
name = "uuid"
-version = "1.4.1"
+version = "1.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "79daa5ed5740825c40b389c5e50312b9c86df53fccd33f281df655642b43869d"
+checksum = "f00cc9702ca12d3c81455259621e676d0f7251cec66a21e98fe2e9a37db93b2a"
dependencies = [
"getrandom 0.2.10",
"serde",
diff --git a/default-plugins/fixture-plugin-for-tests/src/main.rs b/default-plugins/fixture-plugin-for-tests/src/main.rs
index cd4fb45a..c241281a 100644
--- a/default-plugins/fixture-plugin-for-tests/src/main.rs
+++ b/default-plugins/fixture-plugin-for-tests/src/main.rs
@@ -67,6 +67,7 @@ impl ZellijPlugin for State {
EventType::FileSystemUpdate,
EventType::FileSystemDelete,
]);
+ watch_filesystem();
}
fn update(&mut self, event: Event) -> bool {
diff --git a/default-plugins/session-manager/Cargo.toml b/default-plugins/session-manager/Cargo.toml
index 97100ed6..deb8cec5 100644
--- a/default-plugins/session-manager/Cargo.toml
+++ b/default-plugins/session-manager/Cargo.toml
@@ -11,3 +11,4 @@ chrono = "0.4.0"
fuzzy-matcher = "0.3.7"
unicode-width = "0.1.10"
humantime = "2.1.0"
+uuid = { version = "1.7.0", features = ["v4"] }
diff --git a/default-plugins/session-manager/src/main.rs b/default-plugins/session-manager/src/main.rs
index 8040f79e..7015144f 100644
--- a/default-plugins/session-manager/src/main.rs
+++ b/default-plugins/session-manager/src/main.rs
@@ -2,9 +2,9 @@ mod new_session_info;
mod resurrectable_sessions;
mod session_list;
mod ui;
-use zellij_tile::prelude::*;
-
use std::collections::BTreeMap;
+use uuid::Uuid;
+use zellij_tile::prelude::*;
use new_session_info::NewSessionInfo;
use ui::{
@@ -45,6 +45,7 @@ struct State {
colors: Colors,
is_welcome_screen: bool,
show_kill_all_sessions_warning: bool,
+ request_ids: Vec<String>,
}
register_plugin!(State);
@@ -66,6 +67,28 @@ impl ZellijPlugin for State {
]);
}
+ fn pipe(&mut self, pipe_message: PipeMessage) -> bool {
+ if pipe_message.name == "filepicker_result" {
+ match (pipe_message.payload, pipe_message.args.get("request_id")) {
+ (Some(payload), Some(request_id)) => {
+ match self.request_ids.iter().position(|p| p == request_id) {
+ Some(request_id_position) => {
+ self.request_ids.remove(request_id_position);
+ let new_session_folder = std::path::PathBuf::from(payload);
+ self.new_session_info.new_session_folder = Some(new_session_folder);
+ },
+ None => {
+ eprintln!("request id not found");
+ },
+ }
+ },
+ _ => {},
+ }
+ true
+ } else {
+ false
+ }
+ }
fn update(&mut self, event: Event) -> bool {
let mut should_render = false;
match event {
@@ -109,7 +132,7 @@ impl ZellijPlugin for State {
render_new_session_block(
&self.new_session_info,
self.colors,
- height,
+ height.saturating_sub(2),
width,
x,
y + 2,
@@ -184,12 +207,33 @@ impl State {
} else if let Key::Ctrl('w') = key {
self.active_screen = ActiveScreen::NewSession;
should_render = true;
- } else if let Key::Ctrl('c') = key {
- self.new_session_info.handle_key(key);
- should_render = true;
} else if let Key::BackTab = key {
self.toggle_active_screen();
should_render = true;
+ } else if let Key::Ctrl('f') = key {
+ let request_id = Uuid::new_v4();
+ let mut config = BTreeMap::new();
+ let mut args = BTreeMap::new();
+ self.request_ids.push(request_id.to_string());
+ // we insert this into the config so that a new plugin will be opened (the plugin's
+ // uniqueness is determined by its name/url as well as its config)
+ config.insert("request_id".to_owned(), request_id.to_string());
+ // we also insert this into the args so that the plugin will have an easier access to
+ // it
+ args.insert("request_id".to_owned(), request_id.to_string());
+ pipe_message_to_plugin(
+ MessageToPlugin::new("filepicker")
+ .with_plugin_url("filepicker")
+ .with_plugin_config(config)
+ .new_plugin_instance_should_have_pane_title(
+ "Select folder for the new session...",
+ )
+ .with_args(args),
+ );
+ should_render = true;
+ } else if let Key::Ctrl('c') = key {
+ self.new_session_info.new_session_folder = None;
+ should_render = true;
} else if let Key::Esc = key {
self.new_session_info.handle_key(key);
should_render = true;
diff --git a/default-plugins/session-manager/src/new_session_info.rs b/default-plugins/session-manager/src/new_session_info.rs
index 294f4fbc..02e53966 100644
--- a/default-plugins/session-manager/src/new_session_info.rs
+++ b/default-plugins/session-manager/src/new_session_info.rs
@@ -1,6 +1,7 @@
use fuzzy_matcher::skim::SkimMatcherV2;
use fuzzy_matcher::FuzzyMatcher;
use std::cmp::Ordering;
+use std::path::PathBuf;
use zellij_tile::prelude::*;
#[derive(Default)]
@@ -8,6 +9,7 @@ pub struct NewSessionInfo {
name: String,
layout_list: LayoutList,
entering_new_session_info: EnteringState,
+ pub new_session_folder: Option<PathBuf>,
}
#[derive(Eq, PartialEq)]
@@ -104,7 +106,8 @@ impl NewSessionInfo {
if new_session_name != current_session_name.as_ref().map(|s| s.as_str()) {
match new_session_layout {
Some(new_session_layout) => {
- switch_session_with_layout(new_session_name, new_session_layout, None)
+ let cwd = self.new_session_folder.as_ref().map(|c| PathBuf::from(c));
+ switch_session_with_layout(new_session_name, new_session_layout, cwd)
},
None => {
switch_session(new_session_name);
diff --git a/default-plugins/session-manager/src/ui/components.rs b/default-plugins/session-manager/src/ui/components.rs
index d3c4afe8..d44fddf4 100644
--- a/default-plugins/session-manager/src/ui/components.rs
+++ b/default-plugins/session-manager/src/ui/components.rs
@@ -1,3 +1,4 @@
+use std::path::PathBuf;
use unicode_width::UnicodeWidthChar;
use unicode_width::UnicodeWidthStr;
use zellij_tile::prelude::*;
@@ -519,9 +520,6 @@ pub fn render_screen_toggle(active_screen: ActiveScreen, x: usize, y: usize, max
let key_indication_len = key_indication_text.chars().count() + 1;
let first_ribbon_length = new_session_text.chars().count() + 4;
let second_ribbon_length = running_sessions_text.chars().count() + 4;
- let third_ribbon_length = exited_sessions_text.chars().count() + 4;
- let total_len =
- key_indication_len + first_ribbon_length + second_ribbon_length + third_ribbon_length;
let key_indication_x = x;
let first_ribbon_x = key_indication_x + key_indication_len;
let second_ribbon_x = first_ribbon_x + first_ribbon_length;
@@ -552,6 +550,140 @@ pub fn render_screen_toggle(active_screen: ActiveScreen, x: usize, y: usize, max
print_ribbon_with_coordinates(exited_sessions_text, third_ribbon_x, y, None, None);
}
+fn render_new_session_folder_prompt(
+ new_session_info: &NewSessionInfo,
+ colors: Colors,
+ x: usize,
+ y: usize,
+ max_cols: usize,
+) {
+ match new_session_info.new_session_folder.as_ref() {
+ Some(new_session_folder) => {
+ let folder_prompt = "New session folder:";
+ let short_folder_prompt = "Folder:";
+ let new_session_path = new_session_folder.clone();
+ let new_session_folder = new_session_folder.display().to_string();
+ let change_folder_shortcut_text = "<Ctrl f>";
+ let change_folder_shortcut = colors.magenta(&change_folder_shortcut_text);
+ let to_change = "to change";
+ let reset_folder_shortcut_text = "<Ctrl c>";
+ let reset_folder_shortcut = colors.magenta(reset_folder_shortcut_text);
+ let to_reset = "to reset";
+ if max_cols
+ >= folder_prompt.width()
+ + new_session_folder.width()
+ + change_folder_shortcut_text.width()
+ + to_change.width()
+ + reset_folder_shortcut_text.width()
+ + to_reset.width()
+ + 8
+ {
+ print!(
+ "\u{1b}[m{}{} {} ({} {}, {} {})",
+ format!("\u{1b}[{};{}H", y + 1, x + 1),
+ colors.green(folder_prompt),
+ colors.orange(&new_session_folder),
+ change_folder_shortcut,
+ to_change,
+ reset_folder_shortcut,
+ to_reset,
+ );
+ } else if max_cols
+ >= short_folder_prompt.width()
+ + new_session_folder.width()
+ + change_folder_shortcut_text.width()
+ + to_change.width()
+ + reset_folder_shortcut_text.width()
+ + to_reset.width()
+ + 8
+ {
+ print!(
+ "\u{1b}[m{}{} {} ({} {}, {} {})",
+ format!("\u{1b}[{};{}H", y + 1, x + 1),
+ colors.green(short_folder_prompt),
+ colors.orange(&new_session_folder),
+ change_folder_shortcut,
+ to_change,
+ reset_folder_shortcut,
+ to_reset,
+ );
+ } else if max_cols
+ >= short_folder_prompt.width()
+ + new_session_folder.width()
+ + change_folder_shortcut_text.width()
+ + reset_folder_shortcut_text.width()
+ + 5
+ {
+ print!(
+ "\u{1b}[m{}{} {} ({}/{})",
+ format!("\u{1b}[{};{}H", y + 1, x + 1),
+ colors.green(short_folder_prompt),
+ colors.orange(&new_session_folder),
+ change_folder_shortcut,
+ reset_folder_shortcut,
+ );
+ } else {
+ let total_len = short_folder_prompt.width()
+ + change_folder_shortcut_text.width()
+ + reset_folder_shortcut_text.width()
+ + 5;
+ let max_path_len = max_cols.saturating_sub(total_len);
+ let truncated_path = truncate_path(
+ new_session_path,
+ new_session_folder.width().saturating_sub(max_path_len),
+ );
+ print!(
+ "\u{1b}[m{}{} {} ({}/{})",
+ format!("\u{1b}[{};{}H", y + 1, x + 1),
+ colors.green(short_folder_prompt),
+ colors.orange(&truncated_path),
+ change_folder_shortcut,
+ reset_folder_shortcut,
+ );
+ }
+ },
+ None => {
+ let folder_prompt = "New session folder:";
+ let short_folder_prompt = "Folder:";
+ let change_folder_shortcut_text = "<Ctrl f>";
+ let change_folder_shortcut = colors.magenta(change_folder_shortcut_text);
+ let to_set = "to set";
+
+ if max_cols
+ >= folder_prompt.width() + change_folder_shortcut_text.width() + to_set.width() + 4
+ {
+ print!(
+ "\u{1b}[m{}{} ({} {})",
+ format!("\u{1b}[{};{}H", y + 1, x + 1),
+ colors.green(folder_prompt),
+ change_folder_shortcut,
+ to_set,
+ );
+ } else if max_cols
+ >= short_folder_prompt.width()
+ + change_folder_shortcut_text.width()
+ + to_set.width()
+ + 4
+ {
+ print!(
+ "\u{1b}[m{}{} ({} {})",
+ format!("\u{1b}[{};{}H", y + 1, x + 1),
+ colors.green(short_folder_prompt),
+ change_folder_shortcut,
+ to_set,
+ );
+ } else {
+ print!(
+ "\u{1b}[m{}{} {}",
+ format!("\u{1b}[{};{}H", y + 1, x + 1),
+ colors.green(short_folder_prompt),
+ change_folder_shortcut,
+ );
+ }
+ },
+ }
+}
+
pub fn render_new_session_block(
new_session_info: &NewSessionInfo,
colors: Colors,
@@ -615,6 +747,7 @@ pub fn render_new_session_block(
}
render_layout_selection_list(
new_session_info,
+ colors,
max_rows_of_new_session_block.saturating_sub(1),
max_cols_of_new_session_block,
x,
@@ -625,6 +758,7 @@ pub fn render_new_session_block(
pub fn render_layout_selection_list(
new_session_info: &NewSessionInfo,
+ colors: Colors,
max_rows_of_new_session_block: usize,
max_cols_of_new_session_block: usize,
x: usize,
@@ -658,7 +792,7 @@ pub fn render_layout_selection_list(
let layout_name = layout_info.name();
let layout_name_len = layout_name.width();
let is_builtin = layout_info.is_builtin();
- if i > max_rows_of_new_session_block {
+ if i > max_rows_of_new_session_block.saturating_sub(1) {
break;
} else {
let mut layout_cell = if is_builtin {
@@ -682,7 +816,15 @@ pub fn render_layout_selection_list(
table = table.add_styled_row(vec![arrow_cell, layout_cell]);
}
}
- print_table_with_coordinates(table, x, y + 3, None, None);
+ let table_y = y + 3;
+ print_table_with_coordinates(table, x, table_y, None, None);
+ render_new_session_folder_prompt(
+ new_session_info,
+ colors,
+ x,
+ (y + max_rows_of_new_session_block).saturating_sub(3),
+ max_cols_of_new_session_block,
+ );
}
pub fn render_error(error_text: &str, rows: usize, columns: usize, x: usize, y: usize) {
@@ -733,10 +875,6 @@ pub fn render_controls_line(
}
},
ActiveScreen::AttachToSession => {
- let arrows = colors.magenta("<←↓↑→>");
- let navigate = colors.bold("Navigate");
- let enter = colors.magenta("<ENTER>");
- let select = colors.bold("Attach");
let rename = colors.magenta("<Ctrl r>");
let rename_text = colors.bold("Rename");
let disconnect = colors.magenta("<Ctrl x>");
@@ -817,3 +955,25 @@ impl Colors {
self.color(&self.palette.magenta, text)
}
}
+
+fn truncate_path(path: PathBuf, mut char_count_to_remove: usize) -> String {
+ let mut truncated = String::new();
+ let component_count = path.iter().count();
+ for (i, component) in path.iter().enumerate() {
+ let mut component_str = component.to_string_lossy().to_string();
+ if char_count_to_remove > 0 {
+ truncated.push(component_str.remove(0));
+ if i != 0 && i + 1 != component_count {
+ truncated.push('/');
+ }
+ char_count_to_remove =
+ char_count_to_remove.saturating_sub(component_str.width().saturating_sub(1));
+ } else {
+ truncated.push_str(&component_str);
+ if i != 0 && i + 1 != component_count {
+ truncated.push('/');
+ }
+ }
+ }
+ truncated
+}
diff --git a/default-plugins/strider/.cargo/config.toml b/default-plugins/strider/.cargo/config.toml
index bc255e30..6b77899c 100644
--- a/default-plugins/strider/.cargo/config.toml
+++ b/default-plugins/strider/.cargo/config.toml
@@ -1,2 +1,2 @@
[build]
-target = "wasm32-wasi" \ No newline at end of file
+target = "wasm32-wasi"
diff --git a/default-plugins/strider/src/file_list_view.rs b/default-plugins/strider/src/file_list_view.rs
new file mode 100644
index 00000000..e16c66b3
--- /dev/null
+++ b/default-plugins/strider/src/file_list_view.rs
@@ -0,0 +1,191 @@
+use crate::shared::{calculate_list_bounds, render_list_tip};
+use crate::state::{refresh_directory, ROOT};
+use pretty_bytes::converter::convert as pretty_bytes;
+use std::collections::HashMap;
+use std::path::PathBuf;
+use unicode_width::UnicodeWidthStr;
+use zellij_tile::prelude::*;
+
+#[derive(Debug, Clone)]
+pub struct FileListView {
+ pub path: PathBuf,
+ pub path_is_dir: bool,
+ pub files: Vec<FsEntry>,
+ pub cursor_hist: HashMap<PathBuf, usize>,
+}
+
+impl Default for FileListView {
+ fn default() -> Self {
+ FileListView {
+ path_is_dir: true,
+ path: Default::default(),
+ files: Default::default(),
+ cursor_hist: Default::default(),
+ }
+ }
+}
+
+impl FileListView {
+ pub fn descend_to_previous_path(&mut self) {
+ self.path.pop();
+ self.path_is_dir = true;
+ self.files.clear();
+ self.reset_selected();
+ refresh_directory(&self.path);
+ }
+ pub fn descend_to_root_path(&mut self) {
+ self.path.clear();
+ self.path_is_dir = true;
+ self.files.clear();
+ self.reset_selected();
+ refresh_directory(&self.path);
+ }
+ pub fn enter_dir(&mut self, entry: &FsEntry) {
+ let is_dir = entry.is_folder();
+ let path = entry.get_pathbuf_without_root_prefix();
+ self.path = path;
+ self.path_is_dir = is_dir;
+ self.files.clear();
+ self.reset_selected();
+ }
+ pub fn reset_selected(&mut self) {
+ *self.selected_mut() = self.selected().unwrap_or(0);
+ }
+ pub fn update_files(
+ &mut self,
+ paths: Vec<(PathBuf, Option<FileMetadata>)>,
+ hide_hidden_files: bool,
+ ) {
+ let mut files = vec![];
+ for (entry, entry_metadata) in paths {
+ if entry_metadata.map(|e| e.is_symlink).unwrap_or(false) {
+ continue; // ignore symlinks
+ }
+ let entry = if entry_metadata.map(|e| e.is_dir).unwrap_or(false) {
+ FsEntry::Dir(entry)
+ } else {
+ let size = entry_metadata.map(|e| e.len).unwrap_or(0);
+ FsEntry::File(entry, size)
+ };
+ if !entry.is_hidden_file() || !hide_hidden_files {
+ files.push(entry);
+ }
+ }
+ self.files = files;
+ self.files.sort_unstable();
+ }
+ pub fn get_selected_entry(&self) -> Option<FsEntry> {
+ self.selected().and_then(|f| self.files.get(f).cloned())
+ }
+ pub fn selected_mut(&mut self) -> &mut usize {
+ self.cursor_hist.entry(self.path.clone()).or_default()
+ }
+ pub fn selected(&self) -> Option<usize> {
+ self.cursor_hist.get(&self.path).copied()
+ }
+ pub fn move_selection_up(&mut self) {
+ if let Some(selected) = self.selected() {
+ *self.selected_mut() = selected.saturating_sub(1);
+ }
+ }
+ pub fn move_selection_down(&mut self) {
+ if let Some(selected) = self.selected() {
+ let next = selected.saturating_add(1);
+ *self.selected_mut() = std::cmp::min(self.files.len().saturating_sub(1), next);
+ } else {
+ *self.selected_mut() = 0;
+ }
+ }
+ pub fn render(&mut self, rows: usize, cols: usize) {
+ let (start_index, selected_index_in_range, end_index) =
+ calculate_list_bounds(self.files.len(), rows.saturating_sub(1), self.selected());
+
+ render_list_tip(3, cols);
+ for i in start_index..end_index {
+ if let Some(entry) = self.files.get(i) {
+ let is_selected = Some(i) == selected_index_in_range;
+ let mut file_or_folder_name = entry.name();
+ let size = entry
+ .size()
+ .map(|s| pretty_bytes(s as f64))
+ .unwrap_or("".to_owned());
+ if entry.is_folder() {
+ file_or_folder_name.push('/');
+ }
+ let file_or_folder_name_width = file_or_folder_name.width();
+ let size_width = size.width();
+ let text = if file_or_folder_name_width + size_width < cols {
+ let padding = " ".repeat(
+ cols.saturating_sub(file_or_folder_name_width)
+ .saturating_sub(size_width),
+ );
+ format!("{}{}{}", file_or_folder_name, padding, size)
+ } else {
+ // drop the size, no room for it
+ let padding = " ".repeat(cols.saturating_sub(file_or_folder_name_width));
+ format!("{}{}", file_or_folder_name, padding)
+ };
+ let mut text_element = if is_selected {
+ Text::new(text).selected()
+ } else {
+ Text::new(text)
+ };
+ if entry.is_folder() {
+ text_element = text_element.color_range(0, ..);
+ }
+ print_text_with_coordinates(
+ text_element,
+ 0,
+ 4 + i.saturating_sub(start_index),
+ Some(cols),
+ None,
+ );
+ }
+ }
+ }
+}
+
+#[derive(PartialEq, Eq, PartialOrd, Ord, Clone, Debug)]
+pub enum FsEntry {
+ Dir(PathBuf),
+ File(PathBuf, u64),
+}
+
+impl FsEntry {
+ pub fn name(&self) -> String {
+ let path = match self {
+ FsEntry::Dir(p) => p,
+ FsEntry::File(p, _) => p,
+ };
+ path.file_name().unwrap().to_string_lossy().into_owned()
+ }
+ pub fn size(&self) -> Option<u64> {
+ match self {
+ FsEntry::Dir(p) => None,
+ FsEntry::File(_, size) => Some(*size),
+ }
+ }
+ pub fn get_pathbuf_without_root_prefix(&self) -> PathBuf {
+ match self {
+ FsEntry::Dir(p) => p
+ .strip_prefix(ROOT)
+ .map(|p| p.to_path_buf())
+ .unwrap_or_else(|_| p.clone()),
+ FsEntry::File(p, _) => p
+ .strip_prefix(ROOT)
+ .map(|p| p.to_path_buf())
+ .unwrap_or_else(|_| p.clone()),
+ }
+ }
+
+ pub fn is_hidden_file(&self) -> bool {
+ self.name().starts_with('.')
+ }
+
+ pub fn is_folder(&self) -> bool {
+ match self {
+ FsEntry::Dir(_) => true,
+ _ => false,
+ }
+ }
+}
diff --git a/default-plugins/strider/src/main.rs b/default-plugins/strider/src/main.rs
index 1ad2e70c..ce9a5273 100644
--- a/default-plugins/strider/src/main.rs
+++ b/default-plugins/strider/src/main.rs
@@ -1,121 +1,122 @@
+mod file_list_view;
+mod search_view;
+mod shared;
mod state;
-use colored::*;
-use state::{refresh_directory, FsEntry, State};
+use crate::file_list_view::FsEntry;
+use shared::{render_current_path, render_instruction_line, render_search_term};
+use state::{refresh_directory, State};
use std::collections::BTreeMap;
-use std::{cmp::min, time::Instant};
+use std::path::PathBuf;
use zellij_tile::prelude::*;
register_plugin!(State);
impl ZellijPlugin for State {
- fn load(&mut self, _configuration: BTreeMap<String, String>) {
- refresh_directory(self);
+ fn load(&mut self, configuration: BTreeMap<String, String>) {
+ let plugin_ids = get_plugin_ids();
+ self.initial_cwd = plugin_ids.initial_cwd;
+ let show_hidden_files = configuration
+ .get("show_hidden_files")
+ .map(|v| v == "true")
+ .unwrap_or(false);
+ self.hide_hidden_files = !show_hidden_files;
+ self.close_on_selection = configuration
+ .get("close_on_selection")
+ .map(|v| v == "true")
+ .unwrap_or(false);
subscribe(&[
EventType::Key,
EventType::Mouse,
EventType::CustomMessage,
EventType::Timer,
+ EventType::FileSystemUpdate,
]);
+ self.file_list_view.reset_selected();
+ // the caller_cwd might be different from the initial_cwd if this plugin was defined as an
+ // alias, with access to a certain part of the file system (often broader) and was called
+ // from an individual pane somewhere inside this broad scope - in this case, we want to
+ // start in the same cwd as the caller, giving them the full access we were granted
+ match configuration
+ .get("caller_cwd")
+ .map(|c| PathBuf::from(c))
+ .and_then(|c| {
+ c.strip_prefix(&self.initial_cwd)
+ .ok()
+ .map(|c| PathBuf::from(c))
+ }) {
+ Some(relative_caller_path) => {
+ let relative_caller_path = FsEntry::Dir(relative_caller_path.to_path_buf());
+ self.file_list_view.enter_dir(&relative_caller_path);
+ refresh_directory(&self.file_list_view.path);
+ },
+ None => {
+ refresh_directory(&std::path::Path::new("/"));
+ },
+ }
}
fn update(&mut self, event: Event) -> bool {
let mut should_render = false;
- let prev_event = if self.ev_history.len() == 2 {
- self.ev_history.pop_front()
- } else {
- None
- };
- self.ev_history.push_back((event.clone(), Instant::now()));
match event {
+ Event::FileSystemUpdate(paths) => {
+ self.update_files(paths);
+ should_render = true;
+ },
Event::Key(key) => match key {
- Key::Esc => {
- hide_self();
+ Key::Char(character) if character != '\n' => {
+ self.update_search_term(character);
should_render = true;
},
- Key::Up | Key::Char('k') => {
- let currently_selected = self.selected();
- *self.selected_mut() = self.selected().saturating_sub(1);
- if currently_selected != self.selected() {
- should_render = true;
- }
+ Key::Backspace => {
+ self.handle_backspace();
+ should_render = true;
},
- Key::Down | Key::Char('j') => {
- let currently_selected = self.selected();
- let next = self.selected().saturating_add(1);
- if next >= self.files.len() {
- refresh_directory(self);
- }
- *self.selected_mut() = min(self.files.len().saturating_sub(1), next);
- if currently_selected != self.selected() {
- should_render = true;
- }
+ Key::Esc | Key::Ctrl('c') => {
+ self.clear_search_term_or_descend();
+ should_render = true;
},
- Key::Right | Key::Char('\n') | Key::Char('l') if !self.files.is_empty() => {
- self.traverse_dir_or_open_file();
- self.ev_history.clear();
+ Key::Up => {
+ self.move_selection_up();
should_render = true;
},
- Key::Left | Key::Char('h') => {
- if self.path.components().count() > 2 {
- // don't descend into /host
- // the reason this is a hard-coded number (2) and not "== ROOT"
- // or some such is that there are certain cases in which self.path
- // is empty and this will work then too
- should_render = true;
- self.path.pop();
- refresh_directory(self);
- }
+ Key::Down => {
+ self.move_selection_down();
+ should_render = true;
+ },
+ Key::Char('\n') if self.handling_filepick_request_from.is_some() => {
+ self.send_filepick_response();
+ },
+ Key::Char('\n') => {
+ self.open_selected_path();
+ },
+ Key::Right | Key::BackTab => {
+ self.traverse_dir();
+ should_render = true;
+ },
+ Key::Left => {
+ self.descend_to_previous_path();
+ should_render = true;
},
- Key::Char('.') => {
+ Key::Ctrl('e') => {
should_render = true;
self.toggle_hidden_files();
- refresh_directory(self);
+ refresh_directory(&self.file_list_view.path);
},
-
_ => (),
},
Event::Mouse(mouse_event) => match mouse_event {
Mouse::ScrollDown(_) => {
- let currently_selected = self.selected();
- let next = self.selected().saturating_add(1);
- if next >= self.files.len() {
- refresh_directory(self);
- }
- *self.selected_mut() = min(self.files.len().saturating_sub(1), next);
- if currently_selected != self.selected() {
- should_render = true;
- }
+ self.move_selection_down();
+ should_render = true;
},
Mouse::ScrollUp(_) => {
- let currently_selected = self.selected();
- *self.selected_mut() = self.selected().saturating_sub(1);
- if currently_selected != self.selected() {
- should_render = true;
- }
+ self.move_selection_up();
+ should_render = true;
},
- Mouse::Release(line, _) => {
- if line < 0 {
- return should_render;
- }
- let mut should_select = true;
- if let Some((Event::Mouse(Mouse::Release(prev_line, _)), t)) = prev_event {
- if prev_line == line
- && Instant::now().saturating_duration_since(t).as_millis() < 400
- {
- self.traverse_dir_or_open_file();
- self.ev_history.clear();
- should_select = false;
- should_render = true;
- }
- }
- if should_select && self.scroll() + (line as usize) < self.files.len() {
- let currently_selected = self.selected();
- *self.selected_mut() = self.scroll() + (line as usize);
- if currently_selected != self.selected() {
- should_render = true;
- }
- }
+ Mouse::LeftClick(line, _) => {
+ self.handle_left_click(line);
+ should_render = true;
},
_ => {},
},
@@ -125,42 +126,37 @@ impl ZellijPlugin for State {
};
should_render
}
+ fn pipe(&mut self, pipe_message: PipeMessage) -> bool {
+ if pipe_message.is_private && pipe_message.name == "filepicker" {
+ if let PipeSource::Cli(pipe_id) = &pipe_message.source {
+ // here we block the cli pipe input because we want it to wait until the user chose
+ // a file
+ #[cfg(target_family = "wasm")]
+ block_cli_pipe_input(pipe_id);
+ }
+ self.handling_filepick_request_from = Some((pipe_message.source, pipe_message.args));
+ true
+ } else {
+ false
+ }
+ }
fn render(&mut self, rows: usize, cols: usize) {
self.current_rows = Some(rows);
- for i in 0..rows {
- if self.selected() < self.scroll() {
- *self.scroll_mut() = self.selected();
- }
- if self.selected() - self.scroll() + 2 > rows {
- *self.scroll_mut() = self.selected() + 2 - rows;
- }
-
- let is_last_row = i == rows.saturating_sub(1);
- let i = self.scroll() + i;
- if let Some(entry) = self.files.get(i) {
- let mut path = entry.as_line(cols).normal();
-
- if let FsEntry::Dir(..) = entry {
- path = path.dimmed().bold();
- }
-
- if i == self.selected() {
- if is_last_row {
- print!("{}", path.clone().reversed());
- } else {
- println!("{}", path.clone().reversed());
- }
- } else {
- if is_last_row {
- print!("{}", path);
- } else {
- println!("{}", path);
- }
- }
- } else if !is_last_row {
- println!();
- }
+ let rows_for_list = rows.saturating_sub(6);
+ render_search_term(&self.search_term);
+ render_current_path(
+ &self.initial_cwd,
+ &self.file_list_view.path,
+ self.file_list_view.path_is_dir,
+ self.handling_filepick_request_from.is_some(),
+ cols,
+ );
+ if self.is_searching {
+ self.search_view.render(rows_for_list, cols);
+ } else {
+ self.file_list_view.render(rows_for_list, cols);
}
+ render_instruction_line(rows, cols);
}
}
diff --git a/default-plugins/strider/src/search_view.rs b/default-plugins/strider/src/search_view.rs
new file mode 100644
index 00000000..bcc38bc8
--- /dev/null
+++ b/default-plugins/strider/src/search_view.rs
@@ -0,0 +1,134 @@
+use crate::shared::{calculate_list_bounds, render_list_tip};
+use fuzzy_matcher::skim::SkimMatcherV2;
+use fuzzy_matcher::FuzzyMatcher;
+use pretty_bytes::converter::convert as pretty_bytes;
+use unicode_width::UnicodeWidthStr;
+use zellij_tile::prelude::*;
+
+use crate::file_list_view::FsEntry;
+
+#[derive(Default, Debug)]
+pub struct SearchView {
+ pub search_results: Vec<SearchResult>,
+ pub selected_search_result: usize,
+}
+
+impl SearchView {
+ pub fn search_result_count(&self) -> usize {
+ self.search_results.len()
+ }
+ pub fn update_search_results(&mut self, search_term: &str, files: &Vec<FsEntry>) {
+ self.selected_search_result = 0;
+ if search_term.is_empty() {
+ self.search_results.clear();
+ } else {
+ let mut matches = vec![];
+ let matcher = SkimMatcherV2::default().use_cache(true);
+ for file in files {
+ let name = file.name();
+ if let Some((score, indices)) = matcher.fuzzy_indices(&name, search_term) {
+ matches.push(SearchResult::new(file.clone(), score, indices));
+ }
+ }
+ matches.sort_by(|a, b| b.score.cmp(&a.score));
+ self.search_results = matches;
+ }
+ }
+ pub fn clear_and_reset_selection(&mut self) {
+ self.search_results.clear();
+ self.selected_search_result = 0;
+ }
+ pub fn move_selection_up(&mut self) {
+ self.selected_search_result = self.selected_search_result.saturating_sub(1);
+ }
+ pub fn move_selection_down(&mut self) {
+ if self.selected_search_result + 1 < self.search_results.len() {
+ self.selected_search_result += 1;
+ }
+ }
+ pub fn get_selected_entry(&self) -> Option<FsEntry> {
+ self.search_results
+ .get(self.selected_search_result)
+ .map(|s| s.entry.clone())
+ }
+ pub fn render(&mut self, rows: usize, cols: usize) {
+ let (start_index, selected_index_in_range, end_index) = calculate_list_bounds(
+ self.search_results.len(),
+ rows.saturating_sub(1),
+ Some(self.selected_search_result),
+ );
+ render_list_tip(3, cols);
+ for i in start_index..end_index {
+ if let Some(search_result) = self.search_results.get(i) {
+ let is_selected = Some(i) == selected_index_in_range;
+ let mut search_result_text = search_result.name();
+ let size = search_result
+ .size()
+ .map(|s| pretty_bytes(s as f64))
+ .unwrap_or("".to_owned());
+ if search_result.is_folder() {
+ search_result_text.push('/');
+ }
+
+ let search_result_text_width = search_result_text.width();
+ let size_width = size.width();
+ let text = if search_result_text_width + size_width < cols {
+ let padding = " ".repeat(
+ cols.saturating_sub(search_result_text_width)
+ .saturating_sub(size_width),
+ );
+ format!("{}{}{}", search_result_text, padding, size)
+ } else {
+ // drop the size, no room for it
+ let padding = " ".repeat(cols.saturating_sub(search_result_text_width));
+ format!("{}{}", search_result_text, padding)
+ };
+ let mut text_element = if is_selected {
+ Text::new(text).selected()
+ } else {
+ Text::new(text)
+ };
+ if search_result.is_folder() {
+ text_element = text_element.color_range(0, ..);
+ }
+ text_element = text_element.color_indices(3, search_result.indices());
+ print_text_with_coordinates(
+ text_element,
+ 0,
+ i.saturating_sub(start_index) + 4,
+ Some(cols),
+ None,
+ );
+ }
+ }
+ }
+}
+
+#[derive(Debug)]
+pub struct SearchResult {
+ pub entry: FsEntry,
+ pub score: i64,
+ pub indices: Vec<usize>,
+}
+
+impl SearchResult {
+ pub fn new(entry: FsEntry, score: i64, indices: Vec<usize>) -> Self {
+ SearchResult {
+ entry,
+ score,
+ indices,
+ }
+ }
+ pub fn name(&self) -> String {
+ self.entry.name()
+ }
+ pub fn size(&self) -> Option<u64> {
+ self.entry.size()
+ }
+ pub fn indices(&self) -> Vec<usize> {
+ self.indices.clone()
+ }
+ pub fn is_folder(&self) -> bool {
+ self.entry.is_folder()
+ }
+}
diff --git a/default-plugins/strider/src/shared.rs b/default-plugins/strider/src/shared.rs
new file mode 100644
index 00000000..c51cb312
--- /dev/null
+++ b/default-plugins/strider/src/shared.rs
@@ -0,0 +1,156 @@
+use std::path::PathBuf;
+use unicode_width::UnicodeWidthStr;
+use zellij_tile::prelude::*;
+
+pub fn render_instruction_line(y: usize, max_cols: usize) {
+ if max_cols > 78 {
+ let text = "Help: go back with <Ctrl c>, go to root with /, <Ctrl e> - toggle hidden files";
+ let text = Text::new(text)
+ .color_range(3, 19..27)
+ .color_range(3, 45..46)
+ .color_range(3, 48..56);
+ print_text_with_coordinates(text, 0, y, Some(max_cols), None);
+ } else if max_cols > 56 {
+ let text = "Help: <Ctrl c> - back, / - root, <Ctrl e> - hidden files";
+ let text = Text::new(text)
+ .color_range(3, 6..14)
+ .color_range(3, 23..24)
+ .color_range(3, 33..41);
+ print_text_with_coordinates(text, 0, y, Some(max_cols), None);
+ } else if max_cols > 25 {
+ let text = "<Ctrl c> - back, / - root";
+ let text = Text::new(text).color_range(3, ..8).color_range(3, 17..18);
+ print_text_with_coordinates(text, 0, y, Some(max_cols), None);
+ }
+}
+
+pub fn render_list_tip(y: usize, max_cols: usize) {
+ let tip = Text::new(format!("(<↓↑> - Navigate, <TAB> - Select)"))
+ .color_range(3, 1..5)
+ .color_range(3, 18..23);
+ print_text_with_coordinates(tip, 0, y, Some(max_cols), None);
+}
+
+// returns the list (start_index, selected_index_in_range, end_index)
+pub fn calculate_list_bounds(
+ result_count: usize,
+ max_result_count: usize,
+ selected_index_in_all_results: Option<usize>,
+) -> (usize, Option<usize>, usize) {
+ match selected_index_in_all_results {
+ Some(selected_index_in_all_results) => {
+ let mut room_in_list = max_result_count;
+ let mut start_index = selected_index_in_all_results;
+ let mut end_index = selected_index_in_all_results + 1;
+ let mut alternate = false;
+ loop {
+ if room_in_list == 0 {
+ break;
+ }
+ if !alternate && start_index > 0 {
+ start_index -= 1;
+ room_in_list -= 1;
+ } else if alternate && end_index < result_count {
+ end_index += 1;
+ room_in_list -= 1;
+ } else if start_index > 0 {
+ start_index -= 1;
+ room_in_list -= 1;
+ } else if end_index < result_count {
+ end_index += 1;
+ room_in_list -= 1;
+ } else {
+ break;
+ }
+ alternate = !alternate;
+ }
+ (start_index, Some(selected_index_in_all_results), end_index)
+ },
+ None => (0, None, max_result_count + 1),
+ }
+}
+
+pub fn render_search_term(search_term: &str) {
+ let prompt = "FIND: ";
+ let text = Text::new(format!("{}{}_", prompt, search_term))
+ .color_range(2, 0..prompt.len())
+ .color_range(3, prompt.len()..);
+ print_text(text);
+ println!("")
+}
+
+pub fn render_current_path(
+ initial_cwd: &PathBuf,
+ path: &PathBuf,
+ path_is_dir: bool,
+ handling_filepick: bool,
+ max_cols: usize,
+) {
+ let prompt = "PATH: ";
+ let current_path = initial_cwd.join(path);
+ let current_path = current_path.display().to_string();
+ let prompt_len = prompt.width();
+ let current_path_len = current_path.width();
+
+ let enter_tip = if handling_filepick {
+ "Select"
+ } else if path_is_dir {
+ "Open terminal here"
+ } else {
+ "Open in editor"
+ };
+ if max_cols > prompt_len + current_path_len + enter_tip.width() + 13 {
+ let path_end = prompt_len + current_path_len;
+ let current_path = Text::new(format!(
+ "{}{} (<ENTER> - {})",
+ prompt, current_path, enter_tip
+ ))
+ .color_range(2, 0..prompt_len)
+ .color_range(0, prompt_len..path_end)
+ .color_range(3, path_end + 2..path_end + 9);
+ print_text(current_path);
+ } else {
+ let max_path_len = max_cols
+ .saturating_sub(prompt_len)
+ .saturating_sub(8)
+ .saturating_sub(prompt_len);
+ let current_path = if current_path_len <= max_path_len {
+ current_path
+ } else {
+ truncate_path(
+ initial_cwd.join(path),
+ current_path_len.saturating_sub(max_path_len),
+ )
+ };
+ let current_path_len = current_path.width();
+ let path_end = prompt_len + current_path_len;
+ let current_path = Text::new(format!("{}{} <ENTER>", prompt, current_path))
+ .color_range(2, 0..prompt_len)
+ .color_range(0, prompt_len..path_end)
+ .color_range(3, path_end + 1..path_end + 9);
+ print_text(current_path);
+ }
+ println!();
+ println!();
+}
+
+fn truncate_path(path: PathBuf, mut char_count_to_remove: usize) -> String {
+ let mut truncated = String::new();
+ let component_count = path.iter().count();
+ for (i, component) in path.iter().enumerate() {
+ let mut component_str = component.to_string_lossy().to_string();
+ if char_count_to_remove > 0 {
+ truncated.push(component_str.remove(0));
+ if i != 0 && i + 1 != component_count {
+ truncated.push('/');
+ }
+ char_count_to_remove = char_count_to_remove.saturating_sub(component_str.width() + 1);
+ } else {
+ truncated.push_str(&component_str);
+ if i != 0 && i + 1 != component_count {
+ truncated.push('/');
+ }
+ }
+ }
+ truncated
+}
diff --git a/default-plugins/strider/src/state.rs b/default-plugins/strider/src/state.rs
index 230e92ea..1ff58386 100644
--- a/default-plugins/strider/src/state.rs
+++ b/default-plugins/strider/src/state.rs
@@ -1,9 +1,9 @@
-use pretty_bytes::converter as pb;
+use crate::file_list_view::{FileListView, FsEntry};
+use crate::search_view::SearchView;
+use crate::shared::calculate_list_bounds;
use std::{
- collections::{HashMap, VecDeque},
- fs::read_dir,
+ collections::BTreeMap,
path::{Path, PathBuf},
- time::Instant,
};
use zellij_tile::prelude::*;
@@ -11,106 +11,189 @@ pub const ROOT: &str = "/host";
#[derive(Default)]
pub struct State {
- pub path: PathBuf,
- pub files: Vec<FsEntry>,
- pub cursor_hist: HashMap<PathBuf, (usize, usize)>,
+ pub file_list_view: FileListView,
+ pub search_view: SearchView,
pub hide_hidden_files: bool,
- pub ev_history: VecDeque<(Event, Instant)>, // stores last event, can be expanded in future
pub loading: bool,
pub loading_animation_offset: u8,
pub should_open_floating: bool,
pub current_rows: Option<usize>,
+ pub handling_filepick_request_from: Option<(PipeSource, BTreeMap<String, String>)>,
+ pub initial_cwd: PathBuf, // TODO: get this from zellij
+ pub is_searching: bool,
+ pub search_term: String,
+ pub close_on_selection: bool,
}
impl State {
- pub fn selected_mut(&mut self) -> &mut usize {
- &mut self.cursor_hist.entry(self.path.clone()).or_default().0
+ pub fn update_search_term(&mut self, character: char) {
+ self.search_term.push(character);
+ if &self.search_term == ".." {
+ self.descend_to_previous_path();
+ } else if &self.search_term == "/" {
+ self.descend_to_root_path();
+ } else {
+ self.is_searching = true;
+ self.search_view
+ .update_search_results(&self.search_term, &self.file_list_view.files);
+ }
}
- pub fn selected(&self) -> usize {
- self.cursor_hist.get(&self.path).unwrap_or(&(0, 0)).0
+ pub fn handle_backspace(&mut self) {
+ if self.search_term.is_empty() {
+ self.descend_to_previous_path();
+ } else {
+ self.search_term.pop();
+ if self.search_term.is_empty() {
+ self.is_searching = false;
+ }
+ self.search_view
+ .update_search_results(&self.search_term, &self.file_list_view.files);
+ }
}
- pub fn scroll_mut(&mut self) -> &mut usize {
- &mut self.cursor_hist.entry(self.path.clone()).or_default().1
+ pub fn clear_search_term_or_descend(&mut self) {
+ if self.search_term.is_empty() {
+ self.descend_to_previous_path();
+ } else {
+ self.search_term.clear();
+ self.search_view
+ .update_search_results(&self.search_term, &self.file_list_view.files);
+ self.is_searching = false;
+ }
}
- pub fn scroll(&self) -> usize {
- self.cursor_hist.get(&self.path).unwrap_or(&(0, 0)).1
+ pub fn move_selection_up(&mut self) {
+ if self.is_searching {
+ self.search_view.move_selection_up();
+ } else {
+ self.file_list_view.move_selection_up();
+ }
+ }
+ pub fn move_selection_down(&mut self) {
+ if self.is_searching {
+ self.search_view.move_selection_down();
+ } else {
+ self.file_list_view.move_selection_down();
+ }
+ }
+ pub fn handle_left_click(&mut self, line: isize) {
+ if let Some(current_rows) = self.current_rows {
+ let rows_for_list = current_rows.saturating_sub(5);
+ if self.is_searching {
+ let (start_index, _selected_index_in_range, _end_index) = calculate_list_bounds(
+ self.search_view.search_result_count(),
+ rows_for_list,
+ Some(self.search_view.selected_search_result),
+ );
+ let prev_selected = self.search_view.selected_search_result;
+ self.search_view.selected_search_result =
+ (line as usize).saturating_sub(2) + start_index;
+ if prev_selected == self.search_view.selected_search_result {
+ self.traverse_dir();
+ }
+ } else {
+ let (start_index, _selected_index_in_range, _end_index) = calculate_list_bounds(
+ self.file_list_view.files.len(),
+ rows_for_list,
+ self.file_list_view.selected(),
+ );
+ let prev_selected = self.file_list_view.selected();
+ *self.file_list_view.selected_mut() =
+ (line as usize).saturating_sub(2) + start_index;
+ if prev_selected == self.file_list_view.selected() {
+ self.traverse_dir();
+ }
+ }
+ }
+ }
+ pub fn descend_to_previous_path(&mut self) {
+ self.search_term.clear();
+ self.search_view.clear_and_reset_selection();
+ self.file_list_view.descend_to_previous_path();
+ }
+ pub fn descend_to_root_path(&mut self) {
+ self.search_term.clear();
+ self.search_view.clear_and_reset_selection();
+ self.file_list_view.descend_to_root_path();
+ refresh_directory(&self.file_list_view.path);
}
pub fn toggle_hidden_files(&mut self) {
self.hide_hidden_files = !self.hide_hidden_files;
}
- pub fn traverse_dir_or_open_file(&mut self) {
- if let Some(f) = self.files.get(self.selected()) {
- match f.clone() {
- FsEntry::Dir(p) => {
- self.path = p;
- refresh_directory(self);
+ pub fn traverse_dir(&mut self) {
+ let entry = if self.is_searching {
+ self.search_view.get_selected_entry()
+ } else {
+ self.file_list_view.get_selected_entry()
+ };
+ if let Some(entry) = entry {
+ match &entry {
+ FsEntry::Dir(_p) => {
+ self.file_list_view.enter_dir(&entry);
+ self.search_view.clear_and_reset_selection();
+ refresh_directory(&self.file_list_view.path);
+ },
+ FsEntry::File(_p, _) => {
+ self.file_list_view.enter_dir(&entry);
+ self.search_view.clear_and_reset_selection();
},
- FsEntry::File(p, _) => open_file(FileToOpen {
- path: p.strip_prefix(ROOT).unwrap().into(),
- ..Default::default()
- }),
}
}
+ self.is_searching = false;
+ self.search_term.clear();
+ self.search_view.clear_and_reset_selection();
}
-}
-
-#[derive(PartialEq, Eq, PartialOrd, Ord, Clone)]
-pub enum FsEntry {
- Dir(PathBuf),
- File(PathBuf, u64),
-}
-
-impl FsEntry {
- pub fn name(&self) -> String {
- let path = match self {
- FsEntry::Dir(p) => p,
- FsEntry::File(p, _) => p,
- };
- path.file_name().unwrap().to_string_lossy().into_owned()
+ pub fn update_files(&mut self, paths: Vec<(PathBuf, Option<FileMetadata>)>) {
+ self.file_list_view
+ .update_files(paths, self.hide_hidden_files);
}
-
- pub fn as_line(&self, width: usize) -> String {
- let info = match self {
- FsEntry::Dir(_s) => "".to_owned(),
- FsEntry::File(_, s) => pb::convert(*s as f64),
- };
- let space = width.saturating_sub(info.len());
- let name = self.name();
- if space.saturating_sub(1) < name.len() {
- [&name[..space.saturating_sub(2)], &info].join("~ ")
+ pub fn open_selected_path(&mut self) {
+ if self.file_list_view.path_is_dir {
+ open_terminal(&self.file_list_view.path);
} else {
- let padding = " ".repeat(space - name.len());
- [name, padding, info].concat()
+ if let Some(parent_folder) = self.file_list_view.path.parent() {
+ open_file(
+ FileToOpen::new(&self.file_list_view.path).with_cwd(parent_folder.into()),
+ );
+ } else {
+ open_file(FileToOpen::new(&self.file_list_view.path));
+ }
+ }
+ if self.close_on_selection {
+ close_focus();
}
}
-
- pub fn is_hidden_file(&self) -> bool {
- self.name().starts_with('.')
+ pub fn send_filepick_response(&mut self) {
+ let selected_path = self.initial_cwd.join(
+ self.file_list_view
+ .path
+ .strip_prefix(ROOT)
+ .map(|p| p.to_path_buf())
+ .unwrap_or_else(|_| self.file_list_view.path.clone()),
+ );
+ match &self.handling_filepick_request_from {
+ Some((PipeSource::Plugin(plugin_id), args)) => {
+ pipe_message_to_plugin(
+ MessageToPlugin::new("filepicker_result")
+ .with_destination_plugin_id(*plugin_id)
+ .with_args(args.clone())
+ .with_payload(selected_path.display().to_string()),
+ );
+ #[cfg(target_family = "wasm")]
+ close_focus();
+ },
+ Some((PipeSource::Cli(pipe_id), _args)) => {
+ #[cfg(target_family = "wasm")]
+ cli_pipe_output(pipe_id, &selected_path.display().to_string());
+ #[cfg(target_family = "wasm")]
+ unblock_cli_pipe_input(pipe_id);
+ #[cfg(target_family = "wasm")]
+ close_focus();
+ },
+ _ => {},
+ }
}
}
-pub(crate) fn refresh_directory(state: &mut State) {
- // TODO: might be good to do this asynchronously with a worker
- let mut max_lines = (state.current_rows.unwrap_or(50) + state.scroll()) * 2; // 100 is arbitrary for performance reasons
- let mut files = vec![];
- for entry in read_dir(Path::new(ROOT).join(&state.path)).unwrap() {
- if let Ok(entry) = entry {
- if max_lines == 0 {
- break;
- }
- let entry_metadata = entry.metadata().unwrap();
- let entry = if entry_metadata.is_dir() {
- FsEntry::Dir(entry.path())
- } else {
- let size = entry_metadata.len();
- FsEntry::File(entry.path(), size)
- };
- if !entry.is_hidden_file() || !state.hide_hidden_files {
- max_lines = max_lines.saturating_sub(1);
- files.push(entry);
- }
- }
- }
- state.files = files;
- state.files.sort_unstable();
+pub(crate) fn refresh_directory(path: &Path) {
+ let path_on_host = Path::new(ROOT).join(path.strip_prefix("/").unwrap_or(path));
+ scan_host_folder(&path_on_host);
}
diff --git a/src/main.rs b/src/main.rs
index 871d468f..2ae37e2c 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -69,7 +69,7 @@ fn main() {
height,
})) = opts.command
{
- let cwd = std::env::current_dir().ok();
+ let cwd = None;
let command_cli_action = CliAction::NewPane {
command: vec![],
plugin: Some(url),
diff --git a/zellij-client/src/cli_client.rs b/zellij-client/src/cli_client.rs
index 38d73a4c..6d25e21b 100644
--- a/zellij-client/src/cli_client.rs
+++ b/zellij-client/src/cli_client.rs
@@ -74,7 +74,7 @@ fn pipe_client(
os_input: &mut Box<dyn ClientOsApi>,
pipe_id: String,
mut name: Option<String>,
- payload: Option<String>,
+ mut payload: Option<String>,
plugin: Option<String>,
args: Option<BTreeMap<String, String>>,
mut configuration: Option<BTreeMap<String, String>>,
@@ -87,7 +87,13 @@ fn pipe_client(
pane_title: Option<String>,
) {
let mut stdin = os_input.get_stdin_reader();
- let name = name.take().or_else(|| Some(Uuid::new_v4().to_string()));
+ let name = name
+ // first we try to take the explicitly supplied message name
+ .take()
+ // then we use the plugin, to facilitate using aliases
+ .or_else(|| plugin.clone())
+ // then we use a uuid to at least have some sort of identifier for this message
+ .or_else(|| Some(Uuid::new_v4().to_string()));
if launch_new {
// we do this to make sure the plugin is unique (has a unique configuration parameter) so
// that a new one would be launched, but we'll still send it to the same instance rather
@@ -116,26 +122,30 @@ fn pipe_client(
None,
)
};
+ let is_piped = !os_input.stdin_is_terminal();
loop {
- if payload.is_some() {
- // we got payload from the command line, we should use it and not wait for more
- let msg = create_msg(payload);
+ if let Some(payload) = payload.take() {
+ let msg = create_msg(Some(payload));
os_input.send_to_server(msg);
- break;
- }
- // we didn't get payload from the command line, meaning we listen on STDIN because this
- // signifies the user is about to pipe more (eg. cat my-large-file | zellij pipe ...)
- let mut buffer = String::new();
- let _ = stdin.read_line(&mut buffer);
- if buffer.is_empty() {
- // end of pipe, send an empty message down the pipe
+ } else if !is_piped {
+ // here we send an empty message to trigger the plugin, because we don't have any more
+ // data
let msg = create_msg(None);
os_input.send_to_server(msg);
- break;
} else {
- // we've got data! send it down the pipe (most common)
- let msg = create_msg(Some(buffer));
- os_input.send_to_server(msg);
+ // we didn't get payload from the command line, meaning we listen on STDIN because this
+ // signifies the user is about to pipe more (eg. cat my-large-file | zellij pipe ...)
+ let mut buffer = String::new();
+ let _ = stdin.read_line(&mut buffer);
+ if buffer.is_empty() {
+ // TODO: consider notifying the relevant plugin that the pipe has ended with a
+ // specialized message
+ break;
+ } else {
+ // we've got data! send it down the pipe (most common)
+ let msg = create_msg(Some(buffer));
+ os_input.send_to_server(msg);
+ }
}
loop {
// wait for a response and act accordingly
@@ -144,7 +154,13 @@ fn pipe_client(
// unblock this pipe, meaning we need to stop waiting for a response and read
// once more from STDIN
if pipe_name == pipe_id {
- break;
+ if !is_piped {
+ // if this client is not piped, we need to exit the process completely
+ // rather than wait for more data
+ process::exit(0);
+ } else {
+ break;
+ }
}
},
Some((ServerToClientMsg::CliPipeOutput(pipe_name, output), _)) => {
diff --git a/zellij-client/src/os_input_output.rs b/zellij-client/src/os_input_output.rs
index 50e7e2eb..1bfd6666 100644
--- a/zellij-client/src/os_input_output.rs
+++ b/zellij-client/src/os_input_output.rs
@@ -8,6 +8,7 @@ use nix::pty::Winsize;
use nix::sys::termios;
use signal_hook::{consts::signal::*, iterator::Signals};
use std::io::prelude::*;
+use std::io::IsTerminal;
use std::os::unix::io::RawFd;
use std::path::Path;
use std::sync::{Arc, Mutex};
@@ -97,6 +98,12 @@ pub trait ClientOsApi: Send + Sync {
fn get_stdout_writer(&self) -> Box<dyn io::Write>;
/// Returns a BufReader that allows to read from STDIN line by line, also locks STDIN
fn get_stdin_reader(&self) -> Box<dyn io::BufRead>;
+ fn stdin_is_terminal(&self) -> bool {
+ true
+ }
+ fn stdout_is_terminal(&self) -> bool {
+ true
+ }
fn update_session_name(&mut self, new_session_name: String);
/// Returns the raw contents of standard input.
fn read_from_stdin(&mut self) -> Result<Vec<u8>, &'static str>;
@@ -193,6 +200,16 @@ impl ClientOsApi for ClientOsInputOutput {
Box::new(stdin.lock())
}
+ fn stdin_is_terminal(&self) -> bool {
+ let stdin = ::std::io::stdin();
+ stdin.is_terminal()
+ }
+
+ fn stdout_is_terminal(&self) -> bool {
+ let stdout = ::std::io::stdout();
+ stdout.is_terminal()
+ }
+
fn send_to_server(&self, msg: ClientToServerMsg) {
// TODO: handle the error here, right now we silently ignore it
let _ = self
diff --git a/zellij-server/src/plugins/mod.rs b/zellij-server/src/plugins/mod.rs
index d44ebb49..1f2f2be7 100644
--- a/zellij-server/src/plugins/mod.rs
+++ b/zellij-server/src/plugins/mod.rs
@@ -127,6 +127,7 @@ pub enum PluginInstruction {
message: MessageToPlugin,
},
UnblockCliPipes(Vec<PluginRenderAsset>),
+ WatchFilesystem,
Exit,
}
@@ -162,6 +163,7 @@ impl From<&PluginInstruction> for PluginContext {
PluginInstruction::CachePluginEvents { .. } => PluginContext::CachePluginEvents,
PluginInstruction::MessageFromPlugin { .. } => PluginContext::MessageFromPlugin,
PluginInstruction::UnblockCliPipes { .. } => PluginContext::UnblockCliPipes,
+ PluginInstruction::WatchFilesystem => PluginContext::WatchFilesystem,
}
}
}
@@ -428,15 +430,8 @@ pub(crate) fn plugin_thread_main(
wasm_bridge.update_plugins(updates, shutdown_send.clone())?;
},
PluginInstruction::PluginSubscribedToEvents(_plugin_id, _client_id, events) => {
- for event in events {
- if let EventType::FileSystemCreate
- | EventType::FileSystemRead
- | EventType::FileSystemUpdate
- | EventType::FileSystemDelete = event
- {
- wasm_bridge.start_fs_watcher_if_not_started();
- }
- }
+ // no-op, there used to be stuff we did here - now there isn't, but we might want
+ // to add stuff here in the future
},
PluginInstruction::PermissionRequestResult(
plugin_id,
@@ -634,6 +629,9 @@ pub(crate) fn plugin_thread_main(
.context("failed to unblock input pipe");
}
},
+ PluginInstruction::WatchFilesystem => {
+ wasm_bridge.start_fs_watcher_if_not_started();
+ },
PluginInstruction::Exit => {
break;
},
diff --git a/zellij-server/src/plugins/wasm_bridge.rs b/zellij-server/src/plugins/wasm_bridge.rs
index 10a1f51a..7f39c9b1 100644
--- a/zellij-server/src/plugins/wasm_bridge.rs
+++ b/zellij-server/src/plugins/wasm_bridge.rs
@@ -580,6 +580,7 @@ impl WasmBridge {
&mut running_plugin,
&event,
&mut plugin_render_assets,
+ senders.clone(),
) {
Ok(()) => {
let _ = senders.send_to_screen(ScreenInstruction::PluginBytes(
@@ -830,6 +831,7 @@ impl WasmBridge {
&mut running_plugin,
&event,
&mut plugin_render_assets,
+ senders.clone(),
) {
Ok(()) => {
let _ = senders.send_to_screen(
@@ -1261,6 +1263,7 @@ pub fn apply_event_to_plugin(
running_plugin: &mut RunningPlugin,
event: &Event,
plugin_render_assets: &mut Vec<PluginRenderAsset>,
+ senders: ThreadSenders,
) -> Result<()> {
let instance = &running_plugin.instance;
let plugin_env = &running_plugin.plugin_env;
@@ -1315,6 +1318,17 @@ pub fn apply_event_to_plugin(
)
.with_pipes(pipes_to_block_or_unblock);
plugin_render_assets.push(plugin_render_asset);
+ } else {
+ // This is a bit of a hack to get around the fact that plugins are allowed not to
+ // render and still unblock CLI pipes
+ let pipes_to_block_or_unblock = pipes_to_block_or_unblock(running_plugin, None);
+ let plugin_render_asset = PluginRenderAsset::new(plugin_id, client_id, vec![])
+ .with_pipes(pipes_to_block_or_unblock);
+ let _ = senders
+ .send_to_plugin(PluginInstruction::UnblockCliPipes(vec![
+ plugin_render_asset,
+ ]))
+ .context("failed to unblock input pipe");
}
},
(PermissionStatus::Denied, permission) => {
diff --git a/zellij-server/src/plugins/watch_filesystem.rs b/zellij-server/src/plugins/watch_filesystem.rs
index 36c0e04d..d21d1c02 100644
--- a/zellij-server/src/plugins/watch_filesystem.rs
+++ b/zellij-server/src/plugins/watch_filesystem.rs
@@ -90,11 +90,36 @@ pub fn watch_filesystem(
.collect()
})
.collect();
+ // TODO: at some point we might want to add FileMetadata to these, but right now
+ // the API is a bit unstable, so let's not rock the boat too much by adding another
+ // expensive syscall
let _ = senders.send_to_plugin(PluginInstruction::Update(vec![
- (None, None, Event::FileSystemRead(read_paths)),
- (None, None, Event::FileSystemCreate(create_paths)),
- (None, None, Event::FileSystemUpdate(update_paths)),
- (None, None, Event::FileSystemDelete(delete_paths)),
+ (
+ None,
+ None,
+ Event::FileSystemRead(read_paths.into_iter().map(|p| (p, None)).collect()),
+ ),
+ (
+ None,
+ None,
+ Event::FileSystemCreate(
+ create_paths.into_iter().map(|p| (p, None)).collect(),
+ ),
+ ),
+ (
+ None,
+ None,
+ Event::FileSystemUpdate(
+ update_paths.into_iter().map(|p| (p, None)).collect(),
+ ),
+ ),
+ (
+ None,
+ None,
+ Event::FileSystemDelete(
+ delete_paths.into_iter().map(|p| (p, None)).collect(),
+ ),
+ ),
]));
},
Err(errors) => errors
diff --git a/zellij-server/src/plugins/zellij_exports.rs b/zellij-server/src/plugins/zellij_exports.rs
index 2c95de42..65c18e16 100644
--- a/zellij-server/src/plugins/zellij_exports.rs
+++ b/zellij-server/src/plugins/zellij_exports.rs
@@ -259,6 +259,10 @@ fn host_run_plugin_command(env: FunctionEnvMut<ForeignFunctionEnv>) {
PluginCommand::MessageToPlugin(message) => message_to_plugin(env, message)?,
PluginCommand::DisconnectOtherClients => disconnect_other_clients(env),
PluginCommand::KillSessions(session_list) => kill_sessions(session_list),
+ PluginCommand::ScanHostFolder(folder_to_scan) => {
+ scan_host_folder(env, folder_to_scan)
+ },
+ PluginCommand::WatchFilesystem => watch_filesystem(env),
},
(PermissionStatus::Denied, permission) => {
log::error!(
@@ -398,6 +402,7 @@ fn get_plugin_ids(env: &ForeignFunctionEnv) {
let ids = PluginIds {
plugin_id: env.plugin_env.plugin_id,
zellij_pid: process::id(),
+ initial_cwd: env.plugin_env.plugin_cwd.clone(),
};
ProtobufPluginIds::try_from(ids)
.map_err(|e| anyhow!("Failed to serialized plugin ids: {}", e))
@@ -1326,6 +1331,82 @@ fn kill_sessions(session_names: Vec<String>) {
}
}
+fn watch_filesystem(env: &ForeignFunctionEnv) {
+ let _ = env
+ .plugin_env
+ .senders
+ .to_plugin
+ .as_ref()
+ .map(|sender| sender.send(PluginInstruction::WatchFilesystem));
+}
+
+fn scan_host_folder(env: &ForeignFunctionEnv, folder_to_scan: PathBuf) {
+ if !folder_to_scan.starts_with("/host") {
+ log::error!(
+ "Can only scan files in the /host filesystem, found: {}",
+ folder_to_scan.display()
+ );
+ return;
+ }
+ let plugin_host_folder = env.plugin_env.plugin_cwd.clone();
+ let folder_to_scan = plugin_host_folder.join(folder_to_scan.strip_prefix("/host").unwrap());
+ match folder_to_scan.canonicalize() {
+ Ok(folder_to_scan) => {
+ if !folder_to_scan.starts_with(&plugin_host_folder) {
+ log::error!(
+ "Can only scan files in the plugin filesystem: {}, found: {}",
+ plugin_host_folder.display(),
+ folder_to_scan.display()
+ );
+ return;
+ }
+ let reading_folder = std::fs::read_dir(&folder_to_scan);
+ match reading_folder {
+ Ok(reading_folder) => {
+ let send_plugin_instructions = env.plugin_env.senders.to_plugin.clone();
+ let update_target = Some(env.plugin_env.plugin_id);
+ let client_id = env.plugin_env.client_id;
+ thread::spawn({
+ move || {
+ let mut paths_in_folder = vec![];
+ for entry in reading_folder {
+ if let Ok(entry) = entry {
+ let entry_metadata = entry.metadata().ok().map(|m| m.into());
+ paths_in_folder.push((
+ PathBuf::from("/host").join(
+ entry.path().strip_prefix(&plugin_host_folder).unwrap(),
+ ),
+ entry_metadata.into(),
+ ));
+ }
+ }
+ let _ = send_plugin_instructions
+ .ok_or(anyhow!("found no sender to send plugin instruction to"))
+ .map(|sender| {
+ let _ = sender.send(PluginInstruction::Update(vec![(
+ update_target,
+ Some(client_id),
+ Event::FileSystemUpdate(paths_in_folder),
+ )]));
+ })
+ .non_fatal();
+ }
+ });
+ },
+ Err(e) => {
+ log::error!("Failed to read folder {}: {e}", folder_to_scan.display());
+ },
+ }
+ },
+ Err(e) => {
+ log::error!(
+ "Failed to canonicalize path {folder_to_scan:?} when scanning folder: {:?}",
+ e
+ );
+ },
+ }
+}
+
// Custom panic handler for plugins.
//
// This is called when a panic occurs in a plugin. Since most panics will likely originate in the
diff --git a/zellij-tile/src/shim.rs b/zellij-tile/src/shim.rs
index 064a53d8..2d04a7b6 100644
--- a/zellij-tile/src/shim.rs
+++ b/zellij-tile/src/shim.rs
@@ -766,6 +766,24 @@ where
unsafe { host_run_plugin_command() };
}
+/// Scan a specific folder in the host filesystem (this is a hack around some WASI runtime performance
+/// issues), will not follow symlinks
+pub fn scan_host_folder<S: AsRef<Path>>(folder_to_scan: &S) {
+ let plugin_command = PluginCommand::ScanHostFolder(folder_to_scan.as_ref().to_path_buf());
+ let protobuf_plugin_command: ProtobufPluginCommand = plugin_command.try_into().unwrap();
+ object_to_stdout(&protobuf_plugin_command.encode_to_vec());
+ unsafe { host_run_plugin_command() };
+}
+
+/// Start watching the host folder for filesystem changes (Note: somewhat unstable at the time
+/// being)
+pub fn watch_filesystem() {
+ let plugin_command = PluginCommand::WatchFilesystem;
+ let protobuf_plugin_command: ProtobufPluginCommand = plugin_command.try_into().unwrap();
+ object_to_stdout(&protobuf_plugin_command.encode_to_vec());
+ unsafe { host_run_plugin_command() };
+}
+
// Utility Functions
#[allow(unused)]
diff --git a/zellij-utils/assets/prost/api.event.rs b/zellij-utils/assets/prost/api.event.rs
index 6d4ed729..2a4f8427 100644
--- a/zellij-utils/assets/prost/api.event.rs
+++ b/zellij-utils/assets/prost/api.event.rs
@@ -109,6 +109,23 @@ pub struct PermissionRequestResultPayload {
pub struct FileListPayload {
#[prost(string, repeated, tag = "1")]
pub paths: ::prost::alloc::vec::Vec<::prost::alloc::string::String>,
+ #[prost(message, repeated, tag = "2")]
+ pub paths_metadata: ::prost::alloc::vec::Vec<FileMetadata>,
+}
+#[allow(clippy::derive_partial_eq_without_eq)]
+#[derive(Clone, PartialEq, ::prost::Message)]
+pub struct FileMetadata {
+ /// if this is false, the metadata for this file has not been read
+ #[prost(bool, tag = "1")]
+ pub metadata_is_set: bool,
+ #[prost(bool, tag = "2")]
+ pub is_dir: bool,
+ #[prost(bool, tag = "3")]
+ pub is_file: bool,
+ #[prost(bool, tag = "4")]
+ pub is_symlink: bool,
+ #[prost(uint64, tag = "5")]
+ pub len: u64,
}
#[allow(clippy::derive_partial_eq_without_eq)]
#[derive(Clone, PartialEq, ::prost::Message)]
diff --git a/zellij-utils/assets/prost/api.plugin_command.rs b/zellij-utils/assets/prost/api.plugin_command.rs
index 44110ac8..5ec93e2e 100644
--- a/zellij-utils/assets/prost/api.plugin_command.rs
+++ b/zellij-utils/assets/prost/api.plugin_command.rs
@@ -5,7 +5,7 @@ pub struct PluginCommand {
pub name: i32,
#[prost(
oneof = "plugin_command::Payload",
- tags = "2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 60"
+ tags = "2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 60, 61"
)]
pub payload: ::core::option::Option<plugin_command::Payload>,
}
@@ -114,6 +114,8 @@ pub mod plugin_command {
MessageToPluginPayload(super::MessageToPluginPayload),
#[prost(message, tag = "60")]
KillSessionsPayload(super::KillSessionsPayload),
+ #[prost(string, tag = "61")]
+ ScanHostFolderPayload(::prost::alloc::string::String),
}
}
#[allow(clippy::derive_partial_eq_without_eq)]
@@ -416,6 +418,8 @@ pub enum CommandName {
MessageToPlugin = 79,
DisconnectOtherClients = 80,
KillSessions = 81,
+ ScanHostFolder = 82,
+ WatchFilesystem = 83,
}
impl CommandName {
/// String value of the enum field names used in the ProtoBuf definition.
@@ -506,6 +510,8 @@ impl CommandName {
CommandName::MessageToPlugin => "MessageToPlugin",
CommandName::DisconnectOtherClients => "DisconnectOtherClients",
CommandName::KillSessions => "KillSessions",
+ CommandName::ScanHostFolder => "ScanHostFolder",
+ CommandName::WatchFilesystem => "WatchFilesystem",
}
}
/// Creates an enum from field names used in the ProtoBuf definition.
@@ -593,6 +599,8 @@ impl CommandName {
"MessageToPlugin" => Some(Self::MessageToPlugin),
"DisconnectOtherClients" => Some(Self::DisconnectOtherClients),
"KillSessions" => Some(Self::KillSessions),
+ "ScanHostFolder" => Some(Self::ScanHostFolder),
+ "WatchFilesystem" => Some(Self::WatchFilesystem),
_ => None,
}
}
diff --git a/zellij-utils/assets/prost/api.plugin_ids.rs b/zellij-utils/assets/prost/api.plugin_ids.rs
index 647cc06c..972bc949 100644
--- a/zellij-utils/assets/prost/api.plugin_ids.rs
+++ b/zellij-utils/assets/prost/api.plugin_ids.rs
@@ -5,6 +5,8 @@ pub struct PluginIds {
pub plugin_id: i32,
#[prost(int32, tag = "2")]
pub zellij_pid: i32,
+ #[prost(string, tag = "3")]
+ pub initial_cwd: ::prost::alloc::string::String,
}
#[allow(clippy::derive_partial_eq_without_eq)]
#[derive(Clone, PartialEq, ::prost::Message)]
diff --git a/zellij-utils/src/data.rs b/zellij-utils/src/data.rs
index 63ccccce..9bb0d1f9 100644
--- a/zellij-utils/src/data.rs
+++ b/zellij-utils/src/data.rs
@@ -5,6 +5,7 @@ use clap::ArgEnum;
use serde::{Deserialize, Serialize};
use std::collections::{BTreeMap, HashMap, HashSet};
use std::fmt;
+use std::fs::Metadata;
use std::path::{Path, PathBuf};
use std::str::FromStr;
use std::time::Duration;
@@ -458,6 +459,25 @@ pub enum Mouse {
Release(isize, usize), // line and column
}
+#[derive(Debug, Default, Copy, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
+pub struct FileMetadata {
+ pub is_dir: bool,
+ pub is_file: bool,
+ pub is_symlink: bool,
+ pub len: u64,
+}
+
+impl From<Metadata> for FileMetadata {
+ fn from(metadata: Metadata) -> Self {
+ FileMetadata {
+ is_dir: metadata.is_dir(),
+ is_file: metadata.is_file(),
+ is_symlink: metadata.is_symlink(),
+ len: metadata.len(),
+ }
+ }
+}
+
/// These events can be subscribed to with subscribe method exported by `zellij-tile`.
/// Once subscribed to, they will trigger the `update` method of the `ZellijPlugin` trait.
#[derive(Debug, Clone, PartialEq, EnumDiscriminants, ToString, Serialize, Deserialize)]
@@ -488,13 +508,13 @@ pub enum Event {
String, // payload
),
/// A file was created somewhere in the Zellij CWD folder
- FileSystemCreate(Vec<PathBuf>),
+ FileSystemCreate(Vec<(PathBuf, Option<FileMetadata>)>),
/// A file was accessed somewhere in the Zellij CWD folder
- FileSystemRead(Vec<PathBuf>),
+ FileSystemRead(Vec<(PathBuf, Option<FileMetadata>)>),
/// A file was modified somewhere in the Zellij CWD folder
- FileSystemUpdate(Vec<PathBuf>),
+ FileSystemUpdate(Vec<(PathBuf, Option<FileMetadata>)>),
/// A file was deleted somewhere in the Zellij CWD folder
- FileSystemDelete(Vec<PathBuf>),
+ FileSystemDelete(Vec<(PathBuf, Option<FileMetadata>)>),
/// A Result of plugin permission request
PermissionRequestResult(PermissionStatus),
SessionUpdate(
@@ -904,10 +924,11 @@ pub struct PaneInfo {
pub is_selectable: bool,
}
-#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, Deserialize, Serialize)]
+#[derive(Debug, Clone, PartialEq, Eq, Hash, Deserialize, Serialize)]
pub struct PluginIds {
pub plugin_id: u32,
pub zellij_pid: u32,
+ pub initial_cwd: PathBuf,
}
/// Tag used to identify the plugin in layout and config kdl files
@@ -1350,4 +1371,6 @@ pub enum PluginCommand {
MessageToPlugin(MessageToPlugin),
DisconnectOtherClients,
KillSessions(Vec<String>), // one or more session names
+ ScanHostFolder(PathBuf), // TODO: rename to ScanHostFolder
+ WatchFilesystem,
}
diff --git a/zellij-utils/src/errors.rs b/zellij-utils/src/errors.rs
index 0677f611..9a100e38 100644
--- a/zellij-utils/src/errors.rs
+++ b/zellij-utils/src/errors.rs
@@ -400,6 +400,7 @@ pub enum PluginContext {
CachePluginEvents,
MessageFromPlugin,
UnblockCliPipes,
+ WatchFilesystem,
}
/// Stack call representations corresponding to the different types of [`ClientInstruction`]s.
diff --git a/zellij-utils/src/input/actions.rs b/zellij-utils/src/input/actions.rs
index c71a8ca6..fdf04030 100644
--- a/zellij-utils/src/input/actions.rs
+++ b/zellij-utils/src/input/actions.rs
@@ -353,7 +353,7 @@ impl Action {
let alias_cwd = cwd.clone().map(|cwd| current_dir.join(cwd));
let cwd = cwd
.map(|cwd| current_dir.join(cwd))
- .or_else(|| Some(current_dir));
+ .or_else(|| Some(current_dir.clone()));
if let Some(plugin) = plugin {
let plugin = match RunPluginLocation::parse(&plugin, cwd.clone()) {
Ok(location) => {
@@ -365,11 +365,17 @@ impl Action {
initial_cwd: cwd.clone(),
})
},
- Err(_) => RunPluginOrAlias::Alias(PluginAlias::new(
- &plugin,
- &configuration.map(|c| c.inner().clone()),
- alias_cwd,
- )),
+ Err(_) => {
+ let mut user_configuration =
+ configuration.map(|c| c.inner().clone()).unwrap_or_default();
+ user_configuration
+ .insert("caller_cwd".to_owned(), current_dir.display().to_string());
+ RunPluginOrAlias::Alias(PluginAlias::new(
+ &plugin,
+ &Some(user_configuration),
+ alias_cwd,
+ ))
+ },
};
if floating {
Ok(vec![Action::NewFloatingPluginPane(
diff --git a/zellij-utils/src/plugin_api/event.proto b/zellij-utils/src/plugin_api/event.proto
index e26a9c23..abc239c0 100644
--- a/zellij-utils/src/plugin_api/event.proto
+++ b/zellij-utils/src/plugin_api/event.proto
@@ -103,6 +103,15 @@ message PermissionRequestResultPayload {
message FileListPayload {
repeated string paths = 1;
+ repeated FileMetadata paths_metadata = 2;
+}
+
+message FileMetadata {
+ bool metadata_is_set = 1; // if this is false, the metadata for this file has not been read
+ bool is_dir = 2;
+ bool is_file = 3;
+ bool is_symlink = 4;
+ uint64 len = 5;
}
message CustomMessagePayload {
diff --git a/zellij-utils/src/plugin_api/event.rs b/zellij-utils/src/plugin_api/event.rs
index 21ff1eff..1b2cbce2 100644
--- a/zellij-utils/src/plugin_api/event.rs
+++ b/zellij-utils/src/plugin_api/event.rs
@@ -3,10 +3,11 @@ pub use super::generated_api::api::{
event::{
event::Payload as ProtobufEventPayload, CopyDestination as ProtobufCopyDestination,
Event as ProtobufEvent, EventNameList as ProtobufEventNameList,
- EventType as ProtobufEventType, InputModeKeybinds as ProtobufInputModeKeybinds,
- KeyBind as ProtobufKeyBind, LayoutInfo as ProtobufLayoutInfo,
- ModeUpdatePayload as ProtobufModeUpdatePayload, PaneInfo as ProtobufPaneInfo,
- PaneManifest as ProtobufPaneManifest, ResurrectableSession as ProtobufResurrectableSession,
+ EventType as ProtobufEventType, FileMetadata as ProtobufFileMetadata,
+ InputModeKeybinds as ProtobufInputModeKeybinds, KeyBind as ProtobufKeyBind,
+ LayoutInfo as ProtobufLayoutInfo, ModeUpdatePayload as ProtobufModeUpdatePayload,
+ PaneInfo as ProtobufPaneInfo, PaneManifest as ProtobufPaneManifest,
+ ResurrectableSession as ProtobufResurrectableSession,
SessionManifest as ProtobufSessionManifest, TabInfo as ProtobufTabInfo, *,
},
input_mode::InputMode as ProtobufInputMode,
@@ -14,8 +15,8 @@ pub use super::generated_api::api::{
style::Style as ProtobufStyle,
};
use crate::data::{
- CopyDestination, Event, EventType, InputMode, Key, LayoutInfo, ModeInfo, Mouse, PaneInfo,
- PaneManifest, PermissionStatus, PluginCapabilities, SessionInfo, Style, TabInfo,
+ CopyDestination, Event, EventType, FileMetadata, InputMode, Key, LayoutInfo, ModeInfo, Mouse,
+ PaneInfo, PaneManifest, PermissionStatus, PluginCapabilities, SessionInfo, Style, TabInfo,
};
use crate::errors::prelude::*;
@@ -124,7 +125,8 @@ impl TryFrom<ProtobufEvent> for Event {
let file_paths = file_list_payload
.paths
.iter()
- .map(|p| PathBuf::from(p))
+ .zip(file_list_payload.paths_metadata.iter())
+ .map(|(p, m)| (PathBuf::from(p), m.into()))
.collect();
Ok(Event::FileSystemCreate(file_paths))
},
@@ -135,7 +137,8 @@ impl TryFrom<ProtobufEvent> for Event {
let file_paths = file_list_payload
.paths
.iter()
- .map(|p| PathBuf::from(p))
+ .zip(file_list_payload.paths_metadata.iter())
+ .map(|(p, m)| (PathBuf::from(p), m.into()))
.collect();
Ok(Event::FileSystemRead(file_paths))
},
@@ -146,7 +149,8 @@ impl TryFrom<ProtobufEvent> for Event {
let file_paths = file_list_payload
.paths
.iter()
- .map(|p| PathBuf::from(p))
+ .zip(file_list_payload.paths_metadata.iter())
+ .map(|(p, m)| (PathBuf::from(p), m.into()))
.collect();
Ok(Event::FileSystemUpdate(file_paths))
},
@@ -157,7 +161,8 @@ impl TryFrom<ProtobufEvent> for Event {
let file_paths = file_list_payload
.paths
.iter()
- .map(|p| PathBuf::from(p))
+ .zip(file_list_payload.paths_metadata.iter())
+ .map(|(p, m)| (PathBuf::from(p), m.into()))
.collect();
Ok(Event::FileSystemDelete(file_paths))
},
@@ -322,36 +327,64 @@ impl TryFrom<Event> for ProtobufEvent {
payload,
})),
}),
- Event::FileSystemCreate(paths) => {
+ Event::FileSystemCreate(event_paths) => {
+ let mut paths = vec![];
+ let mut paths_metadata = vec![];
+ for (path, path_metadata) in event_paths {
+ paths.push(path.display().to_string());
+ paths_metadata.push(path_metadata.into());
+ }
let file_list_payload = FileListPayload {
- paths: paths.iter().map(|p| p.display().to_string()).collect(),
+ paths,
+ paths_metadata,
};
Ok(ProtobufEvent {
name: ProtobufEventType::FileSystemCreate as i32,
payload: Some(event::Payload::FileListPayload(file_list_payload)),
})
},
- Event::FileSystemRead(paths) => {
+ Event::FileSystemRead(event_paths) => {
+ let mut paths = vec![];
+ let mut paths_metadata = vec![];
+ for (path, path_metadata) in event_paths {
+ paths.push(path.display().to_string());
+ paths_metadata.push(path_metadata.into());
+ }
let file_list_payload = FileListPayload {
- paths: paths.iter().map(|p| p.display().to_string()).collect(),
+ paths,
+ paths_metadata,
};
Ok(ProtobufEvent {
name: ProtobufEventType::FileSystemRead as i32,
payload: Some(event::Payload::FileListPayload(file_list_payload)),
})
},
- Event::FileSystemUpdate(paths) => {
+ Event::FileSystemUpdate(event_paths) => {
+ let mut paths = vec![];
+ let mut paths_metadata = vec![];
+ for (path, path_metadata) in event_paths {
+ paths.push(path.display().to_string());
+ paths_metadata.push(path_metadata.into());
+ }
let file_list_payload = FileListPayload {
- paths: paths.iter().map(|p| p.display().to_string()).collect(),
+ paths,
+ paths_metadata,
};
Ok(ProtobufEvent {
name: ProtobufEventType::FileSystemUpdate as i32,
payload: Some(event::Payload::FileListPayload(file_list_payload)),
})
},
- Event::FileSystemDelete(paths) => {
+ Event::FileSystemDelete(event_paths) => {
+ let mut paths = vec![];
+ let mut paths_metadata = vec![];
+ for (path, path_metadata) in event_paths {
+ paths.push(path.display().to_string());
+ paths_metadata.push(path_metadata.into());
+ }
let file_list_payload = FileListPayload {
- paths: paths.iter().map(|p| p.display().to_string()).collect(),
+ paths,
+ paths_metadata,
};
Ok(ProtobufEvent {
name: ProtobufEventType::FileSystemDelete as i32,
@@ -958,6 +991,39 @@ impl From<(String, Duration)> for ProtobufResurrectableSession {
}
}
+impl From<&ProtobufFileMetadata> for Option<FileMetadata> {
+ fn from(protobuf_file_metadata: &ProtobufFileMetadata) -> Option<FileMetadata> {
+ if protobuf_file_metadata.metadata_is_set {
+ Some(FileMetadata {
+ is_file: protobuf_file_metadata.is_file,
+ is_dir: protobuf_file_metadata.is_dir,
+ is_symlink: protobuf_file_metadata.is_symlink,
+ len: protobuf_file_metadata.len,
+ })
+ } else {
+ None
+ }
+ }
+}
+
+impl From<Option<FileMetadata>> for ProtobufFileMetadata {
+ fn from(file_metadata: Option<FileMetadata>) -> ProtobufFileMetadata {
+ match file_metadata {
+ Some(file_metadata) => ProtobufFileMetadata {
+ metadata_is_set: true,
+ is_file: file_metadata.is_file,
+ is_dir: file_metadata.is_dir,
+ is_symlink: file_metadata.is_symlink,
+ len: file_metadata.len,
+ },
+ None => ProtobufFileMetadata {
+ metadata_is_set: false,
+ ..Default::default()
+ },
+ }
+ }
+}
+
#[test]
fn serialize_mode_update_event() {
use prost::Message;
@@ -1256,8 +1322,10 @@ fn serialize_custom_message_event() {
#[test]
fn serialize_file_system_create_event() {
use prost::Message;
- let file_system_event =
- Event::FileSystemCreate(vec!["/absolute/path".into(), "./relative_path".into()]);
+ let file_system_event = Event::FileSystemCreate(vec![
+ ("/absolute/path".into(), None),
+ ("./relative_path".into(), Default::default()),
+ ]);
let protobuf_event: ProtobufEvent = file_system_event.clone().try_into().unwrap();
let serialized_protobuf_event = protobuf_event.encode_to_vec();
let deserialized_protobuf_event: ProtobufEvent =
@@ -1272,8 +1340,10 @@ fn serialize_file_system_create_event() {
#[test]
fn serialize_file_system_read_event() {
use prost::Message;
- let file_system_event =
- Event::FileSystemRead(vec!["/absolute/path".into(), "./relative_path".into()]);
+ let file_system_event = Event::FileSystemRead(vec![
+ ("/absolute/path".into(), None),
+ ("./relative_path".into(), Default::default()),
+ ]);
let protobuf_event: ProtobufEvent = file_system_event.clone().try_into().unwrap();
let serialized_protobuf_event = protobuf_event.encode_to_vec();
let deserialized_protobuf_event: ProtobufEvent =
@@ -1288,8 +1358,10 @@ fn serialize_file_system_read_event() {
#[test]
fn serialize_file_system_update_event() {
use prost::Message;
- let file_system_event =
- Event::FileSystemUpdate(vec!["/absolute/path".into(), "./relative_path".into()]);
+ let file_system_event = Event::FileSystemUpdate(vec![
+ ("/absolute/path".into(), None),
+ ("./relative_path".into(), Some(Default::default())),
+ ]);
let protobuf_event: ProtobufEvent = file_system_event.clone().try_into().unwrap();
let serialized_protobuf_event = protobuf_event.encode_to_vec();
let deserialized_protobuf_event: ProtobufEvent =
@@ -1304,8 +1376,10 @@ fn serialize_file_system_update_event() {
#[test]
fn serialize_file_system_delete_event() {
use prost::Message;
- let file_system_event =
- Event::FileSystemDelete(vec!["/absolute/path".into(), "./relative_path".into()]);
+ let file_system_event = Event::FileSystemDelete(vec![
+ ("/absolute/path".into(), None),
+ ("./relative_path".into(), Default::default()),
+ ]);
let protobuf_event: ProtobufEvent = file_system_event.clone().try_into().unwrap();
let serialized_protobuf_event = protobuf_event.encode_to_vec();
let deserialized_protobuf_event: ProtobufEvent =
diff --git a/zellij-utils/src/plugin_api/plugin_command.proto b/zellij-utils/src/plugin_api/plugin_command.proto
index eecae43b..c95111e5 100644
--- a/zellij-utils/src/plugin_api/plugin_command.proto
+++ b/zellij-utils/src/plugin_api/plugin_command.proto
@@ -93,6 +93,8 @@ enum CommandName {
MessageToPlugin = 79;
DisconnectOtherClients = 80;
KillSessions = 81;
+ ScanHostFolder = 82;
+ WatchFilesystem = 83;
}
message PluginCommand {
@@ -148,6 +150,7 @@ message PluginCommand {
CliPipeOutputPayload cli_pipe_output_payload = 49;
MessageToPluginPayload message_to_plugin_payload = 50;
KillSessionsPayload kill_sessions_payload = 60;
+ string scan_host_folder_payload = 61;
}
}
diff --git a/zellij-utils/src/plugin_api/plugin_command.rs b/zellij-utils/src/plugin_api/plugin_command.rs
index ab9d5294..e452e53c 100644
--- a/zellij-utils/src/plugin_api/plugin_command.rs
+++ b/zellij-utils/src/plugin_api/plugin_command.rs
@@ -855,7 +855,17 @@ impl TryFrom<ProtobufPluginCommand> for PluginCommand {
Some(Payload::KillSessionsPayload(KillSessionsPayload { session_names })) => {
Ok(PluginCommand::KillSessions(session_names))
},
- _ => Err("Mismatched payload for PipeOutput"),
+ _ => Err("Mismatched payload for KillSessions"),
+ },
+ Some(CommandName::ScanHostFolder) => match protobuf_plugin_command.payload {
+ Some(Payload::ScanHostFolderPayload(folder_to_scan)) => {
+ Ok(PluginCommand::ScanHostFolder(PathBuf::from(folder_to_scan)))
+ },
+ _ => Err("Mismatched payload for ScanHostFolder"),
+ },
+ Some(CommandName::WatchFilesystem) => match protobuf_plugin_command.payload {
+ Some(_) => Err("WatchFilesystem should have no payload, found a payload"),
+ None => Ok(PluginCommand::WatchFilesystem),
},
None => Err("Unrecognized plugin command"),
}
@@ -1361,6 +1371,16 @@ impl TryFrom<PluginCommand> for ProtobufPluginCommand {
session_names,
})),
}),
+ PluginCommand::ScanHostFolder(folder_to_scan) => Ok(ProtobufPluginCommand {
+ name: CommandName::ScanHostFolder as i32,
+ payload: Some(Payload::ScanHostFolderPayload(
+ folder_to_scan.display().to_string(),
+ )),
+ }),
+ PluginCommand::WatchFilesystem => Ok(ProtobufPluginCommand {
+ name: CommandName::WatchFilesystem as i32,
+ payload: None,
+ }),
}
}
}
diff --git a/zellij-utils/src/plugin_api/plugin_ids.proto b/zellij-utils/src/plugin_api/plugin_ids.proto
index 2977dbe4..09d30bd0 100644
--- a/zellij-utils/src/plugin_api/plugin_ids.proto
+++ b/zellij-utils/src/plugin_api/plugin_ids.proto
@@ -5,6 +5,7 @@ package api.plugin_ids;
message PluginIds {
int32 plugin_id = 1;
int32 zellij_pid = 2;
+ string initial_cwd = 3;
}
message ZellijVersion {
diff --git a/zellij-utils/src/plugin_api/plugin_ids.rs b/zellij-utils/src/plugin_api/plugin_ids.rs
index 51f526c6..749ccf22 100644
--- a/zellij-utils/src/plugin_api/plugin_ids.rs
+++ b/zellij-utils/src/plugin_api/plugin_ids.rs
@@ -4,6 +4,7 @@ pub use super::generated_api::api::plugin_ids::{
use crate::data::PluginIds;
use std::convert::TryFrom;
+use std::path::PathBuf;
impl TryFrom<ProtobufPluginIds> for PluginIds {
type Error = &'static str;
@@ -11,6 +12,7 @@ impl TryFrom<ProtobufPluginIds> for PluginIds {
Ok(PluginIds {
plugin_id: protobuf_plugin_ids.plugin_id as u32,
zellij_pid: protobuf_plugin_ids.zellij_pid as u32,
+ initial_cwd: PathBuf::from(protobuf_plugin_ids.initial_cwd),
})
}
}
@@ -21,6 +23,7 @@ impl TryFrom<PluginIds> for ProtobufPluginIds {
Ok(ProtobufPluginIds {
plugin_id: plugin_ids.plugin_id as i32,
zellij_pid: plugin_ids.zellij_pid as i32,
+ initial_cwd: plugin_ids.initial_cwd.display().to_string(),
})
}
}