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:
Aram Drevekenin 2024-03-18 09:19:58 +01:00 committed by GitHub
parent 12daac3b54
commit ee16a4b8c3
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
32 changed files with 1397 additions and 291 deletions

5
Cargo.lock generated
View file

@ -3338,6 +3338,7 @@ dependencies = [
"fuzzy-matcher",
"humantime",
"unicode-width",
"uuid",
"zellij-tile",
]
@ -4191,9 +4192,9 @@ checksum = "936e4b492acfd135421d8dca4b1aa80a7bfc26e702ef3af710e0752684df5372"
[[package]]
name = "uuid"
version = "1.4.1"
version = "1.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "79daa5ed5740825c40b389c5e50312b9c86df53fccd33f281df655642b43869d"
checksum = "f00cc9702ca12d3c81455259621e676d0f7251cec66a21e98fe2e9a37db93b2a"
dependencies = [
"getrandom 0.2.10",
"serde",

View file

@ -67,6 +67,7 @@ impl ZellijPlugin for State {
EventType::FileSystemUpdate,
EventType::FileSystemDelete,
]);
watch_filesystem();
}
fn update(&mut self, event: Event) -> bool {

View file

@ -11,3 +11,4 @@ chrono = "0.4.0"
fuzzy-matcher = "0.3.7"
unicode-width = "0.1.10"
humantime = "2.1.0"
uuid = { version = "1.7.0", features = ["v4"] }

View file

@ -2,9 +2,9 @@ mod new_session_info;
mod resurrectable_sessions;
mod session_list;
mod ui;
use zellij_tile::prelude::*;
use std::collections::BTreeMap;
use uuid::Uuid;
use zellij_tile::prelude::*;
use new_session_info::NewSessionInfo;
use ui::{
@ -45,6 +45,7 @@ struct State {
colors: Colors,
is_welcome_screen: bool,
show_kill_all_sessions_warning: bool,
request_ids: Vec<String>,
}
register_plugin!(State);
@ -66,6 +67,28 @@ impl ZellijPlugin for State {
]);
}
fn pipe(&mut self, pipe_message: PipeMessage) -> bool {
if pipe_message.name == "filepicker_result" {
match (pipe_message.payload, pipe_message.args.get("request_id")) {
(Some(payload), Some(request_id)) => {
match self.request_ids.iter().position(|p| p == request_id) {
Some(request_id_position) => {
self.request_ids.remove(request_id_position);
let new_session_folder = std::path::PathBuf::from(payload);
self.new_session_info.new_session_folder = Some(new_session_folder);
},
None => {
eprintln!("request id not found");
},
}
},
_ => {},
}
true
} else {
false
}
}
fn update(&mut self, event: Event) -> bool {
let mut should_render = false;
match event {
@ -109,7 +132,7 @@ impl ZellijPlugin for State {
render_new_session_block(
&self.new_session_info,
self.colors,
height,
height.saturating_sub(2),
width,
x,
y + 2,
@ -184,12 +207,33 @@ impl State {
} else if let Key::Ctrl('w') = key {
self.active_screen = ActiveScreen::NewSession;
should_render = true;
} else if let Key::Ctrl('c') = key {
self.new_session_info.handle_key(key);
should_render = true;
} else if let Key::BackTab = key {
self.toggle_active_screen();
should_render = true;
} else if let Key::Ctrl('f') = key {
let request_id = Uuid::new_v4();
let mut config = BTreeMap::new();
let mut args = BTreeMap::new();
self.request_ids.push(request_id.to_string());
// we insert this into the config so that a new plugin will be opened (the plugin's
// uniqueness is determined by its name/url as well as its config)
config.insert("request_id".to_owned(), request_id.to_string());
// we also insert this into the args so that the plugin will have an easier access to
// it
args.insert("request_id".to_owned(), request_id.to_string());
pipe_message_to_plugin(
MessageToPlugin::new("filepicker")
.with_plugin_url("filepicker")
.with_plugin_config(config)
.new_plugin_instance_should_have_pane_title(
"Select folder for the new session...",
)
.with_args(args),
);
should_render = true;
} else if let Key::Ctrl('c') = key {
self.new_session_info.new_session_folder = None;
should_render = true;
} else if let Key::Esc = key {
self.new_session_info.handle_key(key);
should_render = true;

View file

@ -1,6 +1,7 @@
use fuzzy_matcher::skim::SkimMatcherV2;
use fuzzy_matcher::FuzzyMatcher;
use std::cmp::Ordering;
use std::path::PathBuf;
use zellij_tile::prelude::*;
#[derive(Default)]
@ -8,6 +9,7 @@ pub struct NewSessionInfo {
name: String,
layout_list: LayoutList,
entering_new_session_info: EnteringState,
pub new_session_folder: Option<PathBuf>,
}
#[derive(Eq, PartialEq)]
@ -104,7 +106,8 @@ impl NewSessionInfo {
if new_session_name != current_session_name.as_ref().map(|s| s.as_str()) {
match new_session_layout {
Some(new_session_layout) => {
switch_session_with_layout(new_session_name, new_session_layout, None)
let cwd = self.new_session_folder.as_ref().map(|c| PathBuf::from(c));
switch_session_with_layout(new_session_name, new_session_layout, cwd)
},
None => {
switch_session(new_session_name);

View file

@ -1,3 +1,4 @@
use std::path::PathBuf;
use unicode_width::UnicodeWidthChar;
use unicode_width::UnicodeWidthStr;
use zellij_tile::prelude::*;
@ -519,9 +520,6 @@ pub fn render_screen_toggle(active_screen: ActiveScreen, x: usize, y: usize, max
let key_indication_len = key_indication_text.chars().count() + 1;
let first_ribbon_length = new_session_text.chars().count() + 4;
let second_ribbon_length = running_sessions_text.chars().count() + 4;
let third_ribbon_length = exited_sessions_text.chars().count() + 4;
let total_len =
key_indication_len + first_ribbon_length + second_ribbon_length + third_ribbon_length;
let key_indication_x = x;
let first_ribbon_x = key_indication_x + key_indication_len;
let second_ribbon_x = first_ribbon_x + first_ribbon_length;
@ -552,6 +550,140 @@ pub fn render_screen_toggle(active_screen: ActiveScreen, x: usize, y: usize, max
print_ribbon_with_coordinates(exited_sessions_text, third_ribbon_x, y, None, None);
}
fn render_new_session_folder_prompt(
new_session_info: &NewSessionInfo,
colors: Colors,
x: usize,
y: usize,
max_cols: usize,
) {
match new_session_info.new_session_folder.as_ref() {
Some(new_session_folder) => {
let folder_prompt = "New session folder:";
let short_folder_prompt = "Folder:";
let new_session_path = new_session_folder.clone();
let new_session_folder = new_session_folder.display().to_string();
let change_folder_shortcut_text = "<Ctrl f>";
let change_folder_shortcut = colors.magenta(&change_folder_shortcut_text);
let to_change = "to change";
let reset_folder_shortcut_text = "<Ctrl c>";
let reset_folder_shortcut = colors.magenta(reset_folder_shortcut_text);
let to_reset = "to reset";
if max_cols
>= folder_prompt.width()
+ new_session_folder.width()
+ change_folder_shortcut_text.width()
+ to_change.width()
+ reset_folder_shortcut_text.width()
+ to_reset.width()
+ 8
{
print!(
"\u{1b}[m{}{} {} ({} {}, {} {})",
format!("\u{1b}[{};{}H", y + 1, x + 1),
colors.green(folder_prompt),
colors.orange(&new_session_folder),
change_folder_shortcut,
to_change,
reset_folder_shortcut,
to_reset,
);
} else if max_cols
>= short_folder_prompt.width()
+ new_session_folder.width()
+ change_folder_shortcut_text.width()
+ to_change.width()
+ reset_folder_shortcut_text.width()
+ to_reset.width()
+ 8
{
print!(
"\u{1b}[m{}{} {} ({} {}, {} {})",
format!("\u{1b}[{};{}H", y + 1, x + 1),
colors.green(short_folder_prompt),
colors.orange(&new_session_folder),
change_folder_shortcut,
to_change,
reset_folder_shortcut,
to_reset,
);
} else if max_cols
>= short_folder_prompt.width()
+ new_session_folder.width()
+ change_folder_shortcut_text.width()
+ reset_folder_shortcut_text.width()
+ 5
{
print!(
"\u{1b}[m{}{} {} ({}/{})",
format!("\u{1b}[{};{}H", y + 1, x + 1),
colors.green(short_folder_prompt),
colors.orange(&new_session_folder),
change_folder_shortcut,
reset_folder_shortcut,
);
} else {
let total_len = short_folder_prompt.width()
+ change_folder_shortcut_text.width()
+ reset_folder_shortcut_text.width()
+ 5;
let max_path_len = max_cols.saturating_sub(total_len);
let truncated_path = truncate_path(
new_session_path,
new_session_folder.width().saturating_sub(max_path_len),
);
print!(
"\u{1b}[m{}{} {} ({}/{})",
format!("\u{1b}[{};{}H", y + 1, x + 1),
colors.green(short_folder_prompt),
colors.orange(&truncated_path),
change_folder_shortcut,
reset_folder_shortcut,
);
}
},
None => {
let folder_prompt = "New session folder:";
let short_folder_prompt = "Folder:";
let change_folder_shortcut_text = "<Ctrl f>";
let change_folder_shortcut = colors.magenta(change_folder_shortcut_text);
let to_set = "to set";
if max_cols
>= folder_prompt.width() + change_folder_shortcut_text.width() + to_set.width() + 4
{
print!(
"\u{1b}[m{}{} ({} {})",
format!("\u{1b}[{};{}H", y + 1, x + 1),
colors.green(folder_prompt),
change_folder_shortcut,
to_set,
);
} else if max_cols
>= short_folder_prompt.width()
+ change_folder_shortcut_text.width()
+ to_set.width()
+ 4
{
print!(
"\u{1b}[m{}{} ({} {})",
format!("\u{1b}[{};{}H", y + 1, x + 1),
colors.green(short_folder_prompt),
change_folder_shortcut,
to_set,
);
} else {
print!(
"\u{1b}[m{}{} {}",
format!("\u{1b}[{};{}H", y + 1, x + 1),
colors.green(short_folder_prompt),
change_folder_shortcut,
);
}
},
}
}
pub fn render_new_session_block(
new_session_info: &NewSessionInfo,
colors: Colors,
@ -615,6 +747,7 @@ pub fn render_new_session_block(
}
render_layout_selection_list(
new_session_info,
colors,
max_rows_of_new_session_block.saturating_sub(1),
max_cols_of_new_session_block,
x,
@ -625,6 +758,7 @@ pub fn render_new_session_block(
pub fn render_layout_selection_list(
new_session_info: &NewSessionInfo,
colors: Colors,
max_rows_of_new_session_block: usize,
max_cols_of_new_session_block: usize,
x: usize,
@ -658,7 +792,7 @@ pub fn render_layout_selection_list(
let layout_name = layout_info.name();
let layout_name_len = layout_name.width();
let is_builtin = layout_info.is_builtin();
if i > max_rows_of_new_session_block {
if i > max_rows_of_new_session_block.saturating_sub(1) {
break;
} else {
let mut layout_cell = if is_builtin {
@ -682,7 +816,15 @@ pub fn render_layout_selection_list(
table = table.add_styled_row(vec![arrow_cell, layout_cell]);
}
}
print_table_with_coordinates(table, x, y + 3, None, None);
let table_y = y + 3;
print_table_with_coordinates(table, x, table_y, None, None);
render_new_session_folder_prompt(
new_session_info,
colors,
x,
(y + max_rows_of_new_session_block).saturating_sub(3),
max_cols_of_new_session_block,
);
}
pub fn render_error(error_text: &str, rows: usize, columns: usize, x: usize, y: usize) {
@ -733,10 +875,6 @@ pub fn render_controls_line(
}
},
ActiveScreen::AttachToSession => {
let arrows = colors.magenta("<←↓↑→>");
let navigate = colors.bold("Navigate");
let enter = colors.magenta("<ENTER>");
let select = colors.bold("Attach");
let rename = colors.magenta("<Ctrl r>");
let rename_text = colors.bold("Rename");
let disconnect = colors.magenta("<Ctrl x>");
@ -817,3 +955,25 @@ impl Colors {
self.color(&self.palette.magenta, text)
}
}
fn truncate_path(path: PathBuf, mut char_count_to_remove: usize) -> String {
let mut truncated = String::new();
let component_count = path.iter().count();
for (i, component) in path.iter().enumerate() {
let mut component_str = component.to_string_lossy().to_string();
if char_count_to_remove > 0 {
truncated.push(component_str.remove(0));
if i != 0 && i + 1 != component_count {
truncated.push('/');
}
char_count_to_remove =
char_count_to_remove.saturating_sub(component_str.width().saturating_sub(1));
} else {
truncated.push_str(&component_str);
if i != 0 && i + 1 != component_count {
truncated.push('/');
}
}
}
truncated
}

View file

@ -1,2 +1,2 @@
[build]
target = "wasm32-wasi"
target = "wasm32-wasi"

View 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,
}
}
}

View file

@ -1,121 +1,122 @@
mod file_list_view;
mod search_view;
mod shared;
mod state;
use colored::*;
use state::{refresh_directory, FsEntry, State};
use crate::file_list_view::FsEntry;
use shared::{render_current_path, render_instruction_line, render_search_term};
use state::{refresh_directory, State};
use std::collections::BTreeMap;
use std::{cmp::min, time::Instant};
use std::path::PathBuf;
use zellij_tile::prelude::*;
register_plugin!(State);
impl ZellijPlugin for State {
fn load(&mut self, _configuration: BTreeMap<String, String>) {
refresh_directory(self);
fn load(&mut self, configuration: BTreeMap<String, String>) {
let plugin_ids = get_plugin_ids();
self.initial_cwd = plugin_ids.initial_cwd;
let show_hidden_files = configuration
.get("show_hidden_files")
.map(|v| v == "true")
.unwrap_or(false);
self.hide_hidden_files = !show_hidden_files;
self.close_on_selection = configuration
.get("close_on_selection")
.map(|v| v == "true")
.unwrap_or(false);
subscribe(&[
EventType::Key,
EventType::Mouse,
EventType::CustomMessage,
EventType::Timer,
EventType::FileSystemUpdate,
]);
self.file_list_view.reset_selected();
// the caller_cwd might be different from the initial_cwd if this plugin was defined as an
// alias, with access to a certain part of the file system (often broader) and was called
// from an individual pane somewhere inside this broad scope - in this case, we want to
// start in the same cwd as the caller, giving them the full access we were granted
match configuration
.get("caller_cwd")
.map(|c| PathBuf::from(c))
.and_then(|c| {
c.strip_prefix(&self.initial_cwd)
.ok()
.map(|c| PathBuf::from(c))
}) {
Some(relative_caller_path) => {
let relative_caller_path = FsEntry::Dir(relative_caller_path.to_path_buf());
self.file_list_view.enter_dir(&relative_caller_path);
refresh_directory(&self.file_list_view.path);
},
None => {
refresh_directory(&std::path::Path::new("/"));
},
}
}
fn update(&mut self, event: Event) -> bool {
let mut should_render = false;
let prev_event = if self.ev_history.len() == 2 {
self.ev_history.pop_front()
} else {
None
};
self.ev_history.push_back((event.clone(), Instant::now()));
match event {
Event::FileSystemUpdate(paths) => {
self.update_files(paths);
should_render = true;
},
Event::Key(key) => match key {
Key::Esc => {
hide_self();
Key::Char(character) if character != '\n' => {
self.update_search_term(character);
should_render = true;
},
Key::Up | Key::Char('k') => {
let currently_selected = self.selected();
*self.selected_mut() = self.selected().saturating_sub(1);
if currently_selected != self.selected() {
should_render = true;
}
},
Key::Down | Key::Char('j') => {
let currently_selected = self.selected();
let next = self.selected().saturating_add(1);
if next >= self.files.len() {
refresh_directory(self);
}
*self.selected_mut() = min(self.files.len().saturating_sub(1), next);
if currently_selected != self.selected() {
should_render = true;
}
},
Key::Right | Key::Char('\n') | Key::Char('l') if !self.files.is_empty() => {
self.traverse_dir_or_open_file();
self.ev_history.clear();
Key::Backspace => {
self.handle_backspace();
should_render = true;
},
Key::Left | Key::Char('h') => {
if self.path.components().count() > 2 {
// don't descend into /host
// the reason this is a hard-coded number (2) and not "== ROOT"
// or some such is that there are certain cases in which self.path
// is empty and this will work then too
should_render = true;
self.path.pop();
refresh_directory(self);
}
Key::Esc | Key::Ctrl('c') => {
self.clear_search_term_or_descend();
should_render = true;
},
Key::Char('.') => {
Key::Up => {
self.move_selection_up();
should_render = true;
},
Key::Down => {
self.move_selection_down();
should_render = true;
},
Key::Char('\n') if self.handling_filepick_request_from.is_some() => {
self.send_filepick_response();
},
Key::Char('\n') => {
self.open_selected_path();
},
Key::Right | Key::BackTab => {
self.traverse_dir();
should_render = true;
},
Key::Left => {
self.descend_to_previous_path();
should_render = true;
},
Key::Ctrl('e') => {
should_render = true;
self.toggle_hidden_files();
refresh_directory(self);
refresh_directory(&self.file_list_view.path);
},
_ => (),
},
Event::Mouse(mouse_event) => match mouse_event {
Mouse::ScrollDown(_) => {
let currently_selected = self.selected();
let next = self.selected().saturating_add(1);
if next >= self.files.len() {
refresh_directory(self);
}
*self.selected_mut() = min(self.files.len().saturating_sub(1), next);
if currently_selected != self.selected() {
should_render = true;
}
self.move_selection_down();
should_render = true;
},
Mouse::ScrollUp(_) => {
let currently_selected = self.selected();
*self.selected_mut() = self.selected().saturating_sub(1);
if currently_selected != self.selected() {
should_render = true;
}
self.move_selection_up();
should_render = true;
},
Mouse::Release(line, _) => {
if line < 0 {
return should_render;
}
let mut should_select = true;
if let Some((Event::Mouse(Mouse::Release(prev_line, _)), t)) = prev_event {
if prev_line == line
&& Instant::now().saturating_duration_since(t).as_millis() < 400
{
self.traverse_dir_or_open_file();
self.ev_history.clear();
should_select = false;
should_render = true;
}
}
if should_select && self.scroll() + (line as usize) < self.files.len() {
let currently_selected = self.selected();
*self.selected_mut() = self.scroll() + (line as usize);
if currently_selected != self.selected() {
should_render = true;
}
}
Mouse::LeftClick(line, _) => {
self.handle_left_click(line);
should_render = true;
},
_ => {},
},
@ -125,42 +126,37 @@ impl ZellijPlugin for State {
};
should_render
}
fn pipe(&mut self, pipe_message: PipeMessage) -> bool {
if pipe_message.is_private && pipe_message.name == "filepicker" {
if let PipeSource::Cli(pipe_id) = &pipe_message.source {
// here we block the cli pipe input because we want it to wait until the user chose
// a file
#[cfg(target_family = "wasm")]
block_cli_pipe_input(pipe_id);
}
self.handling_filepick_request_from = Some((pipe_message.source, pipe_message.args));
true
} else {
false
}
}
fn render(&mut self, rows: usize, cols: usize) {
self.current_rows = Some(rows);
for i in 0..rows {
if self.selected() < self.scroll() {
*self.scroll_mut() = self.selected();
}
if self.selected() - self.scroll() + 2 > rows {
*self.scroll_mut() = self.selected() + 2 - rows;
}
let is_last_row = i == rows.saturating_sub(1);
let i = self.scroll() + i;
if let Some(entry) = self.files.get(i) {
let mut path = entry.as_line(cols).normal();
if let FsEntry::Dir(..) = entry {
path = path.dimmed().bold();
}
if i == self.selected() {
if is_last_row {
print!("{}", path.clone().reversed());
} else {
println!("{}", path.clone().reversed());
}
} else {
if is_last_row {
print!("{}", path);
} else {
println!("{}", path);
}
}
} else if !is_last_row {
println!();
}
let rows_for_list = rows.saturating_sub(6);
render_search_term(&self.search_term);
render_current_path(
&self.initial_cwd,
&self.file_list_view.path,
self.file_list_view.path_is_dir,
self.handling_filepick_request_from.is_some(),
cols,
);
if self.is_searching {
self.search_view.render(rows_for_list, cols);
} else {
self.file_list_view.render(rows_for_list, cols);
}
render_instruction_line(rows, cols);
}
}

View 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()
}
}

View 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
}

View file

@ -1,9 +1,9 @@
use pretty_bytes::converter as pb;
use crate::file_list_view::{FileListView, FsEntry};
use crate::search_view::SearchView;
use crate::shared::calculate_list_bounds;
use std::{
collections::{HashMap, VecDeque},
fs::read_dir,
collections::BTreeMap,
path::{Path, PathBuf},
time::Instant,
};
use zellij_tile::prelude::*;
@ -11,106 +11,189 @@ pub const ROOT: &str = "/host";
#[derive(Default)]
pub struct State {
pub path: PathBuf,
pub files: Vec<FsEntry>,
pub cursor_hist: HashMap<PathBuf, (usize, usize)>,
pub file_list_view: FileListView,
pub search_view: SearchView,
pub hide_hidden_files: bool,
pub ev_history: VecDeque<(Event, Instant)>, // stores last event, can be expanded in future
pub loading: bool,
pub loading_animation_offset: u8,
pub should_open_floating: bool,
pub current_rows: Option<usize>,
pub handling_filepick_request_from: Option<(PipeSource, BTreeMap<String, String>)>,
pub initial_cwd: PathBuf, // TODO: get this from zellij
pub is_searching: bool,
pub search_term: String,
pub close_on_selection: bool,
}
impl State {
pub fn selected_mut(&mut self) -> &mut usize {
&mut self.cursor_hist.entry(self.path.clone()).or_default().0
pub fn update_search_term(&mut self, character: char) {
self.search_term.push(character);
if &self.search_term == ".." {
self.descend_to_previous_path();
} else if &self.search_term == "/" {
self.descend_to_root_path();
} else {
self.is_searching = true;
self.search_view
.update_search_results(&self.search_term, &self.file_list_view.files);
}
}
pub fn selected(&self) -> usize {
self.cursor_hist.get(&self.path).unwrap_or(&(0, 0)).0
pub fn handle_backspace(&mut self) {
if self.search_term.is_empty() {
self.descend_to_previous_path();
} else {
self.search_term.pop();
if self.search_term.is_empty() {
self.is_searching = false;
}
self.search_view
.update_search_results(&self.search_term, &self.file_list_view.files);
}
}
pub fn scroll_mut(&mut self) -> &mut usize {
&mut self.cursor_hist.entry(self.path.clone()).or_default().1
pub fn clear_search_term_or_descend(&mut self) {
if self.search_term.is_empty() {
self.descend_to_previous_path();
} else {
self.search_term.clear();
self.search_view
.update_search_results(&self.search_term, &self.file_list_view.files);
self.is_searching = false;
}
}
pub fn scroll(&self) -> usize {
self.cursor_hist.get(&self.path).unwrap_or(&(0, 0)).1
pub fn move_selection_up(&mut self) {
if self.is_searching {
self.search_view.move_selection_up();
} else {
self.file_list_view.move_selection_up();
}
}
pub fn move_selection_down(&mut self) {
if self.is_searching {
self.search_view.move_selection_down();
} else {
self.file_list_view.move_selection_down();
}
}
pub fn handle_left_click(&mut self, line: isize) {
if let Some(current_rows) = self.current_rows {
let rows_for_list = current_rows.saturating_sub(5);
if self.is_searching {
let (start_index, _selected_index_in_range, _end_index) = calculate_list_bounds(
self.search_view.search_result_count(),
rows_for_list,
Some(self.search_view.selected_search_result),
);
let prev_selected = self.search_view.selected_search_result;
self.search_view.selected_search_result =
(line as usize).saturating_sub(2) + start_index;
if prev_selected == self.search_view.selected_search_result {
self.traverse_dir();
}
} else {
let (start_index, _selected_index_in_range, _end_index) = calculate_list_bounds(
self.file_list_view.files.len(),
rows_for_list,
self.file_list_view.selected(),
);
let prev_selected = self.file_list_view.selected();
*self.file_list_view.selected_mut() =
(line as usize).saturating_sub(2) + start_index;
if prev_selected == self.file_list_view.selected() {
self.traverse_dir();
}
}
}
}
pub fn descend_to_previous_path(&mut self) {
self.search_term.clear();
self.search_view.clear_and_reset_selection();
self.file_list_view.descend_to_previous_path();
}
pub fn descend_to_root_path(&mut self) {
self.search_term.clear();
self.search_view.clear_and_reset_selection();
self.file_list_view.descend_to_root_path();
refresh_directory(&self.file_list_view.path);
}
pub fn toggle_hidden_files(&mut self) {
self.hide_hidden_files = !self.hide_hidden_files;
}
pub fn traverse_dir_or_open_file(&mut self) {
if let Some(f) = self.files.get(self.selected()) {
match f.clone() {
FsEntry::Dir(p) => {
self.path = p;
refresh_directory(self);
},
FsEntry::File(p, _) => open_file(FileToOpen {
path: p.strip_prefix(ROOT).unwrap().into(),
..Default::default()
}),
}
}
}
}
#[derive(PartialEq, Eq, PartialOrd, Ord, Clone)]
pub enum FsEntry {
Dir(PathBuf),
File(PathBuf, u64),
}
impl FsEntry {
pub fn name(&self) -> String {
let path = match self {
FsEntry::Dir(p) => p,
FsEntry::File(p, _) => p,
};
path.file_name().unwrap().to_string_lossy().into_owned()
}
pub fn as_line(&self, width: usize) -> String {
let info = match self {
FsEntry::Dir(_s) => "".to_owned(),
FsEntry::File(_, s) => pb::convert(*s as f64),
};
let space = width.saturating_sub(info.len());
let name = self.name();
if space.saturating_sub(1) < name.len() {
[&name[..space.saturating_sub(2)], &info].join("~ ")
pub fn traverse_dir(&mut self) {
let entry = if self.is_searching {
self.search_view.get_selected_entry()
} else {
let padding = " ".repeat(space - name.len());
[name, padding, info].concat()
}
}
pub fn is_hidden_file(&self) -> bool {
self.name().starts_with('.')
}
}
pub(crate) fn refresh_directory(state: &mut State) {
// TODO: might be good to do this asynchronously with a worker
let mut max_lines = (state.current_rows.unwrap_or(50) + state.scroll()) * 2; // 100 is arbitrary for performance reasons
let mut files = vec![];
for entry in read_dir(Path::new(ROOT).join(&state.path)).unwrap() {
if let Ok(entry) = entry {
if max_lines == 0 {
break;
self.file_list_view.get_selected_entry()
};
if let Some(entry) = entry {
match &entry {
FsEntry::Dir(_p) => {
self.file_list_view.enter_dir(&entry);
self.search_view.clear_and_reset_selection();
refresh_directory(&self.file_list_view.path);
},
FsEntry::File(_p, _) => {
self.file_list_view.enter_dir(&entry);
self.search_view.clear_and_reset_selection();
},
}
let entry_metadata = entry.metadata().unwrap();
let entry = if entry_metadata.is_dir() {
FsEntry::Dir(entry.path())
}
self.is_searching = false;
self.search_term.clear();
self.search_view.clear_and_reset_selection();
}
pub fn update_files(&mut self, paths: Vec<(PathBuf, Option<FileMetadata>)>) {
self.file_list_view
.update_files(paths, self.hide_hidden_files);
}
pub fn open_selected_path(&mut self) {
if self.file_list_view.path_is_dir {
open_terminal(&self.file_list_view.path);
} else {
if let Some(parent_folder) = self.file_list_view.path.parent() {
open_file(
FileToOpen::new(&self.file_list_view.path).with_cwd(parent_folder.into()),
);
} else {
let size = entry_metadata.len();
FsEntry::File(entry.path(), size)
};
if !entry.is_hidden_file() || !state.hide_hidden_files {
max_lines = max_lines.saturating_sub(1);
files.push(entry);
open_file(FileToOpen::new(&self.file_list_view.path));
}
}
if self.close_on_selection {
close_focus();
}
}
pub fn send_filepick_response(&mut self) {
let selected_path = self.initial_cwd.join(
self.file_list_view
.path
.strip_prefix(ROOT)
.map(|p| p.to_path_buf())
.unwrap_or_else(|_| self.file_list_view.path.clone()),
);
match &self.handling_filepick_request_from {
Some((PipeSource::Plugin(plugin_id), args)) => {
pipe_message_to_plugin(
MessageToPlugin::new("filepicker_result")
.with_destination_plugin_id(*plugin_id)
.with_args(args.clone())
.with_payload(selected_path.display().to_string()),
);
#[cfg(target_family = "wasm")]
close_focus();
},
Some((PipeSource::Cli(pipe_id), _args)) => {
#[cfg(target_family = "wasm")]
cli_pipe_output(pipe_id, &selected_path.display().to_string());
#[cfg(target_family = "wasm")]
unblock_cli_pipe_input(pipe_id);
#[cfg(target_family = "wasm")]
close_focus();
},
_ => {},
}
}
state.files = files;
state.files.sort_unstable();
}
pub(crate) fn refresh_directory(path: &Path) {
let path_on_host = Path::new(ROOT).join(path.strip_prefix("/").unwrap_or(path));
scan_host_folder(&path_on_host);
}

View file

@ -69,7 +69,7 @@ fn main() {
height,
})) = opts.command
{
let cwd = std::env::current_dir().ok();
let cwd = None;
let command_cli_action = CliAction::NewPane {
command: vec![],
plugin: Some(url),

View file

@ -74,7 +74,7 @@ fn pipe_client(
os_input: &mut Box<dyn ClientOsApi>,
pipe_id: String,
mut name: Option<String>,
payload: Option<String>,
mut payload: Option<String>,
plugin: Option<String>,
args: Option<BTreeMap<String, String>>,
mut configuration: Option<BTreeMap<String, String>>,
@ -87,7 +87,13 @@ fn pipe_client(
pane_title: Option<String>,
) {
let mut stdin = os_input.get_stdin_reader();
let name = name.take().or_else(|| Some(Uuid::new_v4().to_string()));
let name = name
// first we try to take the explicitly supplied message name
.take()
// then we use the plugin, to facilitate using aliases
.or_else(|| plugin.clone())
// then we use a uuid to at least have some sort of identifier for this message
.or_else(|| Some(Uuid::new_v4().to_string()));
if launch_new {
// we do this to make sure the plugin is unique (has a unique configuration parameter) so
// that a new one would be launched, but we'll still send it to the same instance rather
@ -116,26 +122,30 @@ fn pipe_client(
None,
)
};
let is_piped = !os_input.stdin_is_terminal();
loop {
if payload.is_some() {
// we got payload from the command line, we should use it and not wait for more
let msg = create_msg(payload);
if let Some(payload) = payload.take() {
let msg = create_msg(Some(payload));
os_input.send_to_server(msg);
break;
}
// we didn't get payload from the command line, meaning we listen on STDIN because this
// signifies the user is about to pipe more (eg. cat my-large-file | zellij pipe ...)
let mut buffer = String::new();
let _ = stdin.read_line(&mut buffer);
if buffer.is_empty() {
// end of pipe, send an empty message down the pipe
} else if !is_piped {
// here we send an empty message to trigger the plugin, because we don't have any more
// data
let msg = create_msg(None);
os_input.send_to_server(msg);
break;
} else {
// we've got data! send it down the pipe (most common)
let msg = create_msg(Some(buffer));
os_input.send_to_server(msg);
// we didn't get payload from the command line, meaning we listen on STDIN because this
// signifies the user is about to pipe more (eg. cat my-large-file | zellij pipe ...)
let mut buffer = String::new();
let _ = stdin.read_line(&mut buffer);
if buffer.is_empty() {
// TODO: consider notifying the relevant plugin that the pipe has ended with a
// specialized message
break;
} else {
// we've got data! send it down the pipe (most common)
let msg = create_msg(Some(buffer));
os_input.send_to_server(msg);
}
}
loop {
// wait for a response and act accordingly
@ -144,7 +154,13 @@ fn pipe_client(
// unblock this pipe, meaning we need to stop waiting for a response and read
// once more from STDIN
if pipe_name == pipe_id {
break;
if !is_piped {
// if this client is not piped, we need to exit the process completely
// rather than wait for more data
process::exit(0);
} else {
break;
}
}
},
Some((ServerToClientMsg::CliPipeOutput(pipe_name, output), _)) => {

View file

@ -8,6 +8,7 @@ use nix::pty::Winsize;
use nix::sys::termios;
use signal_hook::{consts::signal::*, iterator::Signals};
use std::io::prelude::*;
use std::io::IsTerminal;
use std::os::unix::io::RawFd;
use std::path::Path;
use std::sync::{Arc, Mutex};
@ -97,6 +98,12 @@ pub trait ClientOsApi: Send + Sync {
fn get_stdout_writer(&self) -> Box<dyn io::Write>;
/// Returns a BufReader that allows to read from STDIN line by line, also locks STDIN
fn get_stdin_reader(&self) -> Box<dyn io::BufRead>;
fn stdin_is_terminal(&self) -> bool {
true
}
fn stdout_is_terminal(&self) -> bool {
true
}
fn update_session_name(&mut self, new_session_name: String);
/// Returns the raw contents of standard input.
fn read_from_stdin(&mut self) -> Result<Vec<u8>, &'static str>;
@ -193,6 +200,16 @@ impl ClientOsApi for ClientOsInputOutput {
Box::new(stdin.lock())
}
fn stdin_is_terminal(&self) -> bool {
let stdin = ::std::io::stdin();
stdin.is_terminal()
}
fn stdout_is_terminal(&self) -> bool {
let stdout = ::std::io::stdout();
stdout.is_terminal()
}
fn send_to_server(&self, msg: ClientToServerMsg) {
// TODO: handle the error here, right now we silently ignore it
let _ = self

View file

@ -127,6 +127,7 @@ pub enum PluginInstruction {
message: MessageToPlugin,
},
UnblockCliPipes(Vec<PluginRenderAsset>),
WatchFilesystem,
Exit,
}
@ -162,6 +163,7 @@ impl From<&PluginInstruction> for PluginContext {
PluginInstruction::CachePluginEvents { .. } => PluginContext::CachePluginEvents,
PluginInstruction::MessageFromPlugin { .. } => PluginContext::MessageFromPlugin,
PluginInstruction::UnblockCliPipes { .. } => PluginContext::UnblockCliPipes,
PluginInstruction::WatchFilesystem => PluginContext::WatchFilesystem,
}
}
}
@ -428,15 +430,8 @@ pub(crate) fn plugin_thread_main(
wasm_bridge.update_plugins(updates, shutdown_send.clone())?;
},
PluginInstruction::PluginSubscribedToEvents(_plugin_id, _client_id, events) => {
for event in events {
if let EventType::FileSystemCreate
| EventType::FileSystemRead
| EventType::FileSystemUpdate
| EventType::FileSystemDelete = event
{
wasm_bridge.start_fs_watcher_if_not_started();
}
}
// no-op, there used to be stuff we did here - now there isn't, but we might want
// to add stuff here in the future
},
PluginInstruction::PermissionRequestResult(
plugin_id,
@ -634,6 +629,9 @@ pub(crate) fn plugin_thread_main(
.context("failed to unblock input pipe");
}
},
PluginInstruction::WatchFilesystem => {
wasm_bridge.start_fs_watcher_if_not_started();
},
PluginInstruction::Exit => {
break;
},

View file

@ -580,6 +580,7 @@ impl WasmBridge {
&mut running_plugin,
&event,
&mut plugin_render_assets,
senders.clone(),
) {
Ok(()) => {
let _ = senders.send_to_screen(ScreenInstruction::PluginBytes(
@ -830,6 +831,7 @@ impl WasmBridge {
&mut running_plugin,
&event,
&mut plugin_render_assets,
senders.clone(),
) {
Ok(()) => {
let _ = senders.send_to_screen(
@ -1261,6 +1263,7 @@ pub fn apply_event_to_plugin(
running_plugin: &mut RunningPlugin,
event: &Event,
plugin_render_assets: &mut Vec<PluginRenderAsset>,
senders: ThreadSenders,
) -> Result<()> {
let instance = &running_plugin.instance;
let plugin_env = &running_plugin.plugin_env;
@ -1315,6 +1318,17 @@ pub fn apply_event_to_plugin(
)
.with_pipes(pipes_to_block_or_unblock);
plugin_render_assets.push(plugin_render_asset);
} else {
// This is a bit of a hack to get around the fact that plugins are allowed not to
// render and still unblock CLI pipes
let pipes_to_block_or_unblock = pipes_to_block_or_unblock(running_plugin, None);
let plugin_render_asset = PluginRenderAsset::new(plugin_id, client_id, vec![])
.with_pipes(pipes_to_block_or_unblock);
let _ = senders
.send_to_plugin(PluginInstruction::UnblockCliPipes(vec![
plugin_render_asset,
]))
.context("failed to unblock input pipe");
}
},
(PermissionStatus::Denied, permission) => {

View file

@ -90,11 +90,36 @@ pub fn watch_filesystem(
.collect()
})
.collect();
// TODO: at some point we might want to add FileMetadata to these, but right now
// the API is a bit unstable, so let's not rock the boat too much by adding another
// expensive syscall
let _ = senders.send_to_plugin(PluginInstruction::Update(vec![
(None, None, Event::FileSystemRead(read_paths)),
(None, None, Event::FileSystemCreate(create_paths)),
(None, None, Event::FileSystemUpdate(update_paths)),
(None, None, Event::FileSystemDelete(delete_paths)),
(
None,
None,
Event::FileSystemRead(read_paths.into_iter().map(|p| (p, None)).collect()),
),
(
None,
None,
Event::FileSystemCreate(
create_paths.into_iter().map(|p| (p, None)).collect(),
),
),
(
None,
None,
Event::FileSystemUpdate(
update_paths.into_iter().map(|p| (p, None)).collect(),
),
),
(
None,
None,
Event::FileSystemDelete(
delete_paths.into_iter().map(|p| (p, None)).collect(),
),
),
]));
},
Err(errors) => errors

View file

@ -259,6 +259,10 @@ fn host_run_plugin_command(env: FunctionEnvMut<ForeignFunctionEnv>) {
PluginCommand::MessageToPlugin(message) => message_to_plugin(env, message)?,
PluginCommand::DisconnectOtherClients => disconnect_other_clients(env),
PluginCommand::KillSessions(session_list) => kill_sessions(session_list),
PluginCommand::ScanHostFolder(folder_to_scan) => {
scan_host_folder(env, folder_to_scan)
},
PluginCommand::WatchFilesystem => watch_filesystem(env),
},
(PermissionStatus::Denied, permission) => {
log::error!(
@ -398,6 +402,7 @@ fn get_plugin_ids(env: &ForeignFunctionEnv) {
let ids = PluginIds {
plugin_id: env.plugin_env.plugin_id,
zellij_pid: process::id(),
initial_cwd: env.plugin_env.plugin_cwd.clone(),
};
ProtobufPluginIds::try_from(ids)
.map_err(|e| anyhow!("Failed to serialized plugin ids: {}", e))
@ -1326,6 +1331,82 @@ fn kill_sessions(session_names: Vec<String>) {
}
}
fn watch_filesystem(env: &ForeignFunctionEnv) {
let _ = env
.plugin_env
.senders
.to_plugin
.as_ref()
.map(|sender| sender.send(PluginInstruction::WatchFilesystem));
}
fn scan_host_folder(env: &ForeignFunctionEnv, folder_to_scan: PathBuf) {
if !folder_to_scan.starts_with("/host") {
log::error!(
"Can only scan files in the /host filesystem, found: {}",
folder_to_scan.display()
);
return;
}
let plugin_host_folder = env.plugin_env.plugin_cwd.clone();
let folder_to_scan = plugin_host_folder.join(folder_to_scan.strip_prefix("/host").unwrap());
match folder_to_scan.canonicalize() {
Ok(folder_to_scan) => {
if !folder_to_scan.starts_with(&plugin_host_folder) {
log::error!(
"Can only scan files in the plugin filesystem: {}, found: {}",
plugin_host_folder.display(),
folder_to_scan.display()
);
return;
}
let reading_folder = std::fs::read_dir(&folder_to_scan);
match reading_folder {
Ok(reading_folder) => {
let send_plugin_instructions = env.plugin_env.senders.to_plugin.clone();
let update_target = Some(env.plugin_env.plugin_id);
let client_id = env.plugin_env.client_id;
thread::spawn({
move || {
let mut paths_in_folder = vec![];
for entry in reading_folder {
if let Ok(entry) = entry {
let entry_metadata = entry.metadata().ok().map(|m| m.into());
paths_in_folder.push((
PathBuf::from("/host").join(
entry.path().strip_prefix(&plugin_host_folder).unwrap(),
),
entry_metadata.into(),
));
}
}
let _ = send_plugin_instructions
.ok_or(anyhow!("found no sender to send plugin instruction to"))
.map(|sender| {
let _ = sender.send(PluginInstruction::Update(vec![(
update_target,
Some(client_id),
Event::FileSystemUpdate(paths_in_folder),
)]));
})
.non_fatal();
}
});
},
Err(e) => {
log::error!("Failed to read folder {}: {e}", folder_to_scan.display());
},
}
},
Err(e) => {
log::error!(
"Failed to canonicalize path {folder_to_scan:?} when scanning folder: {:?}",
e
);
},
}
}
// Custom panic handler for plugins.
//
// This is called when a panic occurs in a plugin. Since most panics will likely originate in the

View file

@ -766,6 +766,24 @@ where
unsafe { host_run_plugin_command() };
}
/// Scan a specific folder in the host filesystem (this is a hack around some WASI runtime performance
/// issues), will not follow symlinks
pub fn scan_host_folder<S: AsRef<Path>>(folder_to_scan: &S) {
let plugin_command = PluginCommand::ScanHostFolder(folder_to_scan.as_ref().to_path_buf());
let protobuf_plugin_command: ProtobufPluginCommand = plugin_command.try_into().unwrap();
object_to_stdout(&protobuf_plugin_command.encode_to_vec());
unsafe { host_run_plugin_command() };
}
/// Start watching the host folder for filesystem changes (Note: somewhat unstable at the time
/// being)
pub fn watch_filesystem() {
let plugin_command = PluginCommand::WatchFilesystem;
let protobuf_plugin_command: ProtobufPluginCommand = plugin_command.try_into().unwrap();
object_to_stdout(&protobuf_plugin_command.encode_to_vec());
unsafe { host_run_plugin_command() };
}
// Utility Functions
#[allow(unused)]

View file

@ -109,6 +109,23 @@ pub struct PermissionRequestResultPayload {
pub struct FileListPayload {
#[prost(string, repeated, tag = "1")]
pub paths: ::prost::alloc::vec::Vec<::prost::alloc::string::String>,
#[prost(message, repeated, tag = "2")]
pub paths_metadata: ::prost::alloc::vec::Vec<FileMetadata>,
}
#[allow(clippy::derive_partial_eq_without_eq)]
#[derive(Clone, PartialEq, ::prost::Message)]
pub struct FileMetadata {
/// if this is false, the metadata for this file has not been read
#[prost(bool, tag = "1")]
pub metadata_is_set: bool,
#[prost(bool, tag = "2")]
pub is_dir: bool,
#[prost(bool, tag = "3")]
pub is_file: bool,
#[prost(bool, tag = "4")]
pub is_symlink: bool,
#[prost(uint64, tag = "5")]
pub len: u64,
}
#[allow(clippy::derive_partial_eq_without_eq)]
#[derive(Clone, PartialEq, ::prost::Message)]

View file

@ -5,7 +5,7 @@ pub struct PluginCommand {
pub name: i32,
#[prost(
oneof = "plugin_command::Payload",
tags = "2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 60"
tags = "2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 60, 61"
)]
pub payload: ::core::option::Option<plugin_command::Payload>,
}
@ -114,6 +114,8 @@ pub mod plugin_command {
MessageToPluginPayload(super::MessageToPluginPayload),
#[prost(message, tag = "60")]
KillSessionsPayload(super::KillSessionsPayload),
#[prost(string, tag = "61")]
ScanHostFolderPayload(::prost::alloc::string::String),
}
}
#[allow(clippy::derive_partial_eq_without_eq)]
@ -416,6 +418,8 @@ pub enum CommandName {
MessageToPlugin = 79,
DisconnectOtherClients = 80,
KillSessions = 81,
ScanHostFolder = 82,
WatchFilesystem = 83,
}
impl CommandName {
/// String value of the enum field names used in the ProtoBuf definition.
@ -506,6 +510,8 @@ impl CommandName {
CommandName::MessageToPlugin => "MessageToPlugin",
CommandName::DisconnectOtherClients => "DisconnectOtherClients",
CommandName::KillSessions => "KillSessions",
CommandName::ScanHostFolder => "ScanHostFolder",
CommandName::WatchFilesystem => "WatchFilesystem",
}
}
/// Creates an enum from field names used in the ProtoBuf definition.
@ -593,6 +599,8 @@ impl CommandName {
"MessageToPlugin" => Some(Self::MessageToPlugin),
"DisconnectOtherClients" => Some(Self::DisconnectOtherClients),
"KillSessions" => Some(Self::KillSessions),
"ScanHostFolder" => Some(Self::ScanHostFolder),
"WatchFilesystem" => Some(Self::WatchFilesystem),
_ => None,
}
}

View file

@ -5,6 +5,8 @@ pub struct PluginIds {
pub plugin_id: i32,
#[prost(int32, tag = "2")]
pub zellij_pid: i32,
#[prost(string, tag = "3")]
pub initial_cwd: ::prost::alloc::string::String,
}
#[allow(clippy::derive_partial_eq_without_eq)]
#[derive(Clone, PartialEq, ::prost::Message)]

View file

@ -5,6 +5,7 @@ use clap::ArgEnum;
use serde::{Deserialize, Serialize};
use std::collections::{BTreeMap, HashMap, HashSet};
use std::fmt;
use std::fs::Metadata;
use std::path::{Path, PathBuf};
use std::str::FromStr;
use std::time::Duration;
@ -458,6 +459,25 @@ pub enum Mouse {
Release(isize, usize), // line and column
}
#[derive(Debug, Default, Copy, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub struct FileMetadata {
pub is_dir: bool,
pub is_file: bool,
pub is_symlink: bool,
pub len: u64,
}
impl From<Metadata> for FileMetadata {
fn from(metadata: Metadata) -> Self {
FileMetadata {
is_dir: metadata.is_dir(),
is_file: metadata.is_file(),
is_symlink: metadata.is_symlink(),
len: metadata.len(),
}
}
}
/// These events can be subscribed to with subscribe method exported by `zellij-tile`.
/// Once subscribed to, they will trigger the `update` method of the `ZellijPlugin` trait.
#[derive(Debug, Clone, PartialEq, EnumDiscriminants, ToString, Serialize, Deserialize)]
@ -488,13 +508,13 @@ pub enum Event {
String, // payload
),
/// A file was created somewhere in the Zellij CWD folder
FileSystemCreate(Vec<PathBuf>),
FileSystemCreate(Vec<(PathBuf, Option<FileMetadata>)>),
/// A file was accessed somewhere in the Zellij CWD folder
FileSystemRead(Vec<PathBuf>),
FileSystemRead(Vec<(PathBuf, Option<FileMetadata>)>),
/// A file was modified somewhere in the Zellij CWD folder
FileSystemUpdate(Vec<PathBuf>),
FileSystemUpdate(Vec<(PathBuf, Option<FileMetadata>)>),
/// A file was deleted somewhere in the Zellij CWD folder
FileSystemDelete(Vec<PathBuf>),
FileSystemDelete(Vec<(PathBuf, Option<FileMetadata>)>),
/// A Result of plugin permission request
PermissionRequestResult(PermissionStatus),
SessionUpdate(
@ -904,10 +924,11 @@ pub struct PaneInfo {
pub is_selectable: bool,
}
#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, Deserialize, Serialize)]
#[derive(Debug, Clone, PartialEq, Eq, Hash, Deserialize, Serialize)]
pub struct PluginIds {
pub plugin_id: u32,
pub zellij_pid: u32,
pub initial_cwd: PathBuf,
}
/// Tag used to identify the plugin in layout and config kdl files
@ -1350,4 +1371,6 @@ pub enum PluginCommand {
MessageToPlugin(MessageToPlugin),
DisconnectOtherClients,
KillSessions(Vec<String>), // one or more session names
ScanHostFolder(PathBuf), // TODO: rename to ScanHostFolder
WatchFilesystem,
}

View file

@ -400,6 +400,7 @@ pub enum PluginContext {
CachePluginEvents,
MessageFromPlugin,
UnblockCliPipes,
WatchFilesystem,
}
/// Stack call representations corresponding to the different types of [`ClientInstruction`]s.

View file

@ -353,7 +353,7 @@ impl Action {
let alias_cwd = cwd.clone().map(|cwd| current_dir.join(cwd));
let cwd = cwd
.map(|cwd| current_dir.join(cwd))
.or_else(|| Some(current_dir));
.or_else(|| Some(current_dir.clone()));
if let Some(plugin) = plugin {
let plugin = match RunPluginLocation::parse(&plugin, cwd.clone()) {
Ok(location) => {
@ -365,11 +365,17 @@ impl Action {
initial_cwd: cwd.clone(),
})
},
Err(_) => RunPluginOrAlias::Alias(PluginAlias::new(
&plugin,
&configuration.map(|c| c.inner().clone()),
alias_cwd,
)),
Err(_) => {
let mut user_configuration =
configuration.map(|c| c.inner().clone()).unwrap_or_default();
user_configuration
.insert("caller_cwd".to_owned(), current_dir.display().to_string());
RunPluginOrAlias::Alias(PluginAlias::new(
&plugin,
&Some(user_configuration),
alias_cwd,
))
},
};
if floating {
Ok(vec![Action::NewFloatingPluginPane(

View file

@ -103,6 +103,15 @@ message PermissionRequestResultPayload {
message FileListPayload {
repeated string paths = 1;
repeated FileMetadata paths_metadata = 2;
}
message FileMetadata {
bool metadata_is_set = 1; // if this is false, the metadata for this file has not been read
bool is_dir = 2;
bool is_file = 3;
bool is_symlink = 4;
uint64 len = 5;
}
message CustomMessagePayload {

View file

@ -3,10 +3,11 @@ pub use super::generated_api::api::{
event::{
event::Payload as ProtobufEventPayload, CopyDestination as ProtobufCopyDestination,
Event as ProtobufEvent, EventNameList as ProtobufEventNameList,
EventType as ProtobufEventType, InputModeKeybinds as ProtobufInputModeKeybinds,
KeyBind as ProtobufKeyBind, LayoutInfo as ProtobufLayoutInfo,
ModeUpdatePayload as ProtobufModeUpdatePayload, PaneInfo as ProtobufPaneInfo,
PaneManifest as ProtobufPaneManifest, ResurrectableSession as ProtobufResurrectableSession,
EventType as ProtobufEventType, FileMetadata as ProtobufFileMetadata,
InputModeKeybinds as ProtobufInputModeKeybinds, KeyBind as ProtobufKeyBind,
LayoutInfo as ProtobufLayoutInfo, ModeUpdatePayload as ProtobufModeUpdatePayload,
PaneInfo as ProtobufPaneInfo, PaneManifest as ProtobufPaneManifest,
ResurrectableSession as ProtobufResurrectableSession,
SessionManifest as ProtobufSessionManifest, TabInfo as ProtobufTabInfo, *,
},
input_mode::InputMode as ProtobufInputMode,
@ -14,8 +15,8 @@ pub use super::generated_api::api::{
style::Style as ProtobufStyle,
};
use crate::data::{
CopyDestination, Event, EventType, InputMode, Key, LayoutInfo, ModeInfo, Mouse, PaneInfo,
PaneManifest, PermissionStatus, PluginCapabilities, SessionInfo, Style, TabInfo,
CopyDestination, Event, EventType, FileMetadata, InputMode, Key, LayoutInfo, ModeInfo, Mouse,
PaneInfo, PaneManifest, PermissionStatus, PluginCapabilities, SessionInfo, Style, TabInfo,
};
use crate::errors::prelude::*;
@ -124,7 +125,8 @@ impl TryFrom<ProtobufEvent> for Event {
let file_paths = file_list_payload
.paths
.iter()
.map(|p| PathBuf::from(p))
.zip(file_list_payload.paths_metadata.iter())
.map(|(p, m)| (PathBuf::from(p), m.into()))
.collect();
Ok(Event::FileSystemCreate(file_paths))
},
@ -135,7 +137,8 @@ impl TryFrom<ProtobufEvent> for Event {
let file_paths = file_list_payload
.paths
.iter()
.map(|p| PathBuf::from(p))
.zip(file_list_payload.paths_metadata.iter())
.map(|(p, m)| (PathBuf::from(p), m.into()))
.collect();
Ok(Event::FileSystemRead(file_paths))
},
@ -146,7 +149,8 @@ impl TryFrom<ProtobufEvent> for Event {
let file_paths = file_list_payload
.paths
.iter()
.map(|p| PathBuf::from(p))
.zip(file_list_payload.paths_metadata.iter())
.map(|(p, m)| (PathBuf::from(p), m.into()))
.collect();
Ok(Event::FileSystemUpdate(file_paths))
},
@ -157,7 +161,8 @@ impl TryFrom<ProtobufEvent> for Event {
let file_paths = file_list_payload
.paths
.iter()
.map(|p| PathBuf::from(p))
.zip(file_list_payload.paths_metadata.iter())
.map(|(p, m)| (PathBuf::from(p), m.into()))
.collect();
Ok(Event::FileSystemDelete(file_paths))
},
@ -322,36 +327,64 @@ impl TryFrom<Event> for ProtobufEvent {
payload,
})),
}),
Event::FileSystemCreate(paths) => {
Event::FileSystemCreate(event_paths) => {
let mut paths = vec![];
let mut paths_metadata = vec![];
for (path, path_metadata) in event_paths {
paths.push(path.display().to_string());
paths_metadata.push(path_metadata.into());
}
let file_list_payload = FileListPayload {
paths: paths.iter().map(|p| p.display().to_string()).collect(),
paths,
paths_metadata,
};
Ok(ProtobufEvent {
name: ProtobufEventType::FileSystemCreate as i32,
payload: Some(event::Payload::FileListPayload(file_list_payload)),
})
},
Event::FileSystemRead(paths) => {
Event::FileSystemRead(event_paths) => {
let mut paths = vec![];
let mut paths_metadata = vec![];
for (path, path_metadata) in event_paths {
paths.push(path.display().to_string());
paths_metadata.push(path_metadata.into());
}
let file_list_payload = FileListPayload {
paths: paths.iter().map(|p| p.display().to_string()).collect(),
paths,
paths_metadata,
};
Ok(ProtobufEvent {
name: ProtobufEventType::FileSystemRead as i32,
payload: Some(event::Payload::FileListPayload(file_list_payload)),
})
},
Event::FileSystemUpdate(paths) => {
Event::FileSystemUpdate(event_paths) => {
let mut paths = vec![];
let mut paths_metadata = vec![];
for (path, path_metadata) in event_paths {
paths.push(path.display().to_string());
paths_metadata.push(path_metadata.into());
}
let file_list_payload = FileListPayload {
paths: paths.iter().map(|p| p.display().to_string()).collect(),
paths,
paths_metadata,
};
Ok(ProtobufEvent {
name: ProtobufEventType::FileSystemUpdate as i32,
payload: Some(event::Payload::FileListPayload(file_list_payload)),
})
},
Event::FileSystemDelete(paths) => {
Event::FileSystemDelete(event_paths) => {
let mut paths = vec![];
let mut paths_metadata = vec![];
for (path, path_metadata) in event_paths {
paths.push(path.display().to_string());
paths_metadata.push(path_metadata.into());
}
let file_list_payload = FileListPayload {
paths: paths.iter().map(|p| p.display().to_string()).collect(),
paths,
paths_metadata,
};
Ok(ProtobufEvent {
name: ProtobufEventType::FileSystemDelete as i32,
@ -958,6 +991,39 @@ impl From<(String, Duration)> for ProtobufResurrectableSession {
}
}
impl From<&ProtobufFileMetadata> for Option<FileMetadata> {
fn from(protobuf_file_metadata: &ProtobufFileMetadata) -> Option<FileMetadata> {
if protobuf_file_metadata.metadata_is_set {
Some(FileMetadata {
is_file: protobuf_file_metadata.is_file,
is_dir: protobuf_file_metadata.is_dir,
is_symlink: protobuf_file_metadata.is_symlink,
len: protobuf_file_metadata.len,
})
} else {
None
}
}
}
impl From<Option<FileMetadata>> for ProtobufFileMetadata {
fn from(file_metadata: Option<FileMetadata>) -> ProtobufFileMetadata {
match file_metadata {
Some(file_metadata) => ProtobufFileMetadata {
metadata_is_set: true,
is_file: file_metadata.is_file,
is_dir: file_metadata.is_dir,
is_symlink: file_metadata.is_symlink,
len: file_metadata.len,
},
None => ProtobufFileMetadata {
metadata_is_set: false,
..Default::default()
},
}
}
}
#[test]
fn serialize_mode_update_event() {
use prost::Message;
@ -1256,8 +1322,10 @@ fn serialize_custom_message_event() {
#[test]
fn serialize_file_system_create_event() {
use prost::Message;
let file_system_event =
Event::FileSystemCreate(vec!["/absolute/path".into(), "./relative_path".into()]);
let file_system_event = Event::FileSystemCreate(vec![
("/absolute/path".into(), None),
("./relative_path".into(), Default::default()),
]);
let protobuf_event: ProtobufEvent = file_system_event.clone().try_into().unwrap();
let serialized_protobuf_event = protobuf_event.encode_to_vec();
let deserialized_protobuf_event: ProtobufEvent =
@ -1272,8 +1340,10 @@ fn serialize_file_system_create_event() {
#[test]
fn serialize_file_system_read_event() {
use prost::Message;
let file_system_event =
Event::FileSystemRead(vec!["/absolute/path".into(), "./relative_path".into()]);
let file_system_event = Event::FileSystemRead(vec![
("/absolute/path".into(), None),
("./relative_path".into(), Default::default()),
]);
let protobuf_event: ProtobufEvent = file_system_event.clone().try_into().unwrap();
let serialized_protobuf_event = protobuf_event.encode_to_vec();
let deserialized_protobuf_event: ProtobufEvent =
@ -1288,8 +1358,10 @@ fn serialize_file_system_read_event() {
#[test]
fn serialize_file_system_update_event() {
use prost::Message;
let file_system_event =
Event::FileSystemUpdate(vec!["/absolute/path".into(), "./relative_path".into()]);
let file_system_event = Event::FileSystemUpdate(vec![
("/absolute/path".into(), None),
("./relative_path".into(), Some(Default::default())),
]);
let protobuf_event: ProtobufEvent = file_system_event.clone().try_into().unwrap();
let serialized_protobuf_event = protobuf_event.encode_to_vec();
let deserialized_protobuf_event: ProtobufEvent =
@ -1304,8 +1376,10 @@ fn serialize_file_system_update_event() {
#[test]
fn serialize_file_system_delete_event() {
use prost::Message;
let file_system_event =
Event::FileSystemDelete(vec!["/absolute/path".into(), "./relative_path".into()]);
let file_system_event = Event::FileSystemDelete(vec![
("/absolute/path".into(), None),
("./relative_path".into(), Default::default()),
]);
let protobuf_event: ProtobufEvent = file_system_event.clone().try_into().unwrap();
let serialized_protobuf_event = protobuf_event.encode_to_vec();
let deserialized_protobuf_event: ProtobufEvent =

View file

@ -93,6 +93,8 @@ enum CommandName {
MessageToPlugin = 79;
DisconnectOtherClients = 80;
KillSessions = 81;
ScanHostFolder = 82;
WatchFilesystem = 83;
}
message PluginCommand {
@ -148,6 +150,7 @@ message PluginCommand {
CliPipeOutputPayload cli_pipe_output_payload = 49;
MessageToPluginPayload message_to_plugin_payload = 50;
KillSessionsPayload kill_sessions_payload = 60;
string scan_host_folder_payload = 61;
}
}

View file

@ -855,7 +855,17 @@ impl TryFrom<ProtobufPluginCommand> for PluginCommand {
Some(Payload::KillSessionsPayload(KillSessionsPayload { session_names })) => {
Ok(PluginCommand::KillSessions(session_names))
},
_ => Err("Mismatched payload for PipeOutput"),
_ => Err("Mismatched payload for KillSessions"),
},
Some(CommandName::ScanHostFolder) => match protobuf_plugin_command.payload {
Some(Payload::ScanHostFolderPayload(folder_to_scan)) => {
Ok(PluginCommand::ScanHostFolder(PathBuf::from(folder_to_scan)))
},
_ => Err("Mismatched payload for ScanHostFolder"),
},
Some(CommandName::WatchFilesystem) => match protobuf_plugin_command.payload {
Some(_) => Err("WatchFilesystem should have no payload, found a payload"),
None => Ok(PluginCommand::WatchFilesystem),
},
None => Err("Unrecognized plugin command"),
}
@ -1361,6 +1371,16 @@ impl TryFrom<PluginCommand> for ProtobufPluginCommand {
session_names,
})),
}),
PluginCommand::ScanHostFolder(folder_to_scan) => Ok(ProtobufPluginCommand {
name: CommandName::ScanHostFolder as i32,
payload: Some(Payload::ScanHostFolderPayload(
folder_to_scan.display().to_string(),
)),
}),
PluginCommand::WatchFilesystem => Ok(ProtobufPluginCommand {
name: CommandName::WatchFilesystem as i32,
payload: None,
}),
}
}
}

View file

@ -5,6 +5,7 @@ package api.plugin_ids;
message PluginIds {
int32 plugin_id = 1;
int32 zellij_pid = 2;
string initial_cwd = 3;
}
message ZellijVersion {

View file

@ -4,6 +4,7 @@ pub use super::generated_api::api::plugin_ids::{
use crate::data::PluginIds;
use std::convert::TryFrom;
use std::path::PathBuf;
impl TryFrom<ProtobufPluginIds> for PluginIds {
type Error = &'static str;
@ -11,6 +12,7 @@ impl TryFrom<ProtobufPluginIds> for PluginIds {
Ok(PluginIds {
plugin_id: protobuf_plugin_ids.plugin_id as u32,
zellij_pid: protobuf_plugin_ids.zellij_pid as u32,
initial_cwd: PathBuf::from(protobuf_plugin_ids.initial_cwd),
})
}
}
@ -21,6 +23,7 @@ impl TryFrom<PluginIds> for ProtobufPluginIds {
Ok(ProtobufPluginIds {
plugin_id: plugin_ids.plugin_id as i32,
zellij_pid: plugin_ids.zellij_pid as i32,
initial_cwd: plugin_ids.initial_cwd.display().to_string(),
})
}
}