diff options
| author | Aram Drevekenin <aram@poor.dev> | 2025-07-15 10:27:43 +0200 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2025-07-15 10:27:43 +0200 |
| commit | d4f5975527868d1bcc980e0d149be9aefd4efc34 (patch) | |
| tree | eb96b817d29f3cd4f346d1a579644c30b286678d | |
| parent | fix: populate layount metadata with the right cmd and cwd (#4287) (diff) | |
| download | sl0ppij-d4f5975527868d1bcc980e0d149be9aefd4efc34.tar.gz sl0ppij-d4f5975527868d1bcc980e0d149be9aefd4efc34.tar.xz sl0ppij-d4f5975527868d1bcc980e0d149be9aefd4efc34.zip | |
fix(plugins): forward CWD when opening new panes (#4290)
* use plugin cwd as last focused pane cwd when starting plugins
* fix strider to use the new change_host_folder method
* start terminals in same cwd as plugin pane if focused
* style(fmt): rustfmt
| -rw-r--r-- | default-plugins/strider/src/file_list_view.rs | 58 | ||||
| -rw-r--r-- | default-plugins/strider/src/main.rs | 37 | ||||
| -rw-r--r-- | default-plugins/strider/src/shared.rs | 21 | ||||
| -rw-r--r-- | default-plugins/strider/src/state.rs | 17 | ||||
| -rw-r--r-- | zellij-server/src/plugins/mod.rs | 40 | ||||
| -rw-r--r-- | zellij-server/src/plugins/unit/plugin_tests.rs | 108 | ||||
| -rw-r--r-- | zellij-server/src/plugins/wasm_bridge.rs | 27 | ||||
| -rw-r--r-- | zellij-server/src/plugins/zellij_exports.rs | 1 | ||||
| -rw-r--r-- | zellij-server/src/pty.rs | 68 | ||||
| -rw-r--r-- | zellij-utils/src/errors.rs | 1 |
10 files changed, 280 insertions, 98 deletions
diff --git a/default-plugins/strider/src/file_list_view.rs b/default-plugins/strider/src/file_list_view.rs index ccbbe292..a5a0bfa9 100644 --- a/default-plugins/strider/src/file_list_view.rs +++ b/default-plugins/strider/src/file_list_view.rs @@ -2,7 +2,7 @@ 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 std::path::{Path, PathBuf}; use unicode_width::UnicodeWidthStr; use zellij_tile::prelude::*; @@ -18,7 +18,7 @@ impl Default for FileListView { fn default() -> Self { FileListView { path_is_dir: true, - path: Default::default(), + path: PathBuf::new(), files: Default::default(), cursor_hist: Default::default(), } @@ -27,30 +27,48 @@ impl Default for FileListView { impl FileListView { pub fn descend_to_previous_path(&mut self) { - self.path.pop(); + if let Some(parent) = self.path.parent() { + self.path = parent.to_path_buf(); + } else { + self.path = PathBuf::new(); + } self.path_is_dir = true; self.files.clear(); self.clear_selected(); refresh_directory(&self.path); } - pub fn descend_to_root_path(&mut self) { - self.path.clear(); + + pub fn descend_to_root_path(&mut self, initial_cwd: &PathBuf) { + self.path = initial_cwd.clone(); self.path_is_dir = true; self.files.clear(); self.clear_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(); + let path = entry.get_full_pathbuf(); self.path = path; self.path_is_dir = is_dir; self.files.clear(); self.clear_selected(); } + pub fn clear_selected(&mut self) { self.cursor_hist.remove(&self.path); } + + pub fn get_relative_path(&self) -> PathBuf { + if let Some(initial_cwd_part) = self.path.iter().next() { + self.path + .strip_prefix(initial_cwd_part) + .unwrap_or(&self.path) + .to_path_buf() + } else { + PathBuf::new() + } + } + pub fn update_files( &mut self, paths: Vec<(PathBuf, Option<FileMetadata>)>, @@ -58,8 +76,11 @@ impl FileListView { ) { let mut files = vec![]; for (entry, entry_metadata) in paths { + let entry = self + .path + .join(entry.strip_prefix("/host").unwrap_or(&entry)); if entry_metadata.map(|e| e.is_symlink).unwrap_or(false) { - continue; // ignore symlinks + continue; } let entry = if entry_metadata.map(|e| e.is_dir).unwrap_or(false) { FsEntry::Dir(entry) @@ -74,20 +95,25 @@ impl FileListView { 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); @@ -96,6 +122,7 @@ impl FileListView { *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()); @@ -121,7 +148,6 @@ impl FileListView { ); 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) }; @@ -159,12 +185,14 @@ impl FsEntry { }; 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 @@ -178,14 +206,18 @@ impl FsEntry { } } + pub fn get_full_pathbuf(&self) -> PathBuf { + match self { + FsEntry::Dir(p) => p.clone(), + FsEntry::File(p, _) => 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, - } + matches!(self, FsEntry::Dir(_)) } } diff --git a/default-plugins/strider/src/main.rs b/default-plugins/strider/src/main.rs index cd9dda4d..d770e495 100644 --- a/default-plugins/strider/src/main.rs +++ b/default-plugins/strider/src/main.rs @@ -31,29 +31,24 @@ impl ZellijPlugin for State { EventType::CustomMessage, EventType::Timer, EventType::FileSystemUpdate, + EventType::HostFolderChanged, + EventType::PermissionRequestResult, ]); self.file_list_view.clear_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); + + match configuration.get("caller_cwd").map(|c| PathBuf::from(c)) { + Some(caller_cwd) => { + self.file_list_view.path = caller_cwd; }, None => { - refresh_directory(&std::path::Path::new("/")); + self.file_list_view.path = self.initial_cwd.clone(); }, } + if self.initial_cwd != self.file_list_view.path { + change_host_folder(self.file_list_view.path.clone()); + } else { + scan_host_folder(&"/host"); + } } fn update(&mut self, event: Event) -> bool { @@ -63,6 +58,10 @@ impl ZellijPlugin for State { self.update_files(paths); should_render = true; }, + Event::HostFolderChanged(new_host_folder) => { + scan_host_folder(&"/host"); + should_render = true; + }, Event::Key(key) => match key.bare_key { BareKey::Char(character) if key.has_no_modifiers() => { self.update_search_term(character); @@ -137,11 +136,10 @@ 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); } @@ -157,7 +155,6 @@ impl ZellijPlugin for State { 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(), diff --git a/default-plugins/strider/src/shared.rs b/default-plugins/strider/src/shared.rs index c51cb312..19728034 100644 --- a/default-plugins/strider/src/shared.rs +++ b/default-plugins/strider/src/shared.rs @@ -31,7 +31,6 @@ pub fn render_list_tip(y: usize, max_cols: usize) { 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, @@ -48,17 +47,17 @@ pub fn calculate_list_bounds( break; } if !alternate && start_index > 0 { - start_index -= 1; - room_in_list -= 1; + start_index = start_index.saturating_sub(1); + room_in_list = room_in_list.saturating_sub(1); } else if alternate && end_index < result_count { end_index += 1; - room_in_list -= 1; + room_in_list = room_in_list.saturating_sub(1); } else if start_index > 0 { - start_index -= 1; - room_in_list -= 1; + start_index = start_index.saturating_sub(1); + room_in_list = room_in_list.saturating_sub(1); } else if end_index < result_count { end_index += 1; - room_in_list -= 1; + room_in_list = room_in_list.saturating_sub(1); } else { break; } @@ -80,15 +79,13 @@ pub fn render_search_term(search_term: &str) { } pub fn render_current_path( - initial_cwd: &PathBuf, - path: &PathBuf, + full_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 current_path = full_path.display().to_string(); let prompt_len = prompt.width(); let current_path_len = current_path.width(); @@ -118,7 +115,7 @@ pub fn render_current_path( current_path } else { truncate_path( - initial_cwd.join(path), + full_path.clone(), current_path_len.saturating_sub(max_path_len), ) }; diff --git a/default-plugins/strider/src/state.rs b/default-plugins/strider/src/state.rs index 0bf5b268..93c6cf1b 100644 --- a/default-plugins/strider/src/state.rs +++ b/default-plugins/strider/src/state.rs @@ -16,7 +16,7 @@ pub struct State { pub hide_hidden_files: 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 initial_cwd: PathBuf, pub is_searching: bool, pub search_term: String, pub close_on_selection: bool, @@ -137,7 +137,7 @@ impl State { 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(); + self.file_list_view.descend_to_root_path(&self.initial_cwd); refresh_directory(&self.file_list_view.path); } pub fn toggle_hidden_files(&mut self) { @@ -214,13 +214,7 @@ impl State { } } 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()), - ); + let selected_path = &self.file_list_view.path; match &self.handling_filepick_request_from { Some((PipeSource::Plugin(plugin_id), args)) => { pipe_message_to_plugin( @@ -245,7 +239,6 @@ impl State { } } -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); +pub(crate) fn refresh_directory(full_path: &Path) { + change_host_folder(PathBuf::from(full_path)); } diff --git a/zellij-server/src/plugins/mod.rs b/zellij-server/src/plugins/mod.rs index 48620a99..6e84649a 100644 --- a/zellij-server/src/plugins/mod.rs +++ b/zellij-server/src/plugins/mod.rs @@ -54,9 +54,10 @@ pub enum PluginInstruction { Option<PaneId>, // pane id to replace if this is to be opened "in-place" ClientId, Size, - Option<PathBuf>, // cwd - bool, // skip cache - Option<bool>, // should focus plugin + Option<PathBuf>, // cwd + Option<PluginId>, // the focused plugin id if relevant + bool, // skip cache + Option<bool>, // should focus plugin Option<FloatingPaneCoordinates>, ), LoadBackgroundPlugin(RunPluginOrAlias, ClientId), @@ -253,7 +254,7 @@ pub(crate) fn plugin_thread_main( engine, plugin_dir, path_to_default_shell, - zellij_cwd, + zellij_cwd.clone(), capabilities, client_attributes, default_shell, @@ -287,12 +288,19 @@ pub(crate) fn plugin_thread_main( client_id, size, cwd, + focused_plugin_id, skip_cache, should_focus_plugin, floating_pane_coordinates, ) => { run_plugin_or_alias.populate_run_plugin_if_needed(&plugin_aliases); - let cwd = run_plugin_or_alias.get_initial_cwd().or(cwd); + let cwd = run_plugin_or_alias.get_initial_cwd().or(cwd).or_else(|| { + if let Some(plugin_id) = focused_plugin_id { + wasm_bridge.get_plugin_cwd(plugin_id, client_id) + } else { + None + } + }); let run_plugin = run_plugin_or_alias.get_run_plugin(); let start_suppressed = false; match wasm_bridge.load_plugin( @@ -313,12 +321,17 @@ pub(crate) fn plugin_thread_main( tab_index, plugin_id, pane_id_to_replace, - cwd, + cwd.clone(), start_suppressed, floating_pane_coordinates, should_focus_plugin, Some(client_id), ))); + + drop(bus.senders.send_to_pty(PtyInstruction::ReportPluginCwd( + plugin_id, + cwd.unwrap_or_else(|| zellij_cwd.clone()), + ))); }, Err(e) => { log::error!("Failed to load plugin: {e}"); @@ -916,9 +929,18 @@ pub(crate) fn plugin_thread_main( wasm_bridge.start_fs_watcher_if_not_started(); }, PluginInstruction::ChangePluginHostDir(new_host_folder, plugin_id, client_id) => { - wasm_bridge - .change_plugin_host_dir(new_host_folder, plugin_id, client_id) - .non_fatal(); + if let Ok(_) = wasm_bridge.change_plugin_host_dir( + new_host_folder.clone(), + plugin_id, + client_id, + ) { + drop( + bus.senders.send_to_pty(PtyInstruction::ReportPluginCwd( + plugin_id, + new_host_folder, + )), + ); + } }, PluginInstruction::WebServerStarted(base_url) => { let updates = vec