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
This commit is contained in:
parent
12daac3b54
commit
ee16a4b8c3
32 changed files with 1397 additions and 291 deletions
5
Cargo.lock
generated
5
Cargo.lock
generated
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -67,6 +67,7 @@ impl ZellijPlugin for State {
|
|||
EventType::FileSystemUpdate,
|
||||
EventType::FileSystemDelete,
|
||||
]);
|
||||
watch_filesystem();
|
||||
}
|
||||
|
||||
fn update(&mut self, event: Event) -> bool {
|
||||
|
|
|
|||
|
|
@ -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"] }
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,2 +1,2 @@
|
|||
[build]
|
||||
target = "wasm32-wasi"
|
||||
target = "wasm32-wasi"
|
||||
|
|
|
|||
191
default-plugins/strider/src/file_list_view.rs
Normal file
191
default-plugins/strider/src/file_list_view.rs
Normal file
|
|
@ -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,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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::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::Right | Key::Char('\n') | Key::Char('l') if !self.files.is_empty() => {
|
||||
self.traverse_dir_or_open_file();
|
||||
self.ev_history.clear();
|
||||
Key::Backspace => {
|
||||
self.handle_backspace();
|
||||
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::Esc | Key::Ctrl('c') => {
|
||||
self.clear_search_term_or_descend();
|
||||
should_render = true;
|
||||
},
|
||||
Key::Char('.') => {
|
||||
Key::Up => {
|
||||
self.move_selection_up();
|
||||
should_render = true;
|
||||
},
|
||||
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::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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
134
default-plugins/strider/src/search_view.rs
Normal file
134
default-plugins/strider/src/search_view.rs
Normal file
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
156
default-plugins/strider/src/shared.rs
Normal file
156
default-plugins/strider/src/shared.rs
Normal file
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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);
|
||||
},
|
||||
FsEntry::File(p, _) => open_file(FileToOpen {
|
||||
path: p.strip_prefix(ROOT).unwrap().into(),
|
||||
..Default::default()
|
||||
}),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[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 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 traverse_dir(&mut self) {
|
||||
let entry = if self.is_searching {
|
||||
self.search_view.get_selected_entry()
|
||||
} else {
|
||||
let padding = " ".repeat(space - name.len());
|
||||
[name, padding, info].concat()
|
||||
}
|
||||
}
|
||||
|
||||
pub fn is_hidden_file(&self) -> bool {
|
||||
self.name().starts_with('.')
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
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();
|
||||
},
|
||||
}
|
||||
let entry_metadata = entry.metadata().unwrap();
|
||||
let entry = if entry_metadata.is_dir() {
|
||||
FsEntry::Dir(entry.path())
|
||||
}
|
||||
self.is_searching = false;
|
||||
self.search_term.clear();
|
||||
self.search_view.clear_and_reset_selection();
|
||||
}
|
||||
pub fn update_files(&mut self, paths: Vec<(PathBuf, Option<FileMetadata>)>) {
|
||||
self.file_list_view
|
||||
.update_files(paths, self.hide_hidden_files);
|
||||
}
|
||||
pub fn open_selected_path(&mut self) {
|
||||
if self.file_list_view.path_is_dir {
|
||||
open_terminal(&self.file_list_view.path);
|
||||
} else {
|
||||
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 {
|
||||
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);
|
||||
open_file(FileToOpen::new(&self.file_list_view.path));
|
||||
}
|
||||
}
|
||||
if self.close_on_selection {
|
||||
close_focus();
|
||||
}
|
||||
}
|
||||
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();
|
||||
},
|
||||
_ => {},
|
||||
}
|
||||
}
|
||||
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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
|
|
|
|||
|
|
@ -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), _)) => {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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) => {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)]
|
||||
|
|
|
|||
|
|
@ -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)]
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)]
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -400,6 +400,7 @@ pub enum PluginContext {
|
|||
CachePluginEvents,
|
||||
MessageFromPlugin,
|
||||
UnblockCliPipes,
|
||||
WatchFilesystem,
|
||||
}
|
||||
|
||||
/// Stack call representations corresponding to the different types of [`ClientInstruction`]s.
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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 =
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
}),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ package api.plugin_ids;
|
|||
message PluginIds {
|
||||
int32 plugin_id = 1;
|
||||
int32 zellij_pid = 2;
|
||||
string initial_cwd = 3;
|
||||
}
|
||||
|
||||
message ZellijVersion {
|
||||
|
|
|
|||
|
|
@ -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(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue