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",
|
"fuzzy-matcher",
|
||||||
"humantime",
|
"humantime",
|
||||||
"unicode-width",
|
"unicode-width",
|
||||||
|
"uuid",
|
||||||
"zellij-tile",
|
"zellij-tile",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
@ -4191,9 +4192,9 @@ checksum = "936e4b492acfd135421d8dca4b1aa80a7bfc26e702ef3af710e0752684df5372"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "uuid"
|
name = "uuid"
|
||||||
version = "1.4.1"
|
version = "1.7.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "79daa5ed5740825c40b389c5e50312b9c86df53fccd33f281df655642b43869d"
|
checksum = "f00cc9702ca12d3c81455259621e676d0f7251cec66a21e98fe2e9a37db93b2a"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"getrandom 0.2.10",
|
"getrandom 0.2.10",
|
||||||
"serde",
|
"serde",
|
||||||
|
|
|
||||||
|
|
@ -67,6 +67,7 @@ impl ZellijPlugin for State {
|
||||||
EventType::FileSystemUpdate,
|
EventType::FileSystemUpdate,
|
||||||
EventType::FileSystemDelete,
|
EventType::FileSystemDelete,
|
||||||
]);
|
]);
|
||||||
|
watch_filesystem();
|
||||||
}
|
}
|
||||||
|
|
||||||
fn update(&mut self, event: Event) -> bool {
|
fn update(&mut self, event: Event) -> bool {
|
||||||
|
|
|
||||||
|
|
@ -11,3 +11,4 @@ chrono = "0.4.0"
|
||||||
fuzzy-matcher = "0.3.7"
|
fuzzy-matcher = "0.3.7"
|
||||||
unicode-width = "0.1.10"
|
unicode-width = "0.1.10"
|
||||||
humantime = "2.1.0"
|
humantime = "2.1.0"
|
||||||
|
uuid = { version = "1.7.0", features = ["v4"] }
|
||||||
|
|
|
||||||
|
|
@ -2,9 +2,9 @@ mod new_session_info;
|
||||||
mod resurrectable_sessions;
|
mod resurrectable_sessions;
|
||||||
mod session_list;
|
mod session_list;
|
||||||
mod ui;
|
mod ui;
|
||||||
use zellij_tile::prelude::*;
|
|
||||||
|
|
||||||
use std::collections::BTreeMap;
|
use std::collections::BTreeMap;
|
||||||
|
use uuid::Uuid;
|
||||||
|
use zellij_tile::prelude::*;
|
||||||
|
|
||||||
use new_session_info::NewSessionInfo;
|
use new_session_info::NewSessionInfo;
|
||||||
use ui::{
|
use ui::{
|
||||||
|
|
@ -45,6 +45,7 @@ struct State {
|
||||||
colors: Colors,
|
colors: Colors,
|
||||||
is_welcome_screen: bool,
|
is_welcome_screen: bool,
|
||||||
show_kill_all_sessions_warning: bool,
|
show_kill_all_sessions_warning: bool,
|
||||||
|
request_ids: Vec<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
register_plugin!(State);
|
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 {
|
fn update(&mut self, event: Event) -> bool {
|
||||||
let mut should_render = false;
|
let mut should_render = false;
|
||||||
match event {
|
match event {
|
||||||
|
|
@ -109,7 +132,7 @@ impl ZellijPlugin for State {
|
||||||
render_new_session_block(
|
render_new_session_block(
|
||||||
&self.new_session_info,
|
&self.new_session_info,
|
||||||
self.colors,
|
self.colors,
|
||||||
height,
|
height.saturating_sub(2),
|
||||||
width,
|
width,
|
||||||
x,
|
x,
|
||||||
y + 2,
|
y + 2,
|
||||||
|
|
@ -184,12 +207,33 @@ impl State {
|
||||||
} else if let Key::Ctrl('w') = key {
|
} else if let Key::Ctrl('w') = key {
|
||||||
self.active_screen = ActiveScreen::NewSession;
|
self.active_screen = ActiveScreen::NewSession;
|
||||||
should_render = true;
|
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 {
|
} else if let Key::BackTab = key {
|
||||||
self.toggle_active_screen();
|
self.toggle_active_screen();
|
||||||
should_render = true;
|
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 {
|
} else if let Key::Esc = key {
|
||||||
self.new_session_info.handle_key(key);
|
self.new_session_info.handle_key(key);
|
||||||
should_render = true;
|
should_render = true;
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
use fuzzy_matcher::skim::SkimMatcherV2;
|
use fuzzy_matcher::skim::SkimMatcherV2;
|
||||||
use fuzzy_matcher::FuzzyMatcher;
|
use fuzzy_matcher::FuzzyMatcher;
|
||||||
use std::cmp::Ordering;
|
use std::cmp::Ordering;
|
||||||
|
use std::path::PathBuf;
|
||||||
use zellij_tile::prelude::*;
|
use zellij_tile::prelude::*;
|
||||||
|
|
||||||
#[derive(Default)]
|
#[derive(Default)]
|
||||||
|
|
@ -8,6 +9,7 @@ pub struct NewSessionInfo {
|
||||||
name: String,
|
name: String,
|
||||||
layout_list: LayoutList,
|
layout_list: LayoutList,
|
||||||
entering_new_session_info: EnteringState,
|
entering_new_session_info: EnteringState,
|
||||||
|
pub new_session_folder: Option<PathBuf>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Eq, PartialEq)]
|
#[derive(Eq, PartialEq)]
|
||||||
|
|
@ -104,7 +106,8 @@ impl NewSessionInfo {
|
||||||
if new_session_name != current_session_name.as_ref().map(|s| s.as_str()) {
|
if new_session_name != current_session_name.as_ref().map(|s| s.as_str()) {
|
||||||
match new_session_layout {
|
match new_session_layout {
|
||||||
Some(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 => {
|
None => {
|
||||||
switch_session(new_session_name);
|
switch_session(new_session_name);
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
|
use std::path::PathBuf;
|
||||||
use unicode_width::UnicodeWidthChar;
|
use unicode_width::UnicodeWidthChar;
|
||||||
use unicode_width::UnicodeWidthStr;
|
use unicode_width::UnicodeWidthStr;
|
||||||
use zellij_tile::prelude::*;
|
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 key_indication_len = key_indication_text.chars().count() + 1;
|
||||||
let first_ribbon_length = new_session_text.chars().count() + 4;
|
let first_ribbon_length = new_session_text.chars().count() + 4;
|
||||||
let second_ribbon_length = running_sessions_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 key_indication_x = x;
|
||||||
let first_ribbon_x = key_indication_x + key_indication_len;
|
let first_ribbon_x = key_indication_x + key_indication_len;
|
||||||
let second_ribbon_x = first_ribbon_x + first_ribbon_length;
|
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);
|
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(
|
pub fn render_new_session_block(
|
||||||
new_session_info: &NewSessionInfo,
|
new_session_info: &NewSessionInfo,
|
||||||
colors: Colors,
|
colors: Colors,
|
||||||
|
|
@ -615,6 +747,7 @@ pub fn render_new_session_block(
|
||||||
}
|
}
|
||||||
render_layout_selection_list(
|
render_layout_selection_list(
|
||||||
new_session_info,
|
new_session_info,
|
||||||
|
colors,
|
||||||
max_rows_of_new_session_block.saturating_sub(1),
|
max_rows_of_new_session_block.saturating_sub(1),
|
||||||
max_cols_of_new_session_block,
|
max_cols_of_new_session_block,
|
||||||
x,
|
x,
|
||||||
|
|
@ -625,6 +758,7 @@ pub fn render_new_session_block(
|
||||||
|
|
||||||
pub fn render_layout_selection_list(
|
pub fn render_layout_selection_list(
|
||||||
new_session_info: &NewSessionInfo,
|
new_session_info: &NewSessionInfo,
|
||||||
|
colors: Colors,
|
||||||
max_rows_of_new_session_block: usize,
|
max_rows_of_new_session_block: usize,
|
||||||
max_cols_of_new_session_block: usize,
|
max_cols_of_new_session_block: usize,
|
||||||
x: usize,
|
x: usize,
|
||||||
|
|
@ -658,7 +792,7 @@ pub fn render_layout_selection_list(
|
||||||
let layout_name = layout_info.name();
|
let layout_name = layout_info.name();
|
||||||
let layout_name_len = layout_name.width();
|
let layout_name_len = layout_name.width();
|
||||||
let is_builtin = layout_info.is_builtin();
|
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;
|
break;
|
||||||
} else {
|
} else {
|
||||||
let mut layout_cell = if is_builtin {
|
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]);
|
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) {
|
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 => {
|
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 = colors.magenta("<Ctrl r>");
|
||||||
let rename_text = colors.bold("Rename");
|
let rename_text = colors.bold("Rename");
|
||||||
let disconnect = colors.magenta("<Ctrl x>");
|
let disconnect = colors.magenta("<Ctrl x>");
|
||||||
|
|
@ -817,3 +955,25 @@ impl Colors {
|
||||||
self.color(&self.palette.magenta, text)
|
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
|
||||||
|
}
|
||||||
|
|
|
||||||
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;
|
mod state;
|
||||||
|
|
||||||
use colored::*;
|
use crate::file_list_view::FsEntry;
|
||||||
use state::{refresh_directory, FsEntry, State};
|
use shared::{render_current_path, render_instruction_line, render_search_term};
|
||||||
|
use state::{refresh_directory, State};
|
||||||
use std::collections::BTreeMap;
|
use std::collections::BTreeMap;
|
||||||
use std::{cmp::min, time::Instant};
|
use std::path::PathBuf;
|
||||||
use zellij_tile::prelude::*;
|
use zellij_tile::prelude::*;
|
||||||
|
|
||||||
register_plugin!(State);
|
register_plugin!(State);
|
||||||
|
|
||||||
impl ZellijPlugin for State {
|
impl ZellijPlugin for State {
|
||||||
fn load(&mut self, _configuration: BTreeMap<String, String>) {
|
fn load(&mut self, configuration: BTreeMap<String, String>) {
|
||||||
refresh_directory(self);
|
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(&[
|
subscribe(&[
|
||||||
EventType::Key,
|
EventType::Key,
|
||||||
EventType::Mouse,
|
EventType::Mouse,
|
||||||
EventType::CustomMessage,
|
EventType::CustomMessage,
|
||||||
EventType::Timer,
|
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 {
|
fn update(&mut self, event: Event) -> bool {
|
||||||
let mut should_render = false;
|
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 {
|
match event {
|
||||||
|
Event::FileSystemUpdate(paths) => {
|
||||||
|
self.update_files(paths);
|
||||||
|
should_render = true;
|
||||||
|
},
|
||||||
Event::Key(key) => match key {
|
Event::Key(key) => match key {
|
||||||
Key::Esc => {
|
Key::Char(character) if character != '\n' => {
|
||||||
hide_self();
|
self.update_search_term(character);
|
||||||
should_render = true;
|
should_render = true;
|
||||||
},
|
},
|
||||||
Key::Up | Key::Char('k') => {
|
Key::Backspace => {
|
||||||
let currently_selected = self.selected();
|
self.handle_backspace();
|
||||||
*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();
|
|
||||||
should_render = true;
|
should_render = true;
|
||||||
},
|
},
|
||||||
Key::Left | Key::Char('h') => {
|
Key::Esc | Key::Ctrl('c') => {
|
||||||
if self.path.components().count() > 2 {
|
self.clear_search_term_or_descend();
|
||||||
// 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;
|
should_render = true;
|
||||||
self.path.pop();
|
|
||||||
refresh_directory(self);
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
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;
|
should_render = true;
|
||||||
self.toggle_hidden_files();
|
self.toggle_hidden_files();
|
||||||
refresh_directory(self);
|
refresh_directory(&self.file_list_view.path);
|
||||||
},
|
},
|
||||||
|
|
||||||
_ => (),
|
_ => (),
|
||||||
},
|
},
|
||||||
Event::Mouse(mouse_event) => match mouse_event {
|
Event::Mouse(mouse_event) => match mouse_event {
|
||||||
Mouse::ScrollDown(_) => {
|
Mouse::ScrollDown(_) => {
|
||||||
let currently_selected = self.selected();
|
self.move_selection_down();
|
||||||
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;
|
should_render = true;
|
||||||
}
|
|
||||||
},
|
},
|
||||||
Mouse::ScrollUp(_) => {
|
Mouse::ScrollUp(_) => {
|
||||||
let currently_selected = self.selected();
|
self.move_selection_up();
|
||||||
*self.selected_mut() = self.selected().saturating_sub(1);
|
|
||||||
if currently_selected != self.selected() {
|
|
||||||
should_render = true;
|
should_render = true;
|
||||||
}
|
|
||||||
},
|
},
|
||||||
Mouse::Release(line, _) => {
|
Mouse::LeftClick(line, _) => {
|
||||||
if line < 0 {
|
self.handle_left_click(line);
|
||||||
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;
|
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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
_ => {},
|
_ => {},
|
||||||
},
|
},
|
||||||
|
|
@ -125,42 +126,37 @@ impl ZellijPlugin for State {
|
||||||
};
|
};
|
||||||
should_render
|
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) {
|
fn render(&mut self, rows: usize, cols: usize) {
|
||||||
self.current_rows = Some(rows);
|
self.current_rows = Some(rows);
|
||||||
for i in 0..rows {
|
let rows_for_list = rows.saturating_sub(6);
|
||||||
if self.selected() < self.scroll() {
|
render_search_term(&self.search_term);
|
||||||
*self.scroll_mut() = self.selected();
|
render_current_path(
|
||||||
}
|
&self.initial_cwd,
|
||||||
if self.selected() - self.scroll() + 2 > rows {
|
&self.file_list_view.path,
|
||||||
*self.scroll_mut() = self.selected() + 2 - rows;
|
self.file_list_view.path_is_dir,
|
||||||
}
|
self.handling_filepick_request_from.is_some(),
|
||||||
|
cols,
|
||||||
let is_last_row = i == rows.saturating_sub(1);
|
);
|
||||||
let i = self.scroll() + i;
|
if self.is_searching {
|
||||||
if let Some(entry) = self.files.get(i) {
|
self.search_view.render(rows_for_list, cols);
|
||||||
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 {
|
} else {
|
||||||
println!("{}", path.clone().reversed());
|
self.file_list_view.render(rows_for_list, cols);
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if is_last_row {
|
|
||||||
print!("{}", path);
|
|
||||||
} else {
|
|
||||||
println!("{}", path);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else if !is_last_row {
|
|
||||||
println!();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
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::{
|
use std::{
|
||||||
collections::{HashMap, VecDeque},
|
collections::BTreeMap,
|
||||||
fs::read_dir,
|
|
||||||
path::{Path, PathBuf},
|
path::{Path, PathBuf},
|
||||||
time::Instant,
|
|
||||||
};
|
};
|
||||||
use zellij_tile::prelude::*;
|
use zellij_tile::prelude::*;
|
||||||
|
|
||||||
|
|
@ -11,106 +11,189 @@ pub const ROOT: &str = "/host";
|
||||||
|
|
||||||
#[derive(Default)]
|
#[derive(Default)]
|
||||||
pub struct State {
|
pub struct State {
|
||||||
pub path: PathBuf,
|
pub file_list_view: FileListView,
|
||||||
pub files: Vec<FsEntry>,
|
pub search_view: SearchView,
|
||||||
pub cursor_hist: HashMap<PathBuf, (usize, usize)>,
|
|
||||||
pub hide_hidden_files: bool,
|
pub hide_hidden_files: bool,
|
||||||
pub ev_history: VecDeque<(Event, Instant)>, // stores last event, can be expanded in future
|
|
||||||
pub loading: bool,
|
pub loading: bool,
|
||||||
pub loading_animation_offset: u8,
|
pub loading_animation_offset: u8,
|
||||||
pub should_open_floating: bool,
|
pub should_open_floating: bool,
|
||||||
pub current_rows: Option<usize>,
|
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 {
|
impl State {
|
||||||
pub fn selected_mut(&mut self) -> &mut usize {
|
pub fn update_search_term(&mut self, character: char) {
|
||||||
&mut self.cursor_hist.entry(self.path.clone()).or_default().0
|
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 scroll_mut(&mut self) -> &mut usize {
|
pub fn handle_backspace(&mut self) {
|
||||||
&mut self.cursor_hist.entry(self.path.clone()).or_default().1
|
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;
|
||||||
}
|
}
|
||||||
pub fn scroll(&self) -> usize {
|
self.search_view
|
||||||
self.cursor_hist.get(&self.path).unwrap_or(&(0, 0)).1
|
.update_search_results(&self.search_term, &self.file_list_view.files);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
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 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) {
|
pub fn toggle_hidden_files(&mut self) {
|
||||||
self.hide_hidden_files = !self.hide_hidden_files;
|
self.hide_hidden_files = !self.hide_hidden_files;
|
||||||
}
|
}
|
||||||
pub fn traverse_dir_or_open_file(&mut self) {
|
pub fn traverse_dir(&mut self) {
|
||||||
if let Some(f) = self.files.get(self.selected()) {
|
let entry = if self.is_searching {
|
||||||
match f.clone() {
|
self.search_view.get_selected_entry()
|
||||||
FsEntry::Dir(p) => {
|
} else {
|
||||||
self.path = p;
|
self.file_list_view.get_selected_entry()
|
||||||
refresh_directory(self);
|
};
|
||||||
|
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();
|
||||||
}
|
}
|
||||||
|
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) {
|
||||||
#[derive(PartialEq, Eq, PartialOrd, Ord, Clone)]
|
if self.file_list_view.path_is_dir {
|
||||||
pub enum FsEntry {
|
open_terminal(&self.file_list_view.path);
|
||||||
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("~ ")
|
|
||||||
} else {
|
} else {
|
||||||
let padding = " ".repeat(space - name.len());
|
if let Some(parent_folder) = self.file_list_view.path.parent() {
|
||||||
[name, padding, info].concat()
|
open_file(
|
||||||
}
|
FileToOpen::new(&self.file_list_view.path).with_cwd(parent_folder.into()),
|
||||||
}
|
);
|
||||||
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
let entry_metadata = entry.metadata().unwrap();
|
|
||||||
let entry = if entry_metadata.is_dir() {
|
|
||||||
FsEntry::Dir(entry.path())
|
|
||||||
} else {
|
} else {
|
||||||
let size = entry_metadata.len();
|
open_file(FileToOpen::new(&self.file_list_view.path));
|
||||||
FsEntry::File(entry.path(), size)
|
}
|
||||||
};
|
}
|
||||||
if !entry.is_hidden_file() || !state.hide_hidden_files {
|
if self.close_on_selection {
|
||||||
max_lines = max_lines.saturating_sub(1);
|
close_focus();
|
||||||
files.push(entry);
|
}
|
||||||
|
}
|
||||||
|
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,
|
height,
|
||||||
})) = opts.command
|
})) = opts.command
|
||||||
{
|
{
|
||||||
let cwd = std::env::current_dir().ok();
|
let cwd = None;
|
||||||
let command_cli_action = CliAction::NewPane {
|
let command_cli_action = CliAction::NewPane {
|
||||||
command: vec![],
|
command: vec![],
|
||||||
plugin: Some(url),
|
plugin: Some(url),
|
||||||
|
|
|
||||||
|
|
@ -74,7 +74,7 @@ fn pipe_client(
|
||||||
os_input: &mut Box<dyn ClientOsApi>,
|
os_input: &mut Box<dyn ClientOsApi>,
|
||||||
pipe_id: String,
|
pipe_id: String,
|
||||||
mut name: Option<String>,
|
mut name: Option<String>,
|
||||||
payload: Option<String>,
|
mut payload: Option<String>,
|
||||||
plugin: Option<String>,
|
plugin: Option<String>,
|
||||||
args: Option<BTreeMap<String, String>>,
|
args: Option<BTreeMap<String, String>>,
|
||||||
mut configuration: Option<BTreeMap<String, String>>,
|
mut configuration: Option<BTreeMap<String, String>>,
|
||||||
|
|
@ -87,7 +87,13 @@ fn pipe_client(
|
||||||
pane_title: Option<String>,
|
pane_title: Option<String>,
|
||||||
) {
|
) {
|
||||||
let mut stdin = os_input.get_stdin_reader();
|
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 {
|
if launch_new {
|
||||||
// we do this to make sure the plugin is unique (has a unique configuration parameter) so
|
// 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
|
// that a new one would be launched, but we'll still send it to the same instance rather
|
||||||
|
|
@ -116,27 +122,31 @@ fn pipe_client(
|
||||||
None,
|
None,
|
||||||
)
|
)
|
||||||
};
|
};
|
||||||
|
let is_piped = !os_input.stdin_is_terminal();
|
||||||
loop {
|
loop {
|
||||||
if payload.is_some() {
|
if let Some(payload) = payload.take() {
|
||||||
// we got payload from the command line, we should use it and not wait for more
|
let msg = create_msg(Some(payload));
|
||||||
let msg = create_msg(payload);
|
|
||||||
os_input.send_to_server(msg);
|
os_input.send_to_server(msg);
|
||||||
break;
|
} 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);
|
||||||
|
} else {
|
||||||
// we didn't get payload from the command line, meaning we listen on STDIN because this
|
// 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 ...)
|
// signifies the user is about to pipe more (eg. cat my-large-file | zellij pipe ...)
|
||||||
let mut buffer = String::new();
|
let mut buffer = String::new();
|
||||||
let _ = stdin.read_line(&mut buffer);
|
let _ = stdin.read_line(&mut buffer);
|
||||||
if buffer.is_empty() {
|
if buffer.is_empty() {
|
||||||
// end of pipe, send an empty message down the pipe
|
// TODO: consider notifying the relevant plugin that the pipe has ended with a
|
||||||
let msg = create_msg(None);
|
// specialized message
|
||||||
os_input.send_to_server(msg);
|
|
||||||
break;
|
break;
|
||||||
} else {
|
} else {
|
||||||
// we've got data! send it down the pipe (most common)
|
// we've got data! send it down the pipe (most common)
|
||||||
let msg = create_msg(Some(buffer));
|
let msg = create_msg(Some(buffer));
|
||||||
os_input.send_to_server(msg);
|
os_input.send_to_server(msg);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
loop {
|
loop {
|
||||||
// wait for a response and act accordingly
|
// wait for a response and act accordingly
|
||||||
match os_input.recv_from_server() {
|
match os_input.recv_from_server() {
|
||||||
|
|
@ -144,8 +154,14 @@ fn pipe_client(
|
||||||
// unblock this pipe, meaning we need to stop waiting for a response and read
|
// unblock this pipe, meaning we need to stop waiting for a response and read
|
||||||
// once more from STDIN
|
// once more from STDIN
|
||||||
if pipe_name == pipe_id {
|
if pipe_name == pipe_id {
|
||||||
|
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;
|
break;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
Some((ServerToClientMsg::CliPipeOutput(pipe_name, output), _)) => {
|
Some((ServerToClientMsg::CliPipeOutput(pipe_name, output), _)) => {
|
||||||
// send data to STDOUT, this *does not* mean we need to unblock the input
|
// send data to STDOUT, this *does not* mean we need to unblock the input
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,7 @@ use nix::pty::Winsize;
|
||||||
use nix::sys::termios;
|
use nix::sys::termios;
|
||||||
use signal_hook::{consts::signal::*, iterator::Signals};
|
use signal_hook::{consts::signal::*, iterator::Signals};
|
||||||
use std::io::prelude::*;
|
use std::io::prelude::*;
|
||||||
|
use std::io::IsTerminal;
|
||||||
use std::os::unix::io::RawFd;
|
use std::os::unix::io::RawFd;
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
use std::sync::{Arc, Mutex};
|
use std::sync::{Arc, Mutex};
|
||||||
|
|
@ -97,6 +98,12 @@ pub trait ClientOsApi: Send + Sync {
|
||||||
fn get_stdout_writer(&self) -> Box<dyn io::Write>;
|
fn get_stdout_writer(&self) -> Box<dyn io::Write>;
|
||||||
/// Returns a BufReader that allows to read from STDIN line by line, also locks STDIN
|
/// 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 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);
|
fn update_session_name(&mut self, new_session_name: String);
|
||||||
/// Returns the raw contents of standard input.
|
/// Returns the raw contents of standard input.
|
||||||
fn read_from_stdin(&mut self) -> Result<Vec<u8>, &'static str>;
|
fn read_from_stdin(&mut self) -> Result<Vec<u8>, &'static str>;
|
||||||
|
|
@ -193,6 +200,16 @@ impl ClientOsApi for ClientOsInputOutput {
|
||||||
Box::new(stdin.lock())
|
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) {
|
fn send_to_server(&self, msg: ClientToServerMsg) {
|
||||||
// TODO: handle the error here, right now we silently ignore it
|
// TODO: handle the error here, right now we silently ignore it
|
||||||
let _ = self
|
let _ = self
|
||||||
|
|
|
||||||
|
|
@ -127,6 +127,7 @@ pub enum PluginInstruction {
|
||||||
message: MessageToPlugin,
|
message: MessageToPlugin,
|
||||||
},
|
},
|
||||||
UnblockCliPipes(Vec<PluginRenderAsset>),
|
UnblockCliPipes(Vec<PluginRenderAsset>),
|
||||||
|
WatchFilesystem,
|
||||||
Exit,
|
Exit,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -162,6 +163,7 @@ impl From<&PluginInstruction> for PluginContext {
|
||||||
PluginInstruction::CachePluginEvents { .. } => PluginContext::CachePluginEvents,
|
PluginInstruction::CachePluginEvents { .. } => PluginContext::CachePluginEvents,
|
||||||
PluginInstruction::MessageFromPlugin { .. } => PluginContext::MessageFromPlugin,
|
PluginInstruction::MessageFromPlugin { .. } => PluginContext::MessageFromPlugin,
|
||||||
PluginInstruction::UnblockCliPipes { .. } => PluginContext::UnblockCliPipes,
|
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())?;
|
wasm_bridge.update_plugins(updates, shutdown_send.clone())?;
|
||||||
},
|
},
|
||||||
PluginInstruction::PluginSubscribedToEvents(_plugin_id, _client_id, events) => {
|
PluginInstruction::PluginSubscribedToEvents(_plugin_id, _client_id, events) => {
|
||||||
for event in events {
|
// no-op, there used to be stuff we did here - now there isn't, but we might want
|
||||||
if let EventType::FileSystemCreate
|
// to add stuff here in the future
|
||||||
| EventType::FileSystemRead
|
|
||||||
| EventType::FileSystemUpdate
|
|
||||||
| EventType::FileSystemDelete = event
|
|
||||||
{
|
|
||||||
wasm_bridge.start_fs_watcher_if_not_started();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
PluginInstruction::PermissionRequestResult(
|
PluginInstruction::PermissionRequestResult(
|
||||||
plugin_id,
|
plugin_id,
|
||||||
|
|
@ -634,6 +629,9 @@ pub(crate) fn plugin_thread_main(
|
||||||
.context("failed to unblock input pipe");
|
.context("failed to unblock input pipe");
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
PluginInstruction::WatchFilesystem => {
|
||||||
|
wasm_bridge.start_fs_watcher_if_not_started();
|
||||||
|
},
|
||||||
PluginInstruction::Exit => {
|
PluginInstruction::Exit => {
|
||||||
break;
|
break;
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -580,6 +580,7 @@ impl WasmBridge {
|
||||||
&mut running_plugin,
|
&mut running_plugin,
|
||||||
&event,
|
&event,
|
||||||
&mut plugin_render_assets,
|
&mut plugin_render_assets,
|
||||||
|
senders.clone(),
|
||||||
) {
|
) {
|
||||||
Ok(()) => {
|
Ok(()) => {
|
||||||
let _ = senders.send_to_screen(ScreenInstruction::PluginBytes(
|
let _ = senders.send_to_screen(ScreenInstruction::PluginBytes(
|
||||||
|
|
@ -830,6 +831,7 @@ impl WasmBridge {
|
||||||
&mut running_plugin,
|
&mut running_plugin,
|
||||||
&event,
|
&event,
|
||||||
&mut plugin_render_assets,
|
&mut plugin_render_assets,
|
||||||
|
senders.clone(),
|
||||||
) {
|
) {
|
||||||
Ok(()) => {
|
Ok(()) => {
|
||||||
let _ = senders.send_to_screen(
|
let _ = senders.send_to_screen(
|
||||||
|
|
@ -1261,6 +1263,7 @@ pub fn apply_event_to_plugin(
|
||||||
running_plugin: &mut RunningPlugin,
|
running_plugin: &mut RunningPlugin,
|
||||||
event: &Event,
|
event: &Event,
|
||||||
plugin_render_assets: &mut Vec<PluginRenderAsset>,
|
plugin_render_assets: &mut Vec<PluginRenderAsset>,
|
||||||
|
senders: ThreadSenders,
|
||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
let instance = &running_plugin.instance;
|
let instance = &running_plugin.instance;
|
||||||
let plugin_env = &running_plugin.plugin_env;
|
let plugin_env = &running_plugin.plugin_env;
|
||||||
|
|
@ -1315,6 +1318,17 @@ pub fn apply_event_to_plugin(
|
||||||
)
|
)
|
||||||
.with_pipes(pipes_to_block_or_unblock);
|
.with_pipes(pipes_to_block_or_unblock);
|
||||||
plugin_render_assets.push(plugin_render_asset);
|
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) => {
|
(PermissionStatus::Denied, permission) => {
|
||||||
|
|
|
||||||
|
|
@ -90,11 +90,36 @@ pub fn watch_filesystem(
|
||||||
.collect()
|
.collect()
|
||||||
})
|
})
|
||||||
.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![
|
let _ = senders.send_to_plugin(PluginInstruction::Update(vec![
|
||||||
(None, None, Event::FileSystemRead(read_paths)),
|
(
|
||||||
(None, None, Event::FileSystemCreate(create_paths)),
|
None,
|
||||||
(None, None, Event::FileSystemUpdate(update_paths)),
|
None,
|
||||||
(None, None, Event::FileSystemDelete(delete_paths)),
|
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
|
Err(errors) => errors
|
||||||
|
|
|
||||||
|
|
@ -259,6 +259,10 @@ fn host_run_plugin_command(env: FunctionEnvMut<ForeignFunctionEnv>) {
|
||||||
PluginCommand::MessageToPlugin(message) => message_to_plugin(env, message)?,
|
PluginCommand::MessageToPlugin(message) => message_to_plugin(env, message)?,
|
||||||
PluginCommand::DisconnectOtherClients => disconnect_other_clients(env),
|
PluginCommand::DisconnectOtherClients => disconnect_other_clients(env),
|
||||||
PluginCommand::KillSessions(session_list) => kill_sessions(session_list),
|
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) => {
|
(PermissionStatus::Denied, permission) => {
|
||||||
log::error!(
|
log::error!(
|
||||||
|
|
@ -398,6 +402,7 @@ fn get_plugin_ids(env: &ForeignFunctionEnv) {
|
||||||
let ids = PluginIds {
|
let ids = PluginIds {
|
||||||
plugin_id: env.plugin_env.plugin_id,
|
plugin_id: env.plugin_env.plugin_id,
|
||||||
zellij_pid: process::id(),
|
zellij_pid: process::id(),
|
||||||
|
initial_cwd: env.plugin_env.plugin_cwd.clone(),
|
||||||
};
|
};
|
||||||
ProtobufPluginIds::try_from(ids)
|
ProtobufPluginIds::try_from(ids)
|
||||||
.map_err(|e| anyhow!("Failed to serialized plugin ids: {}", e))
|
.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.
|
// Custom panic handler for plugins.
|
||||||
//
|
//
|
||||||
// This is called when a panic occurs in a plugin. Since most panics will likely originate in the
|
// 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() };
|
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
|
// Utility Functions
|
||||||
|
|
||||||
#[allow(unused)]
|
#[allow(unused)]
|
||||||
|
|
|
||||||
|
|
@ -109,6 +109,23 @@ pub struct PermissionRequestResultPayload {
|
||||||
pub struct FileListPayload {
|
pub struct FileListPayload {
|
||||||
#[prost(string, repeated, tag = "1")]
|
#[prost(string, repeated, tag = "1")]
|
||||||
pub paths: ::prost::alloc::vec::Vec<::prost::alloc::string::String>,
|
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)]
|
#[allow(clippy::derive_partial_eq_without_eq)]
|
||||||
#[derive(Clone, PartialEq, ::prost::Message)]
|
#[derive(Clone, PartialEq, ::prost::Message)]
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,7 @@ pub struct PluginCommand {
|
||||||
pub name: i32,
|
pub name: i32,
|
||||||
#[prost(
|
#[prost(
|
||||||
oneof = "plugin_command::Payload",
|
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>,
|
pub payload: ::core::option::Option<plugin_command::Payload>,
|
||||||
}
|
}
|
||||||
|
|
@ -114,6 +114,8 @@ pub mod plugin_command {
|
||||||
MessageToPluginPayload(super::MessageToPluginPayload),
|
MessageToPluginPayload(super::MessageToPluginPayload),
|
||||||
#[prost(message, tag = "60")]
|
#[prost(message, tag = "60")]
|
||||||
KillSessionsPayload(super::KillSessionsPayload),
|
KillSessionsPayload(super::KillSessionsPayload),
|
||||||
|
#[prost(string, tag = "61")]
|
||||||
|
ScanHostFolderPayload(::prost::alloc::string::String),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
#[allow(clippy::derive_partial_eq_without_eq)]
|
#[allow(clippy::derive_partial_eq_without_eq)]
|
||||||
|
|
@ -416,6 +418,8 @@ pub enum CommandName {
|
||||||
MessageToPlugin = 79,
|
MessageToPlugin = 79,
|
||||||
DisconnectOtherClients = 80,
|
DisconnectOtherClients = 80,
|
||||||
KillSessions = 81,
|
KillSessions = 81,
|
||||||
|
ScanHostFolder = 82,
|
||||||
|
WatchFilesystem = 83,
|
||||||
}
|
}
|
||||||
impl CommandName {
|
impl CommandName {
|
||||||
/// String value of the enum field names used in the ProtoBuf definition.
|
/// String value of the enum field names used in the ProtoBuf definition.
|
||||||
|
|
@ -506,6 +510,8 @@ impl CommandName {
|
||||||
CommandName::MessageToPlugin => "MessageToPlugin",
|
CommandName::MessageToPlugin => "MessageToPlugin",
|
||||||
CommandName::DisconnectOtherClients => "DisconnectOtherClients",
|
CommandName::DisconnectOtherClients => "DisconnectOtherClients",
|
||||||
CommandName::KillSessions => "KillSessions",
|
CommandName::KillSessions => "KillSessions",
|
||||||
|
CommandName::ScanHostFolder => "ScanHostFolder",
|
||||||
|
CommandName::WatchFilesystem => "WatchFilesystem",
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
/// Creates an enum from field names used in the ProtoBuf definition.
|
/// Creates an enum from field names used in the ProtoBuf definition.
|
||||||
|
|
@ -593,6 +599,8 @@ impl CommandName {
|
||||||
"MessageToPlugin" => Some(Self::MessageToPlugin),
|
"MessageToPlugin" => Some(Self::MessageToPlugin),
|
||||||
"DisconnectOtherClients" => Some(Self::DisconnectOtherClients),
|
"DisconnectOtherClients" => Some(Self::DisconnectOtherClients),
|
||||||
"KillSessions" => Some(Self::KillSessions),
|
"KillSessions" => Some(Self::KillSessions),
|
||||||
|
"ScanHostFolder" => Some(Self::ScanHostFolder),
|
||||||
|
"WatchFilesystem" => Some(Self::WatchFilesystem),
|
||||||
_ => None,
|
_ => None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,8 @@ pub struct PluginIds {
|
||||||
pub plugin_id: i32,
|
pub plugin_id: i32,
|
||||||
#[prost(int32, tag = "2")]
|
#[prost(int32, tag = "2")]
|
||||||
pub zellij_pid: i32,
|
pub zellij_pid: i32,
|
||||||
|
#[prost(string, tag = "3")]
|
||||||
|
pub initial_cwd: ::prost::alloc::string::String,
|
||||||
}
|
}
|
||||||
#[allow(clippy::derive_partial_eq_without_eq)]
|
#[allow(clippy::derive_partial_eq_without_eq)]
|
||||||
#[derive(Clone, PartialEq, ::prost::Message)]
|
#[derive(Clone, PartialEq, ::prost::Message)]
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@ use clap::ArgEnum;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use std::collections::{BTreeMap, HashMap, HashSet};
|
use std::collections::{BTreeMap, HashMap, HashSet};
|
||||||
use std::fmt;
|
use std::fmt;
|
||||||
|
use std::fs::Metadata;
|
||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
use std::str::FromStr;
|
use std::str::FromStr;
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
|
@ -458,6 +459,25 @@ pub enum Mouse {
|
||||||
Release(isize, usize), // line and column
|
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`.
|
/// 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.
|
/// Once subscribed to, they will trigger the `update` method of the `ZellijPlugin` trait.
|
||||||
#[derive(Debug, Clone, PartialEq, EnumDiscriminants, ToString, Serialize, Deserialize)]
|
#[derive(Debug, Clone, PartialEq, EnumDiscriminants, ToString, Serialize, Deserialize)]
|
||||||
|
|
@ -488,13 +508,13 @@ pub enum Event {
|
||||||
String, // payload
|
String, // payload
|
||||||
),
|
),
|
||||||
/// A file was created somewhere in the Zellij CWD folder
|
/// 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
|
/// 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
|
/// 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
|
/// A file was deleted somewhere in the Zellij CWD folder
|
||||||
FileSystemDelete(Vec<PathBuf>),
|
FileSystemDelete(Vec<(PathBuf, Option<FileMetadata>)>),
|
||||||
/// A Result of plugin permission request
|
/// A Result of plugin permission request
|
||||||
PermissionRequestResult(PermissionStatus),
|
PermissionRequestResult(PermissionStatus),
|
||||||
SessionUpdate(
|
SessionUpdate(
|
||||||
|
|
@ -904,10 +924,11 @@ pub struct PaneInfo {
|
||||||
pub is_selectable: bool,
|
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 struct PluginIds {
|
||||||
pub plugin_id: u32,
|
pub plugin_id: u32,
|
||||||
pub zellij_pid: u32,
|
pub zellij_pid: u32,
|
||||||
|
pub initial_cwd: PathBuf,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Tag used to identify the plugin in layout and config kdl files
|
/// Tag used to identify the plugin in layout and config kdl files
|
||||||
|
|
@ -1350,4 +1371,6 @@ pub enum PluginCommand {
|
||||||
MessageToPlugin(MessageToPlugin),
|
MessageToPlugin(MessageToPlugin),
|
||||||
DisconnectOtherClients,
|
DisconnectOtherClients,
|
||||||
KillSessions(Vec<String>), // one or more session names
|
KillSessions(Vec<String>), // one or more session names
|
||||||
|
ScanHostFolder(PathBuf), // TODO: rename to ScanHostFolder
|
||||||
|
WatchFilesystem,
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -400,6 +400,7 @@ pub enum PluginContext {
|
||||||
CachePluginEvents,
|
CachePluginEvents,
|
||||||
MessageFromPlugin,
|
MessageFromPlugin,
|
||||||
UnblockCliPipes,
|
UnblockCliPipes,
|
||||||
|
WatchFilesystem,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Stack call representations corresponding to the different types of [`ClientInstruction`]s.
|
/// 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 alias_cwd = cwd.clone().map(|cwd| current_dir.join(cwd));
|
||||||
let cwd = cwd
|
let cwd = cwd
|
||||||
.map(|cwd| current_dir.join(cwd))
|
.map(|cwd| current_dir.join(cwd))
|
||||||
.or_else(|| Some(current_dir));
|
.or_else(|| Some(current_dir.clone()));
|
||||||
if let Some(plugin) = plugin {
|
if let Some(plugin) = plugin {
|
||||||
let plugin = match RunPluginLocation::parse(&plugin, cwd.clone()) {
|
let plugin = match RunPluginLocation::parse(&plugin, cwd.clone()) {
|
||||||
Ok(location) => {
|
Ok(location) => {
|
||||||
|
|
@ -365,11 +365,17 @@ impl Action {
|
||||||
initial_cwd: cwd.clone(),
|
initial_cwd: cwd.clone(),
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
Err(_) => RunPluginOrAlias::Alias(PluginAlias::new(
|
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,
|
&plugin,
|
||||||
&configuration.map(|c| c.inner().clone()),
|
&Some(user_configuration),
|
||||||
alias_cwd,
|
alias_cwd,
|
||||||
)),
|
))
|
||||||
|
},
|
||||||
};
|
};
|
||||||
if floating {
|
if floating {
|
||||||
Ok(vec![Action::NewFloatingPluginPane(
|
Ok(vec![Action::NewFloatingPluginPane(
|
||||||
|
|
|
||||||
|
|
@ -103,6 +103,15 @@ message PermissionRequestResultPayload {
|
||||||
|
|
||||||
message FileListPayload {
|
message FileListPayload {
|
||||||
repeated string paths = 1;
|
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 {
|
message CustomMessagePayload {
|
||||||
|
|
|
||||||
|
|
@ -3,10 +3,11 @@ pub use super::generated_api::api::{
|
||||||
event::{
|
event::{
|
||||||
event::Payload as ProtobufEventPayload, CopyDestination as ProtobufCopyDestination,
|
event::Payload as ProtobufEventPayload, CopyDestination as ProtobufCopyDestination,
|
||||||
Event as ProtobufEvent, EventNameList as ProtobufEventNameList,
|
Event as ProtobufEvent, EventNameList as ProtobufEventNameList,
|
||||||
EventType as ProtobufEventType, InputModeKeybinds as ProtobufInputModeKeybinds,
|
EventType as ProtobufEventType, FileMetadata as ProtobufFileMetadata,
|
||||||
KeyBind as ProtobufKeyBind, LayoutInfo as ProtobufLayoutInfo,
|
InputModeKeybinds as ProtobufInputModeKeybinds, KeyBind as ProtobufKeyBind,
|
||||||
ModeUpdatePayload as ProtobufModeUpdatePayload, PaneInfo as ProtobufPaneInfo,
|
LayoutInfo as ProtobufLayoutInfo, ModeUpdatePayload as ProtobufModeUpdatePayload,
|
||||||
PaneManifest as ProtobufPaneManifest, ResurrectableSession as ProtobufResurrectableSession,
|
PaneInfo as ProtobufPaneInfo, PaneManifest as ProtobufPaneManifest,
|
||||||
|
ResurrectableSession as ProtobufResurrectableSession,
|
||||||
SessionManifest as ProtobufSessionManifest, TabInfo as ProtobufTabInfo, *,
|
SessionManifest as ProtobufSessionManifest, TabInfo as ProtobufTabInfo, *,
|
||||||
},
|
},
|
||||||
input_mode::InputMode as ProtobufInputMode,
|
input_mode::InputMode as ProtobufInputMode,
|
||||||
|
|
@ -14,8 +15,8 @@ pub use super::generated_api::api::{
|
||||||
style::Style as ProtobufStyle,
|
style::Style as ProtobufStyle,
|
||||||
};
|
};
|
||||||
use crate::data::{
|
use crate::data::{
|
||||||
CopyDestination, Event, EventType, InputMode, Key, LayoutInfo, ModeInfo, Mouse, PaneInfo,
|
CopyDestination, Event, EventType, FileMetadata, InputMode, Key, LayoutInfo, ModeInfo, Mouse,
|
||||||
PaneManifest, PermissionStatus, PluginCapabilities, SessionInfo, Style, TabInfo,
|
PaneInfo, PaneManifest, PermissionStatus, PluginCapabilities, SessionInfo, Style, TabInfo,
|
||||||
};
|
};
|
||||||
|
|
||||||
use crate::errors::prelude::*;
|
use crate::errors::prelude::*;
|
||||||
|
|
@ -124,7 +125,8 @@ impl TryFrom<ProtobufEvent> for Event {
|
||||||
let file_paths = file_list_payload
|
let file_paths = file_list_payload
|
||||||
.paths
|
.paths
|
||||||
.iter()
|
.iter()
|
||||||
.map(|p| PathBuf::from(p))
|
.zip(file_list_payload.paths_metadata.iter())
|
||||||
|
.map(|(p, m)| (PathBuf::from(p), m.into()))
|
||||||
.collect();
|
.collect();
|
||||||
Ok(Event::FileSystemCreate(file_paths))
|
Ok(Event::FileSystemCreate(file_paths))
|
||||||
},
|
},
|
||||||
|
|
@ -135,7 +137,8 @@ impl TryFrom<ProtobufEvent> for Event {
|
||||||
let file_paths = file_list_payload
|
let file_paths = file_list_payload
|
||||||
.paths
|
.paths
|
||||||
.iter()
|
.iter()
|
||||||
.map(|p| PathBuf::from(p))
|
.zip(file_list_payload.paths_metadata.iter())
|
||||||
|
.map(|(p, m)| (PathBuf::from(p), m.into()))
|
||||||
.collect();
|
.collect();
|
||||||
Ok(Event::FileSystemRead(file_paths))
|
Ok(Event::FileSystemRead(file_paths))
|
||||||
},
|
},
|
||||||
|
|
@ -146,7 +149,8 @@ impl TryFrom<ProtobufEvent> for Event {
|
||||||
let file_paths = file_list_payload
|
let file_paths = file_list_payload
|
||||||
.paths
|
.paths
|
||||||
.iter()
|
.iter()
|
||||||
.map(|p| PathBuf::from(p))
|
.zip(file_list_payload.paths_metadata.iter())
|
||||||
|
.map(|(p, m)| (PathBuf::from(p), m.into()))
|
||||||
.collect();
|
.collect();
|
||||||
Ok(Event::FileSystemUpdate(file_paths))
|
Ok(Event::FileSystemUpdate(file_paths))
|
||||||
},
|
},
|
||||||
|
|
@ -157,7 +161,8 @@ impl TryFrom<ProtobufEvent> for Event {
|
||||||
let file_paths = file_list_payload
|
let file_paths = file_list_payload
|
||||||
.paths
|
.paths
|
||||||
.iter()
|
.iter()
|
||||||
.map(|p| PathBuf::from(p))
|
.zip(file_list_payload.paths_metadata.iter())
|
||||||
|
.map(|(p, m)| (PathBuf::from(p), m.into()))
|
||||||
.collect();
|
.collect();
|
||||||
Ok(Event::FileSystemDelete(file_paths))
|
Ok(Event::FileSystemDelete(file_paths))
|
||||||
},
|
},
|
||||||
|
|
@ -322,36 +327,64 @@ impl TryFrom<Event> for ProtobufEvent {
|
||||||
payload,
|
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 {
|
let file_list_payload = FileListPayload {
|
||||||
paths: paths.iter().map(|p| p.display().to_string()).collect(),
|
paths,
|
||||||
|
paths_metadata,
|
||||||
};
|
};
|
||||||
Ok(ProtobufEvent {
|
Ok(ProtobufEvent {
|
||||||
name: ProtobufEventType::FileSystemCreate as i32,
|
name: ProtobufEventType::FileSystemCreate as i32,
|
||||||
payload: Some(event::Payload::FileListPayload(file_list_payload)),
|
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 {
|
let file_list_payload = FileListPayload {
|
||||||
paths: paths.iter().map(|p| p.display().to_string()).collect(),
|
paths,
|
||||||
|
paths_metadata,
|
||||||
};
|
};
|
||||||
Ok(ProtobufEvent {
|
Ok(ProtobufEvent {
|
||||||
name: ProtobufEventType::FileSystemRead as i32,
|
name: ProtobufEventType::FileSystemRead as i32,
|
||||||
payload: Some(event::Payload::FileListPayload(file_list_payload)),
|
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 {
|
let file_list_payload = FileListPayload {
|
||||||
paths: paths.iter().map(|p| p.display().to_string()).collect(),
|
paths,
|
||||||
|
paths_metadata,
|
||||||
};
|
};
|
||||||
Ok(ProtobufEvent {
|
Ok(ProtobufEvent {
|
||||||
name: ProtobufEventType::FileSystemUpdate as i32,
|
name: ProtobufEventType::FileSystemUpdate as i32,
|
||||||
payload: Some(event::Payload::FileListPayload(file_list_payload)),
|
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 {
|
let file_list_payload = FileListPayload {
|
||||||
paths: paths.iter().map(|p| p.display().to_string()).collect(),
|
paths,
|
||||||
|
paths_metadata,
|
||||||
};
|
};
|
||||||
Ok(ProtobufEvent {
|
Ok(ProtobufEvent {
|
||||||
name: ProtobufEventType::FileSystemDelete as i32,
|
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]
|
#[test]
|
||||||
fn serialize_mode_update_event() {
|
fn serialize_mode_update_event() {
|
||||||
use prost::Message;
|
use prost::Message;
|
||||||
|
|
@ -1256,8 +1322,10 @@ fn serialize_custom_message_event() {
|
||||||
#[test]
|
#[test]
|
||||||
fn serialize_file_system_create_event() {
|
fn serialize_file_system_create_event() {
|
||||||
use prost::Message;
|
use prost::Message;
|
||||||
let file_system_event =
|
let file_system_event = Event::FileSystemCreate(vec![
|
||||||
Event::FileSystemCreate(vec!["/absolute/path".into(), "./relative_path".into()]);
|
("/absolute/path".into(), None),
|
||||||
|
("./relative_path".into(), Default::default()),
|
||||||
|
]);
|
||||||
let protobuf_event: ProtobufEvent = file_system_event.clone().try_into().unwrap();
|
let protobuf_event: ProtobufEvent = file_system_event.clone().try_into().unwrap();
|
||||||
let serialized_protobuf_event = protobuf_event.encode_to_vec();
|
let serialized_protobuf_event = protobuf_event.encode_to_vec();
|
||||||
let deserialized_protobuf_event: ProtobufEvent =
|
let deserialized_protobuf_event: ProtobufEvent =
|
||||||
|
|
@ -1272,8 +1340,10 @@ fn serialize_file_system_create_event() {
|
||||||
#[test]
|
#[test]
|
||||||
fn serialize_file_system_read_event() {
|
fn serialize_file_system_read_event() {
|
||||||
use prost::Message;
|
use prost::Message;
|
||||||
let file_system_event =
|
let file_system_event = Event::FileSystemRead(vec![
|
||||||
Event::FileSystemRead(vec!["/absolute/path".into(), "./relative_path".into()]);
|
("/absolute/path".into(), None),
|
||||||
|
("./relative_path".into(), Default::default()),
|
||||||
|
]);
|
||||||
let protobuf_event: ProtobufEvent = file_system_event.clone().try_into().unwrap();
|
let protobuf_event: ProtobufEvent = file_system_event.clone().try_into().unwrap();
|
||||||
let serialized_protobuf_event = protobuf_event.encode_to_vec();
|
let serialized_protobuf_event = protobuf_event.encode_to_vec();
|
||||||
let deserialized_protobuf_event: ProtobufEvent =
|
let deserialized_protobuf_event: ProtobufEvent =
|
||||||
|
|
@ -1288,8 +1358,10 @@ fn serialize_file_system_read_event() {
|
||||||
#[test]
|
#[test]
|
||||||
fn serialize_file_system_update_event() {
|
fn serialize_file_system_update_event() {
|
||||||
use prost::Message;
|
use prost::Message;
|
||||||
let file_system_event =
|
let file_system_event = Event::FileSystemUpdate(vec![
|
||||||
Event::FileSystemUpdate(vec!["/absolute/path".into(), "./relative_path".into()]);
|
("/absolute/path".into(), None),
|
||||||
|
("./relative_path".into(), Some(Default::default())),
|
||||||
|
]);
|
||||||
let protobuf_event: ProtobufEvent = file_system_event.clone().try_into().unwrap();
|
let protobuf_event: ProtobufEvent = file_system_event.clone().try_into().unwrap();
|
||||||
let serialized_protobuf_event = protobuf_event.encode_to_vec();
|
let serialized_protobuf_event = protobuf_event.encode_to_vec();
|
||||||
let deserialized_protobuf_event: ProtobufEvent =
|
let deserialized_protobuf_event: ProtobufEvent =
|
||||||
|
|
@ -1304,8 +1376,10 @@ fn serialize_file_system_update_event() {
|
||||||
#[test]
|
#[test]
|
||||||
fn serialize_file_system_delete_event() {
|
fn serialize_file_system_delete_event() {
|
||||||
use prost::Message;
|
use prost::Message;
|
||||||
let file_system_event =
|
let file_system_event = Event::FileSystemDelete(vec![
|
||||||
Event::FileSystemDelete(vec!["/absolute/path".into(), "./relative_path".into()]);
|
("/absolute/path".into(), None),
|
||||||
|
("./relative_path".into(), Default::default()),
|
||||||
|
]);
|
||||||
let protobuf_event: ProtobufEvent = file_system_event.clone().try_into().unwrap();
|
let protobuf_event: ProtobufEvent = file_system_event.clone().try_into().unwrap();
|
||||||
let serialized_protobuf_event = protobuf_event.encode_to_vec();
|
let serialized_protobuf_event = protobuf_event.encode_to_vec();
|
||||||
let deserialized_protobuf_event: ProtobufEvent =
|
let deserialized_protobuf_event: ProtobufEvent =
|
||||||
|
|
|
||||||
|
|
@ -93,6 +93,8 @@ enum CommandName {
|
||||||
MessageToPlugin = 79;
|
MessageToPlugin = 79;
|
||||||
DisconnectOtherClients = 80;
|
DisconnectOtherClients = 80;
|
||||||
KillSessions = 81;
|
KillSessions = 81;
|
||||||
|
ScanHostFolder = 82;
|
||||||
|
WatchFilesystem = 83;
|
||||||
}
|
}
|
||||||
|
|
||||||
message PluginCommand {
|
message PluginCommand {
|
||||||
|
|
@ -148,6 +150,7 @@ message PluginCommand {
|
||||||
CliPipeOutputPayload cli_pipe_output_payload = 49;
|
CliPipeOutputPayload cli_pipe_output_payload = 49;
|
||||||
MessageToPluginPayload message_to_plugin_payload = 50;
|
MessageToPluginPayload message_to_plugin_payload = 50;
|
||||||
KillSessionsPayload kill_sessions_payload = 60;
|
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 })) => {
|
Some(Payload::KillSessionsPayload(KillSessionsPayload { session_names })) => {
|
||||||
Ok(PluginCommand::KillSessions(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"),
|
None => Err("Unrecognized plugin command"),
|
||||||
}
|
}
|
||||||
|
|
@ -1361,6 +1371,16 @@ impl TryFrom<PluginCommand> for ProtobufPluginCommand {
|
||||||
session_names,
|
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 {
|
message PluginIds {
|
||||||
int32 plugin_id = 1;
|
int32 plugin_id = 1;
|
||||||
int32 zellij_pid = 2;
|
int32 zellij_pid = 2;
|
||||||
|
string initial_cwd = 3;
|
||||||
}
|
}
|
||||||
|
|
||||||
message ZellijVersion {
|
message ZellijVersion {
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@ pub use super::generated_api::api::plugin_ids::{
|
||||||
use crate::data::PluginIds;
|
use crate::data::PluginIds;
|
||||||
|
|
||||||
use std::convert::TryFrom;
|
use std::convert::TryFrom;
|
||||||
|
use std::path::PathBuf;
|
||||||
|
|
||||||
impl TryFrom<ProtobufPluginIds> for PluginIds {
|
impl TryFrom<ProtobufPluginIds> for PluginIds {
|
||||||
type Error = &'static str;
|
type Error = &'static str;
|
||||||
|
|
@ -11,6 +12,7 @@ impl TryFrom<ProtobufPluginIds> for PluginIds {
|
||||||
Ok(PluginIds {
|
Ok(PluginIds {
|
||||||
plugin_id: protobuf_plugin_ids.plugin_id as u32,
|
plugin_id: protobuf_plugin_ids.plugin_id as u32,
|
||||||
zellij_pid: protobuf_plugin_ids.zellij_pid 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 {
|
Ok(ProtobufPluginIds {
|
||||||
plugin_id: plugin_ids.plugin_id as i32,
|
plugin_id: plugin_ids.plugin_id as i32,
|
||||||
zellij_pid: plugin_ids.zellij_pid 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