feat(wasm-plugin-system): major overhaul and some goodies (#2510)
* strider resiliency * worker channel prototype * finalized ui * show hide plugin * fs events to plugins * tests for events and new screen instructions * various refactoringz * report plugin errors instead of crashing zellij * fix plugin loading with workers * refactor: move watch filesystem * some fixes and refactoring * refactor(panes): combine pane insertion logic * refactor(screen): launch or focus * refactor(pty): consolidate default shell fetching * refactor: various cleanups * initial refactoring * more initial refactoring * refactor(strider): search * style(fmt): rustfmt * style(pty): cleanup * style(clippy): ok clippy * style(fmt): rustfmt
This commit is contained in:
parent
b8f095330a
commit
c11d75f915
52 changed files with 3067 additions and 1251 deletions
120
Cargo.lock
generated
120
Cargo.lock
generated
|
|
@ -75,11 +75,11 @@ checksum = "8da52d66c7071e2e3fa2a1e5c6d088fec47b593032b254f5e980de8ea54454d6"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "async-channel"
|
name = "async-channel"
|
||||||
version = "1.6.1"
|
version = "1.8.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "2114d64672151c0c5eaa5e131ec84a74f06e1e559830dabba01ca30605d66319"
|
checksum = "cf46fee83e5ccffc220104713af3292ff9bc7c64c7de289f66dae8e38d826833"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"concurrent-queue",
|
"concurrent-queue 2.2.0",
|
||||||
"event-listener",
|
"event-listener",
|
||||||
"futures-core",
|
"futures-core",
|
||||||
]
|
]
|
||||||
|
|
@ -91,7 +91,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "871f9bb5e0a22eeb7e8cf16641feb87c9dc67032ccf8ff49e772eb9941d3a965"
|
checksum = "871f9bb5e0a22eeb7e8cf16641feb87c9dc67032ccf8ff49e772eb9941d3a965"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"async-task",
|
"async-task",
|
||||||
"concurrent-queue",
|
"concurrent-queue 1.2.2",
|
||||||
"fastrand",
|
"fastrand",
|
||||||
"futures-lite",
|
"futures-lite",
|
||||||
"once_cell",
|
"once_cell",
|
||||||
|
|
@ -119,7 +119,7 @@ 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 = "e5e18f61464ae81cde0a23e713ae8fd299580c54d697a35820cfd0625b8b0e07"
|
checksum = "e5e18f61464ae81cde0a23e713ae8fd299580c54d697a35820cfd0625b8b0e07"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"concurrent-queue",
|
"concurrent-queue 1.2.2",
|
||||||
"futures-lite",
|
"futures-lite",
|
||||||
"libc",
|
"libc",
|
||||||
"log",
|
"log",
|
||||||
|
|
@ -501,6 +501,15 @@ dependencies = [
|
||||||
"cache-padded",
|
"cache-padded",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "concurrent-queue"
|
||||||
|
version = "2.2.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "62ec6771ecfa0762d24683ee5a32ad78487a3d3afdc0fb8cae19d2c5deb50b7c"
|
||||||
|
dependencies = [
|
||||||
|
"crossbeam-utils",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "console"
|
name = "console"
|
||||||
version = "0.15.0"
|
version = "0.15.0"
|
||||||
|
|
@ -679,12 +688,11 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "crossbeam-utils"
|
name = "crossbeam-utils"
|
||||||
version = "0.8.8"
|
version = "0.8.15"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "0bf124c720b7686e3c2663cf54062ab0f68a88af2fb6a030e87e30bf721fcb38"
|
checksum = "3c063cd8cc95f5c377ed0d4b49a4b21f632396ff690e8470c29b3359b346984b"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"cfg-if 1.0.0",
|
"cfg-if 1.0.0",
|
||||||
"lazy_static",
|
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|
@ -1008,6 +1016,18 @@ dependencies = [
|
||||||
"winapi",
|
"winapi",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "filetime"
|
||||||
|
version = "0.2.21"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "5cbc844cecaee9d4443931972e1289c8ff485cb4cc2767cb03ca139ed6885153"
|
||||||
|
dependencies = [
|
||||||
|
"cfg-if 1.0.0",
|
||||||
|
"libc",
|
||||||
|
"redox_syscall",
|
||||||
|
"windows-sys 0.48.0",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "finl_unicode"
|
name = "finl_unicode"
|
||||||
version = "1.2.0"
|
version = "1.2.0"
|
||||||
|
|
@ -1045,6 +1065,15 @@ dependencies = [
|
||||||
"percent-encoding",
|
"percent-encoding",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "fsevent-sys"
|
||||||
|
version = "4.1.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "76ee7a02da4d231650c7cea31349b889be2f45ddb3ef3032d2ec8185f6313fd2"
|
||||||
|
dependencies = [
|
||||||
|
"libc",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "futures-channel"
|
name = "futures-channel"
|
||||||
version = "0.3.21"
|
version = "0.3.21"
|
||||||
|
|
@ -1277,6 +1306,26 @@ dependencies = [
|
||||||
"serde",
|
"serde",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "inotify"
|
||||||
|
version = "0.9.6"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "f8069d3ec154eb856955c1c0fbffefbf5f3c40a104ec912d4797314c1801abff"
|
||||||
|
dependencies = [
|
||||||
|
"bitflags",
|
||||||
|
"inotify-sys",
|
||||||
|
"libc",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "inotify-sys"
|
||||||
|
version = "0.1.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "e05c02b5e89bff3b946cedeca278abc628fe811e604f027c45a8aa3cf793d0eb"
|
||||||
|
dependencies = [
|
||||||
|
"libc",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "insta"
|
name = "insta"
|
||||||
version = "1.14.0"
|
version = "1.14.0"
|
||||||
|
|
@ -1391,6 +1440,26 @@ dependencies = [
|
||||||
"thiserror",
|
"thiserror",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "kqueue"
|
||||||
|
version = "1.0.7"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "2c8fc60ba15bf51257aa9807a48a61013db043fcf3a78cb0d916e8e396dcad98"
|
||||||
|
dependencies = [
|
||||||
|
"kqueue-sys",
|
||||||
|
"libc",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "kqueue-sys"
|
||||||
|
version = "1.0.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "8367585489f01bc55dd27404dcf56b95e6da061a256a666ab23be9ba96a2e587"
|
||||||
|
dependencies = [
|
||||||
|
"bitflags",
|
||||||
|
"libc",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "kv-log-macro"
|
name = "kv-log-macro"
|
||||||
version = "1.0.7"
|
version = "1.0.7"
|
||||||
|
|
@ -1663,6 +1732,18 @@ dependencies = [
|
||||||
"winapi",
|
"winapi",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "mio"
|
||||||
|
version = "0.8.6"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "5b9d9a46eff5b4ff64b45a9e316a6d1e0bc719ef429cbec4dc630684212bfdf9"
|
||||||
|
dependencies = [
|
||||||
|
"libc",
|
||||||
|
"log",
|
||||||
|
"wasi 0.11.0+wasi-snapshot-preview1",
|
||||||
|
"windows-sys 0.45.0",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "miow"
|
name = "miow"
|
||||||
version = "0.3.7"
|
version = "0.3.7"
|
||||||
|
|
@ -1732,6 +1813,24 @@ dependencies = [
|
||||||
"minimal-lexical",
|
"minimal-lexical",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "notify"
|
||||||
|
version = "6.0.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "4d9ba6c734de18ca27c8cef5cd7058aa4ac9f63596131e4c7e41e579319032a2"
|
||||||
|
dependencies = [
|
||||||
|
"bitflags",
|
||||||
|
"crossbeam-channel",
|
||||||
|
"filetime",
|
||||||
|
"fsevent-sys",
|
||||||
|
"inotify",
|
||||||
|
"kqueue",
|
||||||
|
"libc",
|
||||||
|
"mio 0.8.6",
|
||||||
|
"walkdir",
|
||||||
|
"windows-sys 0.45.0",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "ntapi"
|
name = "ntapi"
|
||||||
version = "0.3.7"
|
version = "0.3.7"
|
||||||
|
|
@ -2838,6 +2937,7 @@ dependencies = [
|
||||||
"pretty-bytes",
|
"pretty-bytes",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
|
"strip-ansi-escapes",
|
||||||
"unicode-width",
|
"unicode-width",
|
||||||
"walkdir",
|
"walkdir",
|
||||||
"zellij-tile",
|
"zellij-tile",
|
||||||
|
|
@ -4243,7 +4343,7 @@ version = "0.37.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"insta",
|
"insta",
|
||||||
"log",
|
"log",
|
||||||
"mio",
|
"mio 0.7.14",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
"serde_yaml",
|
"serde_yaml",
|
||||||
|
|
@ -4306,6 +4406,7 @@ name = "zellij-utils"
|
||||||
version = "0.37.0"
|
version = "0.37.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
|
"async-channel",
|
||||||
"async-std",
|
"async-std",
|
||||||
"backtrace",
|
"backtrace",
|
||||||
"clap",
|
"clap",
|
||||||
|
|
@ -4323,6 +4424,7 @@ dependencies = [
|
||||||
"log4rs",
|
"log4rs",
|
||||||
"miette",
|
"miette",
|
||||||
"nix 0.23.1",
|
"nix 0.23.1",
|
||||||
|
"notify",
|
||||||
"once_cell",
|
"once_cell",
|
||||||
"percent-encoding",
|
"percent-encoding",
|
||||||
"regex",
|
"regex",
|
||||||
|
|
|
||||||
|
|
@ -32,7 +32,7 @@ impl<'de> ZellijWorker<'de> for TestWorker {
|
||||||
}
|
}
|
||||||
|
|
||||||
register_plugin!(State);
|
register_plugin!(State);
|
||||||
register_worker!(TestWorker, test_worker);
|
register_worker!(TestWorker, test_worker, TEST_WORKER);
|
||||||
|
|
||||||
impl ZellijPlugin for State {
|
impl ZellijPlugin for State {
|
||||||
fn load(&mut self) {
|
fn load(&mut self) {
|
||||||
|
|
@ -40,6 +40,10 @@ impl ZellijPlugin for State {
|
||||||
EventType::InputReceived,
|
EventType::InputReceived,
|
||||||
EventType::SystemClipboardFailure,
|
EventType::SystemClipboardFailure,
|
||||||
EventType::CustomMessage,
|
EventType::CustomMessage,
|
||||||
|
EventType::FileSystemCreate,
|
||||||
|
EventType::FileSystemRead,
|
||||||
|
EventType::FileSystemUpdate,
|
||||||
|
EventType::FileSystemDelete,
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -16,3 +16,4 @@ serde = { version = "1.0", features = ["derive"] }
|
||||||
serde_json = "1.0"
|
serde_json = "1.0"
|
||||||
unicode-width = "0.1.8"
|
unicode-width = "0.1.8"
|
||||||
ansi_term = "0.12.1"
|
ansi_term = "0.12.1"
|
||||||
|
strip-ansi-escapes = "0.1.1"
|
||||||
|
|
|
||||||
|
|
@ -2,26 +2,45 @@ mod search;
|
||||||
mod state;
|
mod state;
|
||||||
|
|
||||||
use colored::*;
|
use colored::*;
|
||||||
use search::{ResultsOfSearch, SearchWorker};
|
use search::{FileContentsWorker, FileNameWorker, MessageToSearch, ResultsOfSearch};
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
use serde_json;
|
use serde_json;
|
||||||
use state::{refresh_directory, FsEntry, State, CURRENT_SEARCH_TERM};
|
use state::{refresh_directory, FsEntry, State};
|
||||||
use std::{cmp::min, time::Instant};
|
use std::{cmp::min, time::Instant};
|
||||||
use zellij_tile::prelude::*;
|
use zellij_tile::prelude::*;
|
||||||
|
|
||||||
register_plugin!(State);
|
register_plugin!(State);
|
||||||
register_worker!(SearchWorker, search_worker);
|
register_worker!(FileNameWorker, file_name_search_worker, FILE_NAME_WORKER);
|
||||||
|
register_worker!(
|
||||||
|
FileContentsWorker,
|
||||||
|
file_contents_search_worker,
|
||||||
|
FILE_CONTENTS_WORKER
|
||||||
|
);
|
||||||
|
|
||||||
impl ZellijPlugin for State {
|
impl ZellijPlugin for State {
|
||||||
fn load(&mut self) {
|
fn load(&mut self) {
|
||||||
refresh_directory(self);
|
refresh_directory(self);
|
||||||
self.loading = true;
|
self.search_state.loading = true;
|
||||||
subscribe(&[
|
subscribe(&[
|
||||||
EventType::Key,
|
EventType::Key,
|
||||||
EventType::Mouse,
|
EventType::Mouse,
|
||||||
EventType::CustomMessage,
|
EventType::CustomMessage,
|
||||||
EventType::Timer,
|
EventType::Timer,
|
||||||
|
EventType::FileSystemCreate,
|
||||||
|
EventType::FileSystemUpdate,
|
||||||
|
EventType::FileSystemDelete,
|
||||||
]);
|
]);
|
||||||
post_message_to("search", String::from("scan_folder"), String::new());
|
post_message_to(
|
||||||
|
"file_name_search",
|
||||||
|
serde_json::to_string(&MessageToSearch::ScanFolder).unwrap(),
|
||||||
|
"".to_owned(),
|
||||||
|
);
|
||||||
|
post_message_to(
|
||||||
|
"file_contents_search",
|
||||||
|
serde_json::to_string(&MessageToSearch::ScanFolder).unwrap(),
|
||||||
|
"".to_owned(),
|
||||||
|
);
|
||||||
|
self.search_state.loading = true;
|
||||||
set_timeout(0.5); // for displaying loading animation
|
set_timeout(0.5); // for displaying loading animation
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -35,57 +54,43 @@ impl ZellijPlugin for State {
|
||||||
self.ev_history.push_back((event.clone(), Instant::now()));
|
self.ev_history.push_back((event.clone(), Instant::now()));
|
||||||
match event {
|
match event {
|
||||||
Event::Timer(_elapsed) => {
|
Event::Timer(_elapsed) => {
|
||||||
should_render = true;
|
if self.search_state.loading {
|
||||||
if self.loading {
|
|
||||||
set_timeout(0.5);
|
set_timeout(0.5);
|
||||||
if self.loading_animation_offset == u8::MAX {
|
self.search_state.progress_animation();
|
||||||
self.loading_animation_offset = 0;
|
should_render = true;
|
||||||
} else {
|
|
||||||
self.loading_animation_offset =
|
|
||||||
self.loading_animation_offset.saturating_add(1);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
Event::CustomMessage(message, payload) => match message.as_str() {
|
Event::CustomMessage(message, payload) => match serde_json::from_str(&message) {
|
||||||
"update_search_results" => {
|
Ok(MessageToPlugin::UpdateFileNameSearchResults) => {
|
||||||
if let Ok(mut results_of_search) =
|
if let Ok(results_of_search) = serde_json::from_str::<ResultsOfSearch>(&payload)
|
||||||
serde_json::from_str::<ResultsOfSearch>(&payload)
|
|
||||||
{
|
{
|
||||||
if Some(results_of_search.search_term) == self.search_term {
|
self.search_state
|
||||||
self.search_results =
|
.update_file_name_search_results(results_of_search);
|
||||||
results_of_search.search_results.drain(..).collect();
|
|
||||||
should_render = true;
|
should_render = true;
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
Ok(MessageToPlugin::UpdateFileContentsSearchResults) => {
|
||||||
|
if let Ok(results_of_search) = serde_json::from_str::<ResultsOfSearch>(&payload)
|
||||||
|
{
|
||||||
|
self.search_state
|
||||||
|
.update_file_contents_search_results(results_of_search);
|
||||||
|
should_render = true;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"done_scanning_folder" => {
|
Ok(MessageToPlugin::DoneScanningFolder) => {
|
||||||
self.loading = false;
|
self.search_state.loading = false;
|
||||||
should_render = true;
|
should_render = true;
|
||||||
},
|
},
|
||||||
_ => {},
|
Err(e) => eprintln!("Failed to deserialize custom message: {:?}", e),
|
||||||
},
|
},
|
||||||
Event::Key(key) => match key {
|
Event::Key(key) => match key {
|
||||||
// modes:
|
Key::Esc if self.typing_search_term() => {
|
||||||
// 1. typing_search_term
|
self.stop_typing_search_term();
|
||||||
// 2. exploring_search_results
|
self.search_state.handle_key(key);
|
||||||
// 3. normal
|
|
||||||
Key::Esc | Key::Char('\n') if self.typing_search_term() => {
|
|
||||||
self.accept_search_term();
|
|
||||||
},
|
|
||||||
_ if self.typing_search_term() => {
|
|
||||||
self.append_to_search_term(key);
|
|
||||||
if let Some(search_term) = self.search_term.as_ref() {
|
|
||||||
std::fs::write(CURRENT_SEARCH_TERM, search_term.as_bytes()).unwrap();
|
|
||||||
post_message_to(
|
|
||||||
"search",
|
|
||||||
String::from("search"),
|
|
||||||
String::from(&self.search_term.clone().unwrap()),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
should_render = true;
|
should_render = true;
|
||||||
},
|
},
|
||||||
Key::Esc if self.exploring_search_results() => {
|
_ if self.typing_search_term() => {
|
||||||
self.stop_exploring_search_results();
|
self.search_state.handle_key(key);
|
||||||
should_render = true;
|
should_render = true;
|
||||||
},
|
},
|
||||||
Key::Char('/') => {
|
Key::Char('/') => {
|
||||||
|
|
@ -94,40 +99,27 @@ impl ZellijPlugin for State {
|
||||||
},
|
},
|
||||||
Key::Esc => {
|
Key::Esc => {
|
||||||
self.stop_typing_search_term();
|
self.stop_typing_search_term();
|
||||||
|
hide_self();
|
||||||
should_render = true;
|
should_render = true;
|
||||||
},
|
},
|
||||||
Key::Up | Key::Char('k') => {
|
Key::Up | Key::Char('k') => {
|
||||||
if self.exploring_search_results() {
|
|
||||||
self.move_search_selection_up();
|
|
||||||
should_render = true;
|
|
||||||
} else {
|
|
||||||
let currently_selected = self.selected();
|
let currently_selected = self.selected();
|
||||||
*self.selected_mut() = self.selected().saturating_sub(1);
|
*self.selected_mut() = self.selected().saturating_sub(1);
|
||||||
if currently_selected != self.selected() {
|
if currently_selected != self.selected() {
|
||||||
should_render = true;
|
should_render = true;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
},
|
},
|
||||||
Key::Down | Key::Char('j') => {
|
Key::Down | Key::Char('j') => {
|
||||||
if self.exploring_search_results() {
|
|
||||||
self.move_search_selection_down();
|
|
||||||
should_render = true;
|
|
||||||
} else {
|
|
||||||
let currently_selected = self.selected();
|
let currently_selected = self.selected();
|
||||||
let next = self.selected().saturating_add(1);
|
let next = self.selected().saturating_add(1);
|
||||||
*self.selected_mut() = min(self.files.len().saturating_sub(1), next);
|
*self.selected_mut() = min(self.files.len().saturating_sub(1), next);
|
||||||
if currently_selected != self.selected() {
|
if currently_selected != self.selected() {
|
||||||
should_render = true;
|
should_render = true;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
},
|
},
|
||||||
Key::Right | Key::Char('\n') | Key::Char('l') if !self.files.is_empty() => {
|
Key::Right | Key::Char('\n') | Key::Char('l') if !self.files.is_empty() => {
|
||||||
if self.exploring_search_results() {
|
|
||||||
self.open_search_result();
|
|
||||||
} else {
|
|
||||||
self.traverse_dir_or_open_file();
|
self.traverse_dir_or_open_file();
|
||||||
self.ev_history.clear();
|
self.ev_history.clear();
|
||||||
}
|
|
||||||
should_render = true;
|
should_render = true;
|
||||||
},
|
},
|
||||||
Key::Left | Key::Char('h') => {
|
Key::Left | Key::Char('h') => {
|
||||||
|
|
@ -190,6 +182,54 @@ impl ZellijPlugin for State {
|
||||||
},
|
},
|
||||||
_ => {},
|
_ => {},
|
||||||
},
|
},
|
||||||
|
Event::FileSystemCreate(paths) => {
|
||||||
|
let paths: Vec<String> = paths
|
||||||
|
.iter()
|
||||||
|
.map(|p| p.to_string_lossy().to_string())
|
||||||
|
.collect();
|
||||||
|
post_message_to(
|
||||||
|
"file_name_search",
|
||||||
|
serde_json::to_string(&MessageToSearch::FileSystemCreate).unwrap(),
|
||||||
|
serde_json::to_string(&paths).unwrap(),
|
||||||
|
);
|
||||||
|
post_message_to(
|
||||||
|
"file_contents_search",
|
||||||
|
serde_json::to_string(&MessageToSearch::FileSystemCreate).unwrap(),
|
||||||
|
serde_json::to_string(&paths).unwrap(),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
Event::FileSystemUpdate(paths) => {
|
||||||
|
let paths: Vec<String> = paths
|
||||||
|
.iter()
|
||||||
|
.map(|p| p.to_string_lossy().to_string())
|
||||||
|
.collect();
|
||||||
|
post_message_to(
|
||||||
|
"file_name_search",
|
||||||
|
serde_json::to_string(&MessageToSearch::FileSystemUpdate).unwrap(),
|
||||||
|
serde_json::to_string(&paths).unwrap(),
|
||||||
|
);
|
||||||
|
post_message_to(
|
||||||
|
"file_contents_search",
|
||||||
|
serde_json::to_string(&MessageToSearch::FileSystemUpdate).unwrap(),
|
||||||
|
serde_json::to_string(&paths).unwrap(),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
Event::FileSystemDelete(paths) => {
|
||||||
|
let paths: Vec<String> = paths
|
||||||
|
.iter()
|
||||||
|
.map(|p| p.to_string_lossy().to_string())
|
||||||
|
.collect();
|
||||||
|
post_message_to(
|
||||||
|
"file_name_search",
|
||||||
|
serde_json::to_string(&MessageToSearch::FileSystemDelete).unwrap(),
|
||||||
|
serde_json::to_string(&paths).unwrap(),
|
||||||
|
);
|
||||||
|
post_message_to(
|
||||||
|
"file_contents_search",
|
||||||
|
serde_json::to_string(&MessageToSearch::FileSystemDelete).unwrap(),
|
||||||
|
serde_json::to_string(&paths).unwrap(),
|
||||||
|
);
|
||||||
|
},
|
||||||
_ => {
|
_ => {
|
||||||
dbg!("Unknown event {:?}", event);
|
dbg!("Unknown event {:?}", event);
|
||||||
},
|
},
|
||||||
|
|
@ -198,8 +238,10 @@ impl ZellijPlugin for State {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn render(&mut self, rows: usize, cols: usize) {
|
fn render(&mut self, rows: usize, cols: usize) {
|
||||||
if self.typing_search_term() || self.exploring_search_results() {
|
if self.typing_search_term() {
|
||||||
return self.render_search(rows, cols);
|
self.search_state.change_size(rows, cols);
|
||||||
|
print!("{}", self.search_state);
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
for i in 0..rows {
|
for i in 0..rows {
|
||||||
|
|
@ -221,9 +263,9 @@ impl ZellijPlugin for State {
|
||||||
|
|
||||||
if i == self.selected() {
|
if i == self.selected() {
|
||||||
if is_last_row {
|
if is_last_row {
|
||||||
print!("{}", path.reversed());
|
print!("{}", path.clone().reversed());
|
||||||
} else {
|
} else {
|
||||||
println!("{}", path.reversed());
|
println!("{}", path.clone().reversed());
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if is_last_row {
|
if is_last_row {
|
||||||
|
|
@ -238,3 +280,10 @@ impl ZellijPlugin for State {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize)]
|
||||||
|
pub enum MessageToPlugin {
|
||||||
|
UpdateFileNameSearchResults,
|
||||||
|
UpdateFileContentsSearchResults,
|
||||||
|
DoneScanningFolder,
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,415 +0,0 @@
|
||||||
use crate::state::{State, CURRENT_SEARCH_TERM, ROOT};
|
|
||||||
|
|
||||||
use unicode_width::UnicodeWidthStr;
|
|
||||||
use zellij_tile::prelude::*;
|
|
||||||
|
|
||||||
use fuzzy_matcher::skim::SkimMatcherV2;
|
|
||||||
use fuzzy_matcher::FuzzyMatcher;
|
|
||||||
use serde::{Deserialize, Serialize};
|
|
||||||
use walkdir::WalkDir;
|
|
||||||
|
|
||||||
use std::io::{self, BufRead};
|
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize, Debug, Clone)]
|
|
||||||
pub enum SearchResult {
|
|
||||||
File {
|
|
||||||
path: String,
|
|
||||||
score: i64,
|
|
||||||
indices: Vec<usize>,
|
|
||||||
},
|
|
||||||
LineInFile {
|
|
||||||
path: String,
|
|
||||||
line: String,
|
|
||||||
line_number: usize,
|
|
||||||
score: i64,
|
|
||||||
indices: Vec<usize>,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
impl SearchResult {
|
|
||||||
pub fn new_file_name(score: i64, indices: Vec<usize>, path: String) -> Self {
|
|
||||||
SearchResult::File {
|
|
||||||
path,
|
|
||||||
score,
|
|
||||||
indices,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
pub fn new_file_line(
|
|
||||||
score: i64,
|
|
||||||
indices: Vec<usize>,
|
|
||||||
path: String,
|
|
||||||
line: String,
|
|
||||||
line_number: usize,
|
|
||||||
) -> Self {
|
|
||||||
SearchResult::LineInFile {
|
|
||||||
path,
|
|
||||||
score,
|
|
||||||
indices,
|
|
||||||
line,
|
|
||||||
line_number,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
pub fn score(&self) -> i64 {
|
|
||||||
match self {
|
|
||||||
SearchResult::File { score, .. } => *score,
|
|
||||||
SearchResult::LineInFile { score, .. } => *score,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
pub fn rendered_height(&self) -> usize {
|
|
||||||
match self {
|
|
||||||
SearchResult::File { .. } => 1,
|
|
||||||
SearchResult::LineInFile { .. } => 2,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
pub fn render(&self, max_width: usize, is_selected: bool) -> String {
|
|
||||||
let green_code = 154;
|
|
||||||
let orange_code = 166;
|
|
||||||
let bold_code = "\u{1b}[1m";
|
|
||||||
let green_foreground = format!("\u{1b}[38;5;{}m", green_code);
|
|
||||||
let orange_foreground = format!("\u{1b}[38;5;{}m", orange_code);
|
|
||||||
let reset_code = "\u{1b}[m";
|
|
||||||
let max_width = max_width.saturating_sub(3); // for the UI left line separator
|
|
||||||
match self {
|
|
||||||
SearchResult::File { path, indices, .. } => {
|
|
||||||
if is_selected {
|
|
||||||
let line = self.render_line_with_indices(
|
|
||||||
path,
|
|
||||||
indices,
|
|
||||||
max_width,
|
|
||||||
None,
|
|
||||||
Some(green_code),
|
|
||||||
true,
|
|
||||||
);
|
|
||||||
format!("{} | {}{}", green_foreground, reset_code, line)
|
|
||||||
} else {
|
|
||||||
let line =
|
|
||||||
self.render_line_with_indices(path, indices, max_width, None, None, true);
|
|
||||||
format!(" | {}", line)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
SearchResult::LineInFile {
|
|
||||||
path,
|
|
||||||
line,
|
|
||||||
line_number,
|
|
||||||
indices,
|
|
||||||
..
|
|
||||||
} => {
|
|
||||||
if is_selected {
|
|
||||||
let first_line = self.render_line_with_indices(
|
|
||||||
path,
|
|
||||||
&vec![],
|
|
||||||
max_width,
|
|
||||||
None,
|
|
||||||
Some(green_code),
|
|
||||||
true,
|
|
||||||
);
|
|
||||||
let line_indication_text = format!("{}-> {}", bold_code, line_number);
|
|
||||||
let line_indication = format!(
|
|
||||||
"{}{}{}",
|
|
||||||
orange_foreground, line_indication_text, reset_code
|
|
||||||
); // TODO: also truncate
|
|
||||||
let second_line = self.render_line_with_indices(
|
|
||||||
line,
|
|
||||||
indices,
|
|
||||||
max_width.saturating_sub(line_indication_text.width()),
|
|
||||||
None,
|
|
||||||
Some(orange_code),
|
|
||||||
false,
|
|
||||||
);
|
|
||||||
format!(
|
|
||||||
" {}│{} {}\n {}│{} {} {}",
|
|
||||||
green_foreground,
|
|
||||||
reset_code,
|
|
||||||
first_line,
|
|
||||||
green_foreground,
|
|
||||||
reset_code,
|
|
||||||
line_indication,
|
|
||||||
second_line
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
let first_line =
|
|
||||||
self.render_line_with_indices(path, &vec![], max_width, None, None, true); // TODO:
|
|
||||||
let line_indication_text = format!("{}-> {}", bold_code, line_number);
|
|
||||||
let second_line = self.render_line_with_indices(
|
|
||||||
line,
|
|
||||||
indices,
|
|
||||||
max_width.saturating_sub(line_indication_text.width()),
|
|
||||||
None,
|
|
||||||
None,
|
|
||||||
false,
|
|
||||||
);
|
|
||||||
format!(
|
|
||||||
" │ {}\n │ {} {}",
|
|
||||||
first_line, line_indication_text, second_line
|
|
||||||
)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
fn render_line_with_indices(
|
|
||||||
&self,
|
|
||||||
line_to_render: &String,
|
|
||||||
indices: &Vec<usize>,
|
|
||||||
max_width: usize,
|
|
||||||
background_color: Option<usize>,
|
|
||||||
foreground_color: Option<usize>,
|
|
||||||
is_bold: bool,
|
|
||||||
) -> String {
|
|
||||||
// TODO: get these from Zellij
|
|
||||||
let reset_code = "\u{1b}[m";
|
|
||||||
let underline_code = "\u{1b}[4m";
|
|
||||||
let foreground_color = foreground_color
|
|
||||||
.map(|c| format!("\u{1b}[38;5;{}m", c))
|
|
||||||
.unwrap_or_else(|| format!(""));
|
|
||||||
let background_color = background_color
|
|
||||||
.map(|c| format!("\u{1b}[48;5;{}m", c))
|
|
||||||
.unwrap_or_else(|| format!(""));
|
|
||||||
let bold = if is_bold { "\u{1b}[1m" } else { "" };
|
|
||||||
let non_index_character_style = format!("{}{}{}", background_color, foreground_color, bold);
|
|
||||||
let index_character_style = format!(
|
|
||||||
"{}{}{}{}",
|
|
||||||
background_color, foreground_color, bold, underline_code
|
|
||||||
);
|
|
||||||
|
|
||||||
let mut truncate_start_position = None;
|
|
||||||
let mut truncate_end_position = None;
|
|
||||||
if line_to_render.width() > max_width {
|
|
||||||
let length_of_each_half = max_width.saturating_sub(4) / 2;
|
|
||||||
truncate_start_position = Some(length_of_each_half);
|
|
||||||
truncate_end_position =
|
|
||||||
Some(line_to_render.width().saturating_sub(length_of_each_half));
|
|
||||||
}
|
|
||||||
let mut first_half = format!("{}", reset_code);
|
|
||||||
let mut second_half = format!("{}", reset_code);
|
|
||||||
for (i, character) in line_to_render.chars().enumerate() {
|
|
||||||
if (truncate_start_position.is_none() && truncate_end_position.is_none())
|
|
||||||
|| Some(i) < truncate_start_position
|
|
||||||
{
|
|
||||||
if indices.contains(&i) {
|
|
||||||
first_half.push_str(&index_character_style);
|
|
||||||
first_half.push(character);
|
|
||||||
first_half.push_str(reset_code);
|
|
||||||
} else {
|
|
||||||
first_half.push_str(&non_index_character_style);
|
|
||||||
first_half.push(character);
|
|
||||||
first_half.push_str(reset_code);
|
|
||||||
}
|
|
||||||
} else if Some(i) > truncate_end_position {
|
|
||||||
if indices.contains(&i) {
|
|
||||||
second_half.push_str(&index_character_style);
|
|
||||||
second_half.push(character);
|
|
||||||
second_half.push_str(reset_code);
|
|
||||||
} else {
|
|
||||||
second_half.push_str(&non_index_character_style);
|
|
||||||
second_half.push(character);
|
|
||||||
second_half.push_str(reset_code);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if let Some(_truncate_start_position) = truncate_start_position {
|
|
||||||
format!(
|
|
||||||
"{}{}{}[..]{}{}{}",
|
|
||||||
first_half, reset_code, foreground_color, reset_code, second_half, reset_code
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
format!("{}{}", first_half, reset_code)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize, Debug, Clone)]
|
|
||||||
pub struct ResultsOfSearch {
|
|
||||||
pub search_term: String,
|
|
||||||
pub search_results: Vec<SearchResult>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl ResultsOfSearch {
|
|
||||||
pub fn new(search_term: String, search_results: Vec<SearchResult>) -> Self {
|
|
||||||
ResultsOfSearch {
|
|
||||||
search_term,
|
|
||||||
search_results,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
pub fn limit_search_results(mut self, max_results: usize) -> Self {
|
|
||||||
self.search_results
|
|
||||||
.sort_by(|a, b| b.score().cmp(&a.score()));
|
|
||||||
self.search_results = if self.search_results.len() > max_results {
|
|
||||||
self.search_results.drain(..max_results).collect()
|
|
||||||
} else {
|
|
||||||
self.search_results.drain(..).collect()
|
|
||||||
};
|
|
||||||
self
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Default, Serialize, Deserialize)]
|
|
||||||
pub struct SearchWorker {
|
|
||||||
pub search_paths: Vec<String>,
|
|
||||||
pub search_file_contents: Vec<(String, usize, String)>, // file_name, line_number, line
|
|
||||||
skip_hidden_files: bool,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<'de> ZellijWorker<'de> for SearchWorker {
|
|
||||||
// TODO: handle out of order messages, likely when rendering
|
|
||||||
fn on_message(&mut self, message: String, payload: String) {
|
|
||||||
match message.as_str() {
|
|
||||||
// TODO: deserialize to type
|
|
||||||
"scan_folder" => {
|
|
||||||
self.populate_search_paths();
|
|
||||||
post_message_to_plugin("done_scanning_folder".into(), "".into());
|
|
||||||
},
|
|
||||||
"search" => {
|
|
||||||
let search_term = payload;
|
|
||||||
let (search_term, matches) = self.search(search_term);
|
|
||||||
let search_results =
|
|
||||||
ResultsOfSearch::new(search_term, matches).limit_search_results(100);
|
|
||||||
post_message_to_plugin(
|
|
||||||
"update_search_results".into(),
|
|
||||||
serde_json::to_string(&search_results).unwrap(),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
"skip_hidden_files" => match serde_json::from_str::<bool>(&payload) {
|
|
||||||
Ok(should_skip_hidden_files) => {
|
|
||||||
self.skip_hidden_files = should_skip_hidden_files;
|
|
||||||
},
|
|
||||||
Err(e) => {
|
|
||||||
eprintln!("Failed to deserialize payload: {:?}", e);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
_ => {},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl SearchWorker {
|
|
||||||
fn search(&mut self, search_term: String) -> (String, Vec<SearchResult>) {
|
|
||||||
if self.search_paths.is_empty() {
|
|
||||||
self.populate_search_paths();
|
|
||||||
}
|
|
||||||
let mut matches = vec![];
|
|
||||||
let mut matcher = SkimMatcherV2::default().use_cache(true).element_limit(100); // TODO: no hard
|
|
||||||
// coded limit!
|
|
||||||
self.search_file_names(&search_term, &mut matcher, &mut matches);
|
|
||||||
self.search_file_contents(&search_term, &mut matcher, &mut matches);
|
|
||||||
|
|
||||||
// if the search term changed before we finished, let's search again!
|
|
||||||
if let Ok(current_search_term) = std::fs::read(CURRENT_SEARCH_TERM) {
|
|
||||||
let current_search_term = String::from_utf8_lossy(¤t_search_term); // TODO: not lossy, search can be lots of stuff
|
|
||||||
if current_search_term != search_term {
|
|
||||||
return self.search(current_search_term.into());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
(search_term, matches)
|
|
||||||
}
|
|
||||||
fn populate_search_paths(&mut self) {
|
|
||||||
for entry in WalkDir::new(ROOT).into_iter().filter_map(|e| e.ok()) {
|
|
||||||
if self.skip_hidden_files
|
|
||||||
&& entry
|
|
||||||
.file_name()
|
|
||||||
.to_str()
|
|
||||||
.map(|s| s.starts_with('.'))
|
|
||||||
.unwrap_or(false)
|
|
||||||
{
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
let file_path = entry.path().display().to_string();
|
|
||||||
|
|
||||||
if entry.metadata().unwrap().is_file() {
|
|
||||||
if let Ok(file) = std::fs::File::open(&file_path) {
|
|
||||||
let lines = io::BufReader::new(file).lines();
|
|
||||||
for (index, line) in lines.enumerate() {
|
|
||||||
match line {
|
|
||||||
Ok(line) => {
|
|
||||||
self.search_file_contents.push((
|
|
||||||
file_path.clone(),
|
|
||||||
index + 1,
|
|
||||||
line,
|
|
||||||
));
|
|
||||||
},
|
|
||||||
Err(_) => {
|
|
||||||
break; // probably a binary file, skip it
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
self.search_paths.push(file_path);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
fn search_file_names(
|
|
||||||
&self,
|
|
||||||
search_term: &str,
|
|
||||||
matcher: &mut SkimMatcherV2,
|
|
||||||
matches: &mut Vec<SearchResult>,
|
|
||||||
) {
|
|
||||||
for entry in &self.search_paths {
|
|
||||||
if let Some((score, indices)) = matcher.fuzzy_indices(&entry, &search_term) {
|
|
||||||
matches.push(SearchResult::new_file_name(
|
|
||||||
score,
|
|
||||||
indices,
|
|
||||||
entry.to_owned(),
|
|
||||||
));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
fn search_file_contents(
|
|
||||||
&self,
|
|
||||||
search_term: &str,
|
|
||||||
matcher: &mut SkimMatcherV2,
|
|
||||||
matches: &mut Vec<SearchResult>,
|
|
||||||
) {
|
|
||||||
for (file_name, line_number, line_entry) in &self.search_file_contents {
|
|
||||||
if let Some((score, indices)) = matcher.fuzzy_indices(&line_entry, &search_term) {
|
|
||||||
matches.push(SearchResult::new_file_line(
|
|
||||||
score,
|
|
||||||
indices,
|
|
||||||
file_name.clone(),
|
|
||||||
line_entry.clone(),
|
|
||||||
*line_number,
|
|
||||||
));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl State {
|
|
||||||
pub fn render_search(&mut self, rows: usize, cols: usize) {
|
|
||||||
if let Some(search_term) = self.search_term.as_ref() {
|
|
||||||
let mut to_render = String::new();
|
|
||||||
to_render.push_str(&format!(
|
|
||||||
" \u{1b}[38;5;51;1mSEARCH:\u{1b}[m {}\n",
|
|
||||||
search_term
|
|
||||||
));
|
|
||||||
let mut rows_left_to_render = rows.saturating_sub(3);
|
|
||||||
if self.loading && self.search_results.is_empty() {
|
|
||||||
to_render.push_str(&self.render_loading());
|
|
||||||
}
|
|
||||||
for (i, result) in self
|
|
||||||
.search_results
|
|
||||||
.iter()
|
|
||||||
.enumerate()
|
|
||||||
.take(rows.saturating_sub(3))
|
|
||||||
{
|
|
||||||
let result_height = result.rendered_height();
|
|
||||||
if result_height + 1 > rows_left_to_render {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
rows_left_to_render -= result_height;
|
|
||||||
rows_left_to_render -= 1; // space between
|
|
||||||
let is_selected = i == self.selected_search_result;
|
|
||||||
let rendered_result = result.render(cols, is_selected);
|
|
||||||
to_render.push_str(&format!("\n{}\n", rendered_result));
|
|
||||||
}
|
|
||||||
print!("{}", to_render);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
pub fn render_loading(&self) -> String {
|
|
||||||
let mut rendered = String::from("Scanning folder");
|
|
||||||
let dot_count = self.loading_animation_offset % 4;
|
|
||||||
for _ in 0..dot_count {
|
|
||||||
rendered.push('.');
|
|
||||||
}
|
|
||||||
rendered
|
|
||||||
}
|
|
||||||
}
|
|
||||||
353
default-plugins/strider/src/search/controls_line.rs
Normal file
353
default-plugins/strider/src/search/controls_line.rs
Normal file
|
|
@ -0,0 +1,353 @@
|
||||||
|
use crate::search::search_state::SearchType;
|
||||||
|
use crate::search::ui::{
|
||||||
|
arrow, bold, color_line_to_end, dot, styled_text, BLACK, GRAY_DARK, GRAY_LIGHT, RED, WHITE,
|
||||||
|
};
|
||||||
|
|
||||||
|
#[derive(Default)]
|
||||||
|
pub struct ControlsLine {
|
||||||
|
controls: Vec<Control>,
|
||||||
|
scanning_indication: Option<Vec<&'static str>>,
|
||||||
|
animation_offset: u8,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ControlsLine {
|
||||||
|
pub fn new(controls: Vec<Control>, scanning_indication: Option<Vec<&'static str>>) -> Self {
|
||||||
|
ControlsLine {
|
||||||
|
controls,
|
||||||
|
scanning_indication,
|
||||||
|
..Default::default()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pub fn with_animation_offset(mut self, animation_offset: u8) -> Self {
|
||||||
|
self.animation_offset = animation_offset;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
pub fn render(&self, max_width: usize, show_controls: bool) -> String {
|
||||||
|
if show_controls {
|
||||||
|
self.render_controls(max_width)
|
||||||
|
} else {
|
||||||
|
self.render_empty_line(max_width)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pub fn render_controls(&self, max_width: usize) -> String {
|
||||||
|
let loading_animation =
|
||||||
|
LoadingAnimation::new(&self.scanning_indication, self.animation_offset);
|
||||||
|
let full_length = loading_animation.full_len()
|
||||||
|
+ self.controls.iter().map(|c| c.full_len()).sum::<usize>();
|
||||||
|
let mid_length =
|
||||||
|
loading_animation.mid_len() + self.controls.iter().map(|c| c.mid_len()).sum::<usize>();
|
||||||
|
let short_length = loading_animation.short_len()
|
||||||
|
+ self.controls.iter().map(|c| c.short_len()).sum::<usize>();
|
||||||
|
if max_width >= full_length {
|
||||||
|
let mut to_render = String::new();
|
||||||
|
for control in &self.controls {
|
||||||
|
to_render.push_str(&control.render_full_length());
|
||||||
|
}
|
||||||
|
to_render.push_str(&self.render_padding(max_width.saturating_sub(full_length)));
|
||||||
|
to_render.push_str(&loading_animation.render_full_length());
|
||||||
|
to_render
|
||||||
|
} else if max_width >= mid_length {
|
||||||
|
let mut to_render = String::new();
|
||||||
|
for control in &self.controls {
|
||||||
|
to_render.push_str(&control.render_mid_length());
|
||||||
|
}
|
||||||
|
to_render.push_str(&self.render_padding(max_width.saturating_sub(mid_length)));
|
||||||
|
to_render.push_str(&loading_animation.render_mid_length());
|
||||||
|
to_render
|
||||||
|
} else if max_width >= short_length {
|
||||||
|
let mut to_render = String::new();
|
||||||
|
for control in &self.controls {
|
||||||
|
to_render.push_str(&control.render_short_length());
|
||||||
|
}
|
||||||
|
to_render.push_str(&self.render_padding(max_width.saturating_sub(short_length)));
|
||||||
|
to_render.push_str(&loading_animation.render_short_length());
|
||||||
|
to_render
|
||||||
|
} else {
|
||||||
|
format!("")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pub fn render_empty_line(&self, max_width: usize) -> String {
|
||||||
|
let loading_animation =
|
||||||
|
LoadingAnimation::new(&self.scanning_indication, self.animation_offset);
|
||||||
|
let mut to_render = String::new();
|
||||||
|
if max_width >= loading_animation.full_len() {
|
||||||
|
to_render.push_str(
|
||||||
|
&self.render_padding(max_width.saturating_sub(loading_animation.full_len())),
|
||||||
|
);
|
||||||
|
to_render.push_str(&loading_animation.render_full_length());
|
||||||
|
} else if max_width >= loading_animation.mid_len() {
|
||||||
|
to_render.push_str(
|
||||||
|
&self.render_padding(max_width.saturating_sub(loading_animation.mid_len())),
|
||||||
|
);
|
||||||
|
to_render.push_str(&loading_animation.render_mid_length());
|
||||||
|
} else if max_width >= loading_animation.short_len() {
|
||||||
|
to_render.push_str(
|
||||||
|
&self.render_padding(max_width.saturating_sub(loading_animation.short_len())),
|
||||||
|
);
|
||||||
|
to_render.push_str(&loading_animation.render_short_length());
|
||||||
|
}
|
||||||
|
to_render
|
||||||
|
}
|
||||||
|
fn render_padding(&self, padding: usize) -> String {
|
||||||
|
// TODO: color whole line
|
||||||
|
format!("{}\u{1b}[{}C", color_line_to_end(GRAY_LIGHT), padding)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct Control {
|
||||||
|
key: &'static str,
|
||||||
|
options: Vec<&'static str>,
|
||||||
|
option_index: (usize, usize), // eg. 1 out of 2 (1, 2)
|
||||||
|
keycode_background_color: u8,
|
||||||
|
keycode_foreground_color: u8,
|
||||||
|
control_text_background_color: u8,
|
||||||
|
control_text_foreground_color: u8,
|
||||||
|
active_dot_color: u8,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for Control {
|
||||||
|
fn default() -> Self {
|
||||||
|
Control {
|
||||||
|
key: "",
|
||||||
|
options: vec![],
|
||||||
|
option_index: (0, 0),
|
||||||
|
keycode_background_color: GRAY_LIGHT,
|
||||||
|
keycode_foreground_color: WHITE,
|
||||||
|
control_text_background_color: GRAY_DARK,
|
||||||
|
control_text_foreground_color: BLACK,
|
||||||
|
active_dot_color: RED,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Control {
|
||||||
|
pub fn new(
|
||||||
|
key: &'static str,
|
||||||
|
options: Vec<&'static str>,
|
||||||
|
option_index: (usize, usize),
|
||||||
|
) -> Self {
|
||||||
|
Control {
|
||||||
|
key,
|
||||||
|
options,
|
||||||
|
option_index,
|
||||||
|
..Default::default()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pub fn new_floating_control(key: &'static str, should_open_floating: bool) -> Self {
|
||||||
|
if should_open_floating {
|
||||||
|
Control::new(key, vec!["OPEN FLOATING", "FLOATING", "F"], (2, 2))
|
||||||
|
} else {
|
||||||
|
Control::new(key, vec!["OPEN TILED", "TILED", "T"], (1, 2))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pub fn new_filter_control(key: &'static str, search_filter: &SearchType) -> Self {
|
||||||
|
match search_filter {
|
||||||
|
SearchType::NamesAndContents => Control::new(
|
||||||
|
key,
|
||||||
|
vec!["FILE NAMES AND CONTENTS", "NAMES + CONTENTS", "N+C"],
|
||||||
|
(1, 3),
|
||||||
|
),
|
||||||
|
SearchType::Names => Control::new(key, vec!["FILE NAMES", "NAMES", "N"], (2, 3)),
|
||||||
|
SearchType::Contents => {
|
||||||
|
Control::new(key, vec!["FILE CONTENTS", "CONTENTS", "C"], (3, 3))
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pub fn short_len(&self) -> usize {
|
||||||
|
let short_text = self
|
||||||
|
.options
|
||||||
|
.get(2)
|
||||||
|
.or_else(|| self.options.get(1))
|
||||||
|
.or_else(|| self.options.get(0))
|
||||||
|
.unwrap_or(&"");
|
||||||
|
short_text.chars().count() + self.key.chars().count() + self.option_index.1 + 7
|
||||||
|
// 7 for all the spaces and decorations
|
||||||
|
}
|
||||||
|
pub fn mid_len(&self) -> usize {
|
||||||
|
let mid_text = self
|
||||||
|
.options
|
||||||
|
.get(1)
|
||||||
|
.or_else(|| self.options.get(0))
|
||||||
|
.unwrap_or(&"");
|
||||||
|
mid_text.chars().count() + self.key.chars().count() + self.option_index.1 + 7
|
||||||
|
// 7 for all the spaces and decorations
|
||||||
|
}
|
||||||
|
pub fn full_len(&self) -> usize {
|
||||||
|
let full_text = self.options.get(0).unwrap_or(&"");
|
||||||
|
full_text.chars().count() + self.key.chars().count() + self.option_index.1 + 7
|
||||||
|
// 7 for all the spaces and decorations
|
||||||
|
}
|
||||||
|
pub fn render_short_length(&self) -> String {
|
||||||
|
let short_text = self
|
||||||
|
.options
|
||||||
|
.get(2)
|
||||||
|
.or_else(|| self.options.get(1))
|
||||||
|
.or_else(|| self.options.get(0))
|
||||||
|
.unwrap_or(&"");
|
||||||
|
self.render(short_text)
|
||||||
|
}
|
||||||
|
pub fn render_mid_length(&self) -> String {
|
||||||
|
let mid_text = self
|
||||||
|
.options
|
||||||
|
.get(1)
|
||||||
|
.or_else(|| self.options.get(0))
|
||||||
|
.unwrap_or(&"");
|
||||||
|
self.render(mid_text)
|
||||||
|
}
|
||||||
|
pub fn render_full_length(&self) -> String {
|
||||||
|
let full_text = self.options.get(0).unwrap_or(&"");
|
||||||
|
self.render(full_text)
|
||||||
|
}
|
||||||
|
fn render(&self, text: &str) -> String {
|
||||||
|
format!(
|
||||||
|
"{}{}{}{}{}{}",
|
||||||
|
self.render_keycode(&format!(" {} ", self.key)),
|
||||||
|
arrow(
|
||||||
|
self.keycode_background_color,
|
||||||
|
self.control_text_background_color
|
||||||
|
),
|
||||||
|
self.render_selection_dots(),
|
||||||
|
self.render_control_text(&format!("{} ", text)),
|
||||||
|
arrow(
|
||||||
|
self.control_text_background_color,
|
||||||
|
self.keycode_background_color
|
||||||
|
),
|
||||||
|
color_line_to_end(self.keycode_background_color),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
fn render_keycode(&self, text: &str) -> String {
|
||||||
|
styled_text(
|
||||||
|
self.keycode_foreground_color,
|
||||||
|
self.keycode_background_color,
|
||||||
|
&bold(text),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
fn render_control_text(&self, text: &str) -> String {
|
||||||
|
styled_text(
|
||||||
|
self.control_text_foreground_color,
|
||||||
|
self.control_text_background_color,
|
||||||
|
&bold(text),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
fn render_selection_dots(&self) -> String {
|
||||||
|
let mut selection_dots = String::from(" ");
|
||||||
|
for i in 1..=self.option_index.1 {
|
||||||
|
if i == self.option_index.0 {
|
||||||
|
selection_dots.push_str(&dot(
|
||||||
|
self.active_dot_color,
|
||||||
|
self.control_text_background_color,
|
||||||
|
));
|
||||||
|
} else {
|
||||||
|
selection_dots.push_str(&dot(
|
||||||
|
self.control_text_foreground_color,
|
||||||
|
self.control_text_background_color,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
selection_dots.push_str(" ");
|
||||||
|
selection_dots
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct LoadingAnimation {
|
||||||
|
scanning_indication: Option<Vec<&'static str>>,
|
||||||
|
animation_offset: u8,
|
||||||
|
background_color: u8,
|
||||||
|
foreground_color: u8,
|
||||||
|
}
|
||||||
|
impl LoadingAnimation {
|
||||||
|
pub fn new(scanning_indication: &Option<Vec<&'static str>>, animation_offset: u8) -> Self {
|
||||||
|
LoadingAnimation {
|
||||||
|
scanning_indication: scanning_indication.clone(),
|
||||||
|
animation_offset,
|
||||||
|
background_color: GRAY_LIGHT,
|
||||||
|
foreground_color: WHITE,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pub fn full_len(&self) -> usize {
|
||||||
|
self.scanning_indication
|
||||||
|
.as_ref()
|
||||||
|
.and_then(|scanning_indication| scanning_indication.get(0))
|
||||||
|
.map(|s| s.chars().count() + 3) // 3 for animation dots
|
||||||
|
.unwrap_or(0)
|
||||||
|
}
|
||||||
|
pub fn mid_len(&self) -> usize {
|
||||||
|
self.scanning_indication
|
||||||
|
.as_ref()
|
||||||
|
.and_then(|scanning_indication| {
|
||||||
|
scanning_indication
|
||||||
|
.get(1)
|
||||||
|
.or_else(|| scanning_indication.get(0))
|
||||||
|
})
|
||||||
|
.map(|s| s.chars().count() + 3) // 3 for animation dots
|
||||||
|
.unwrap_or(0)
|
||||||
|
}
|
||||||
|
pub fn short_len(&self) -> usize {
|
||||||
|
self.scanning_indication
|
||||||
|
.as_ref()
|
||||||
|
.and_then(|scanning_indication| {
|
||||||
|
scanning_indication
|
||||||
|
.get(2)
|
||||||
|
.or_else(|| scanning_indication.get(1))
|
||||||
|
.or_else(|| scanning_indication.get(0))
|
||||||
|
})
|
||||||
|
.map(|s| s.chars().count() + 3) // 3 for animation dots
|
||||||
|
.unwrap_or(0)
|
||||||
|
}
|
||||||
|
pub fn render_full_length(&self) -> String {
|
||||||
|
self.scanning_indication
|
||||||
|
.as_ref()
|
||||||
|
.and_then(|scanning_indication| scanning_indication.get(0))
|
||||||
|
.map(|s| {
|
||||||
|
styled_text(
|
||||||
|
self.foreground_color,
|
||||||
|
self.background_color,
|
||||||
|
&bold(&(s.to_string() + &self.animation_dots())),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.unwrap_or_else(String::new)
|
||||||
|
}
|
||||||
|
pub fn render_mid_length(&self) -> String {
|
||||||
|
self.scanning_indication
|
||||||
|
.as_ref()
|
||||||
|
.and_then(|scanning_indication| {
|
||||||
|
scanning_indication
|
||||||
|
.get(1)
|
||||||
|
.or_else(|| scanning_indication.get(0))
|
||||||
|
})
|
||||||
|
.map(|s| {
|
||||||
|
styled_text(
|
||||||
|
self.background_color,
|
||||||
|
self.foreground_color,
|
||||||
|
&bold(&(s.to_string() + &self.animation_dots())),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.unwrap_or_else(String::new)
|
||||||
|
}
|
||||||
|
pub fn render_short_length(&self) -> String {
|
||||||
|
self.scanning_indication
|
||||||
|
.as_ref()
|
||||||
|
.and_then(|scanning_indication| {
|
||||||
|
scanning_indication
|
||||||
|
.get(2)
|
||||||
|
.or_else(|| scanning_indication.get(1))
|
||||||
|
.or_else(|| scanning_indication.get(0))
|
||||||
|
})
|
||||||
|
.map(|s| {
|
||||||
|
styled_text(
|
||||||
|
self.background_color,
|
||||||
|
self.foreground_color,
|
||||||
|
&bold(&(s.to_string() + &self.animation_dots())),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.unwrap_or_else(String::new)
|
||||||
|
}
|
||||||
|
fn animation_dots(&self) -> String {
|
||||||
|
let mut to_render = String::from("");
|
||||||
|
let dot_count = self.animation_offset % 4;
|
||||||
|
for _ in 0..dot_count {
|
||||||
|
to_render.push('.');
|
||||||
|
}
|
||||||
|
to_render
|
||||||
|
}
|
||||||
|
}
|
||||||
329
default-plugins/strider/src/search/mod.rs
Normal file
329
default-plugins/strider/src/search/mod.rs
Normal file
|
|
@ -0,0 +1,329 @@
|
||||||
|
pub mod controls_line;
|
||||||
|
pub mod search_results;
|
||||||
|
pub mod search_state;
|
||||||
|
pub mod selection_controls_area;
|
||||||
|
pub mod ui;
|
||||||
|
|
||||||
|
use crate::state::{CURRENT_SEARCH_TERM, ROOT};
|
||||||
|
use crate::MessageToPlugin;
|
||||||
|
use search_state::SearchType;
|
||||||
|
use std::collections::{BTreeMap, BTreeSet, HashMap};
|
||||||
|
use std::path::{Path, PathBuf};
|
||||||
|
|
||||||
|
use unicode_width::UnicodeWidthStr;
|
||||||
|
use zellij_tile::prelude::*;
|
||||||
|
|
||||||
|
use fuzzy_matcher::skim::SkimMatcherV2;
|
||||||
|
use fuzzy_matcher::FuzzyMatcher;
|
||||||
|
use search_results::SearchResult;
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use walkdir::WalkDir;
|
||||||
|
|
||||||
|
use std::io::{self, BufRead};
|
||||||
|
|
||||||
|
#[derive(Default, Serialize, Deserialize)]
|
||||||
|
pub struct Search {
|
||||||
|
search_type: SearchType,
|
||||||
|
file_names: BTreeSet<String>,
|
||||||
|
file_contents: BTreeMap<(String, usize), String>, // file_name, line_number, line
|
||||||
|
cached_file_name_results: HashMap<String, Vec<SearchResult>>,
|
||||||
|
cached_file_contents_results: HashMap<String, Vec<SearchResult>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Search {
|
||||||
|
pub fn new(search_type: SearchType) -> Self {
|
||||||
|
Search {
|
||||||
|
search_type,
|
||||||
|
..Default::default()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fn on_message(&mut self, message: String, payload: String) {
|
||||||
|
match serde_json::from_str::<MessageToSearch>(&message) {
|
||||||
|
Ok(MessageToSearch::ScanFolder) => {
|
||||||
|
self.scan_hd();
|
||||||
|
post_message_to_plugin(
|
||||||
|
serde_json::to_string(&MessageToPlugin::DoneScanningFolder).unwrap(),
|
||||||
|
"".to_owned(),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
Ok(MessageToSearch::Search) => {
|
||||||
|
if let Some(current_search_term) = self.read_search_term_from_hd_cache() {
|
||||||
|
self.search(current_search_term);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
Ok(MessageToSearch::FileSystemCreate) => {
|
||||||
|
self.rescan_files(payload);
|
||||||
|
},
|
||||||
|
Ok(MessageToSearch::FileSystemUpdate) => {
|
||||||
|
self.rescan_files(payload);
|
||||||
|
},
|
||||||
|
Ok(MessageToSearch::FileSystemDelete) => {
|
||||||
|
self.delete_files(payload);
|
||||||
|
},
|
||||||
|
Err(e) => eprintln!("Failed to deserialize worker message {:?}", e),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pub fn scan_hd(&mut self) {
|
||||||
|
for entry in WalkDir::new(ROOT).into_iter().filter_map(|e| e.ok()) {
|
||||||
|
self.add_file_entry(entry.path(), entry.metadata().ok());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pub fn search(&mut self, search_term: String) {
|
||||||
|
let search_results_limit = 100; // artificial limit to prevent probably unwanted chaos
|
||||||
|
// let mut matcher = SkimMatcherV2::default().use_cache(true).element_limit(search_results_limit);
|
||||||
|
let mut file_names_search_results = None;
|
||||||
|
let mut file_contents_search_results = None;
|
||||||
|
if let SearchType::Names | SearchType::NamesAndContents = self.search_type {
|
||||||
|
let file_names_matches = match self.cached_file_name_results.get(&search_term) {
|
||||||
|
Some(cached_results) => cached_results.clone(),
|
||||||
|
None => {
|
||||||
|
let mut matcher = SkimMatcherV2::default().use_cache(true);
|
||||||
|
let results = self.search_file_names(&search_term, &mut matcher);
|
||||||
|
self.cached_file_name_results
|
||||||
|
.insert(search_term.clone(), results.clone());
|
||||||
|
results
|
||||||
|
},
|
||||||
|
};
|
||||||
|
file_names_search_results = Some(
|
||||||
|
ResultsOfSearch::new(search_term.clone(), file_names_matches)
|
||||||
|
.limit_search_results(search_results_limit),
|
||||||
|
);
|
||||||
|
};
|
||||||
|
if let SearchType::Contents | SearchType::NamesAndContents = self.search_type {
|
||||||
|
let file_contents_matches = match self.cached_file_contents_results.get(&search_term) {
|
||||||
|
Some(cached_results) => cached_results.clone(),
|
||||||
|
None => {
|
||||||
|
let mut matcher = SkimMatcherV2::default().use_cache(true);
|
||||||
|
let results = self.search_file_contents(&search_term, &mut matcher);
|
||||||
|
self.cached_file_contents_results
|
||||||
|
.insert(search_term.clone(), results.clone());
|
||||||
|
results
|
||||||
|
},
|
||||||
|
};
|
||||||
|
file_contents_search_results = Some(
|
||||||
|
ResultsOfSearch::new(search_term.clone(), file_contents_matches)
|
||||||
|
.limit_search_results(search_results_limit),
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
// if the search term changed before we finished, let's search again!
|
||||||
|
if let Some(current_search_term) = self.read_search_term_from_hd_cache() {
|
||||||
|
if current_search_term != search_term {
|
||||||
|
return self.search(current_search_term.into());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if let Some(file_names_search_results) = file_names_search_results {
|
||||||
|
post_message_to_plugin(
|
||||||
|
serde_json::to_string(&MessageToPlugin::UpdateFileNameSearchResults).unwrap(),
|
||||||
|
serde_json::to_string(&file_names_search_results).unwrap(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if let Some(file_contents_search_results) = file_contents_search_results {
|
||||||
|
post_message_to_plugin(
|
||||||
|
serde_json::to_string(&MessageToPlugin::UpdateFileContentsSearchResults).unwrap(),
|
||||||
|
serde_json::to_string(&file_contents_search_results).unwrap(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pub fn rescan_files(&mut self, paths: String) {
|
||||||
|
match serde_json::from_str::<Vec<PathBuf>>(&paths) {
|
||||||
|
Ok(paths) => {
|
||||||
|
for path in paths {
|
||||||
|
self.add_file_entry(&path, path.metadata().ok());
|
||||||
|
}
|
||||||
|
self.cached_file_name_results.clear();
|
||||||
|
self.cached_file_contents_results.clear();
|
||||||
|
},
|
||||||
|
Err(e) => eprintln!("Failed to deserialize paths: {:?}", e),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pub fn delete_files(&mut self, paths: String) {
|
||||||
|
match serde_json::from_str::<Vec<PathBuf>>(&paths) {
|
||||||
|
Ok(paths) => {
|
||||||
|
self.remove_existing_entries(&paths);
|
||||||
|
self.cached_file_name_results.clear();
|
||||||
|
self.cached_file_contents_results.clear();
|
||||||
|
},
|
||||||
|
Err(e) => eprintln!("Failed to deserialize paths: {:?}", e),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fn add_file_entry(&mut self, file_name: &Path, file_metadata: Option<std::fs::Metadata>) {
|
||||||
|
let file_path = file_name.display().to_string();
|
||||||
|
let file_path_stripped_prefix = self.strip_file_prefix(&file_name);
|
||||||
|
|
||||||
|
self.file_names.insert(file_path_stripped_prefix.clone());
|
||||||
|
if let SearchType::NamesAndContents | SearchType::Contents = self.search_type {
|
||||||
|
if file_metadata.map(|f| f.is_file()).unwrap_or(false) {
|
||||||
|
if let Ok(file) = std::fs::File::open(&file_path) {
|
||||||
|
let lines = io::BufReader::new(file).lines();
|
||||||
|
for (index, line) in lines.enumerate() {
|
||||||
|
match line {
|
||||||
|
Ok(line) => {
|
||||||
|
self.file_contents.insert(
|
||||||
|
(
|
||||||
|
// String::from_utf8_lossy(&strip_ansi_escapes::strip(file_path_stripped_prefix.clone()).unwrap()).to_string(),
|
||||||
|
file_path_stripped_prefix.clone(),
|
||||||
|
index + 1,
|
||||||
|
),
|
||||||
|
String::from_utf8_lossy(
|
||||||
|
&strip_ansi_escapes::strip(line).unwrap(),
|
||||||
|
)
|
||||||
|
.to_string(),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
Err(_) => {
|
||||||
|
break; // probably a binary file, skip it
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fn search_file_names(
|
||||||
|
&self,
|
||||||
|
search_term: &str,
|
||||||
|
matcher: &mut SkimMatcherV2,
|
||||||
|
) -> Vec<SearchResult> {
|
||||||
|
let mut matches = vec![];
|
||||||
|
for entry in &self.file_names {
|
||||||
|
if let Some((score, indices)) = matcher.fuzzy_indices(&entry, &search_term) {
|
||||||
|
matches.push(SearchResult::new_file_name(
|
||||||
|
score,
|
||||||
|
indices,
|
||||||
|
entry.to_owned(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
matches
|
||||||
|
}
|
||||||
|
fn search_file_contents(
|
||||||
|
&self,
|
||||||
|
search_term: &str,
|
||||||
|
matcher: &mut SkimMatcherV2,
|
||||||
|
) -> Vec<SearchResult> {
|
||||||
|
let mut matches = vec![];
|
||||||
|
for ((file_name, line_number), line_entry) in &self.file_contents {
|
||||||
|
if line_entry.contains("struct") {
|
||||||
|
if line_entry.len() < 400 {
|
||||||
|
eprintln!("matching against: {:?}", line_entry)
|
||||||
|
} else {
|
||||||
|
eprintln!("matching again line that has struct but is very long")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if let Some((score, indices)) = matcher.fuzzy_indices(&line_entry, &search_term) {
|
||||||
|
if line_entry.contains("struct") {
|
||||||
|
eprintln!("score: {:?}", score)
|
||||||
|
}
|
||||||
|
matches.push(SearchResult::new_file_line(
|
||||||
|
score,
|
||||||
|
indices,
|
||||||
|
file_name.clone(),
|
||||||
|
line_entry.clone(),
|
||||||
|
*line_number,
|
||||||
|
));
|
||||||
|
} else {
|
||||||
|
if line_entry.contains("struct") {
|
||||||
|
eprintln!("no score!")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
matches
|
||||||
|
}
|
||||||
|
fn strip_file_prefix(&self, file_name: &Path) -> String {
|
||||||
|
let mut file_path_stripped_prefix = file_name.display().to_string().split_off(ROOT.width());
|
||||||
|
if file_path_stripped_prefix.starts_with('/') {
|
||||||
|
file_path_stripped_prefix.remove(0);
|
||||||
|
}
|
||||||
|
file_path_stripped_prefix
|
||||||
|
}
|
||||||
|
fn read_search_term_from_hd_cache(&self) -> Option<String> {
|
||||||
|
match std::fs::read(CURRENT_SEARCH_TERM) {
|
||||||
|
Ok(current_search_term) => {
|
||||||
|
Some(String::from_utf8_lossy(¤t_search_term).to_string())
|
||||||
|
},
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fn remove_existing_entries(&mut self, paths: &Vec<PathBuf>) {
|
||||||
|
let file_path_stripped_prefixes: Vec<String> =
|
||||||
|
paths.iter().map(|p| self.strip_file_prefix(&p)).collect();
|
||||||
|
self.file_names
|
||||||
|
.retain(|file_name| !file_path_stripped_prefixes.contains(file_name));
|
||||||
|
self.file_contents.retain(|(file_name, _line_in_file), _| {
|
||||||
|
!file_path_stripped_prefixes.contains(file_name)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize)]
|
||||||
|
pub enum MessageToSearch {
|
||||||
|
ScanFolder,
|
||||||
|
Search,
|
||||||
|
FileSystemCreate,
|
||||||
|
FileSystemUpdate,
|
||||||
|
FileSystemDelete,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize)]
|
||||||
|
pub struct FileNameWorker {
|
||||||
|
search: Search,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for FileNameWorker {
|
||||||
|
fn default() -> Self {
|
||||||
|
FileNameWorker {
|
||||||
|
search: Search::new(SearchType::Names),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize)]
|
||||||
|
pub struct FileContentsWorker {
|
||||||
|
search: Search,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for FileContentsWorker {
|
||||||
|
fn default() -> Self {
|
||||||
|
FileContentsWorker {
|
||||||
|
search: Search::new(SearchType::Contents),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'de> ZellijWorker<'de> for FileNameWorker {
|
||||||
|
fn on_message(&mut self, message: String, payload: String) {
|
||||||
|
self.search.on_message(message, payload);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'de> ZellijWorker<'de> for FileContentsWorker {
|
||||||
|
fn on_message(&mut self, message: String, payload: String) {
|
||||||
|
self.search.on_message(message, payload);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||||
|
pub struct ResultsOfSearch {
|
||||||
|
pub search_term: String,
|
||||||
|
pub search_results: Vec<SearchResult>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ResultsOfSearch {
|
||||||
|
pub fn new(search_term: String, search_results: Vec<SearchResult>) -> Self {
|
||||||
|
ResultsOfSearch {
|
||||||
|
search_term,
|
||||||
|
search_results,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pub fn limit_search_results(mut self, max_results: usize) -> Self {
|
||||||
|
self.search_results
|
||||||
|
.sort_by(|a, b| b.score().cmp(&a.score()));
|
||||||
|
self.search_results = if self.search_results.len() > max_results {
|
||||||
|
self.search_results.drain(..max_results).collect()
|
||||||
|
} else {
|
||||||
|
self.search_results.drain(..).collect()
|
||||||
|
};
|
||||||
|
self
|
||||||
|
}
|
||||||
|
}
|
||||||
308
default-plugins/strider/src/search/search_results.rs
Normal file
308
default-plugins/strider/src/search/search_results.rs
Normal file
|
|
@ -0,0 +1,308 @@
|
||||||
|
use crate::search::ui::{
|
||||||
|
bold, styled_text, styled_text_background, styled_text_foreground, underline, GRAY_LIGHT,
|
||||||
|
GREEN, ORANGE,
|
||||||
|
};
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use unicode_width::UnicodeWidthStr;
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||||
|
pub enum SearchResult {
|
||||||
|
File {
|
||||||
|
path: String,
|
||||||
|
score: i64,
|
||||||
|
indices: Vec<usize>,
|
||||||
|
},
|
||||||
|
LineInFile {
|
||||||
|
path: String,
|
||||||
|
line: String,
|
||||||
|
line_number: usize,
|
||||||
|
score: i64,
|
||||||
|
indices: Vec<usize>,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SearchResult {
|
||||||
|
pub fn new_file_name(score: i64, indices: Vec<usize>, path: String) -> Self {
|
||||||
|
SearchResult::File {
|
||||||
|
path,
|
||||||
|
score,
|
||||||
|
indices,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pub fn new_file_line(
|
||||||
|
score: i64,
|
||||||
|
indices: Vec<usize>,
|
||||||
|
path: String,
|
||||||
|
line: String,
|
||||||
|
line_number: usize,
|
||||||
|
) -> Self {
|
||||||
|
SearchResult::LineInFile {
|
||||||
|
path,
|
||||||
|
score,
|
||||||
|
indices,
|
||||||
|
line,
|
||||||
|
line_number,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pub fn score(&self) -> i64 {
|
||||||
|
match self {
|
||||||
|
SearchResult::File { score, .. } => *score,
|
||||||
|
SearchResult::LineInFile { score, .. } => *score,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pub fn rendered_height(&self) -> usize {
|
||||||
|
match self {
|
||||||
|
SearchResult::File { .. } => 1,
|
||||||
|
SearchResult::LineInFile { .. } => 2,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pub fn is_same_entry(&self, other: &Self) -> bool {
|
||||||
|
match (&self, other) {
|
||||||
|
(
|
||||||
|
SearchResult::File { path: my_path, .. },
|
||||||
|
SearchResult::File {
|
||||||
|
path: other_path, ..
|
||||||
|
},
|
||||||
|
) => my_path == other_path,
|
||||||
|
(
|
||||||
|
SearchResult::LineInFile {
|
||||||
|
path: my_path,
|
||||||
|
line_number: my_line_number,
|
||||||
|
..
|
||||||
|
},
|
||||||
|
SearchResult::LineInFile {
|
||||||
|
path: other_path,
|
||||||
|
line_number: other_line_number,
|
||||||
|
..
|
||||||
|
},
|
||||||
|
) => my_path == other_path && my_line_number == other_line_number,
|
||||||
|
_ => false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pub fn render(
|
||||||
|
&self,
|
||||||
|
max_width: usize,
|
||||||
|
is_selected: bool,
|
||||||
|
is_below_search_result: bool,
|
||||||
|
) -> String {
|
||||||
|
let max_width = max_width.saturating_sub(4); // for the UI left line separator
|
||||||
|
match self {
|
||||||
|
SearchResult::File { path, indices, .. } => self.render_file_result(
|
||||||
|
path,
|
||||||
|
indices,
|
||||||
|
is_selected,
|
||||||
|
is_below_search_result,
|
||||||
|
max_width,
|
||||||
|
),
|
||||||
|
SearchResult::LineInFile {
|
||||||
|
path,
|
||||||
|
line,
|
||||||
|
line_number,
|
||||||
|
indices,
|
||||||
|
..
|
||||||
|
} => self.render_line_in_file_result(
|
||||||
|
path,
|
||||||
|
line,
|
||||||
|
*line_number,
|
||||||
|
indices,
|
||||||
|
is_selected,
|
||||||
|
is_below_search_result,
|
||||||
|
max_width,
|
||||||
|
),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fn render_file_result(
|
||||||
|
&self,
|
||||||
|
path: &String,
|
||||||
|
indices: &Vec<usize>,
|
||||||
|
is_selected: bool,
|
||||||
|
is_below_search_result: bool,
|
||||||
|
max_width: usize,
|
||||||
|
) -> String {
|
||||||
|
if is_selected {
|
||||||
|
let line = self.render_line_with_indices(
|
||||||
|
path,
|
||||||
|
indices,
|
||||||
|
max_width.saturating_sub(3),
|
||||||
|
Some(GREEN),
|
||||||
|
);
|
||||||
|
let selection_arrow = styled_text_foreground(ORANGE, "┌>");
|
||||||
|
format!("{} {}", selection_arrow, line)
|
||||||
|
} else {
|
||||||
|
let line_prefix = if is_below_search_result { "│ " } else { " " };
|
||||||
|
let line =
|
||||||
|
self.render_line_with_indices(path, indices, max_width.saturating_sub(3), None);
|
||||||
|
format!("{} {}", line_prefix, line)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fn render_line_in_file_result(
|
||||||
|
&self,
|
||||||
|
path: &String,
|
||||||
|
line: &String,
|
||||||
|
line_number: usize,
|
||||||
|
indices: &Vec<usize>,
|
||||||
|
is_selected: bool,
|
||||||
|
is_below_search_result: bool,
|
||||||
|
max_width: usize,
|
||||||
|
) -> String {
|
||||||
|
let line_number_prefix_text = format!("└ {} ", line_number);
|
||||||
|
let max_width_of_line_in_file = max_width
|
||||||
|
.saturating_sub(3)
|
||||||
|
.saturating_sub(line_number_prefix_text.width());
|
||||||
|
if is_selected {
|
||||||
|
let file_name_line = self.render_line_with_indices(
|
||||||
|
path,
|
||||||
|
&vec![],
|
||||||
|
max_width.saturating_sub(3),
|
||||||
|
Some(GREEN),
|
||||||
|
);
|
||||||
|
let line_in_file = self.render_line_with_indices(
|
||||||
|
line,
|
||||||
|
indices,
|
||||||
|
max_width_of_line_in_file,
|
||||||
|
Some(GREEN),
|
||||||
|
);
|
||||||
|
let line_number_prefix = styled_text_foreground(GREEN, &bold(&line_number_prefix_text));
|
||||||
|
format!(
|
||||||
|
"{} {}\n│ {}{}",
|
||||||
|
styled_text_foreground(ORANGE, "┌>"),
|
||||||
|
file_name_line,
|
||||||
|
line_number_prefix,
|
||||||
|
line_in_file
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
let file_name_line =
|
||||||
|
self.render_line_with_indices(path, &vec![], max_width.saturating_sub(3), None);
|
||||||
|
let line_in_file =
|
||||||
|
self.render_line_with_indices(line, indices, max_width_of_line_in_file, None);
|
||||||
|
let line_number_prefix = bold(&line_number_prefix_text);
|
||||||
|
let line_prefix = if is_below_search_result { "│ " } else { " " };
|
||||||
|
format!(
|
||||||
|
"{} {}\n{} {}{}",
|
||||||
|
line_prefix, file_name_line, line_prefix, line_number_prefix, line_in_file
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fn render_line_with_indices(
|
||||||
|
&self,
|
||||||
|
line_to_render: &String,
|
||||||
|
indices: &Vec<usize>,
|
||||||
|
max_width: usize,
|
||||||
|
foreground_color: Option<u8>,
|
||||||
|
) -> String {
|
||||||
|
let non_index_character_style = |c: &str| match foreground_color {
|
||||||
|
Some(foreground_color) => styled_text_foreground(foreground_color, &bold(c)),
|
||||||
|
None => bold(c),
|
||||||
|
};
|
||||||
|
let index_character_style = |c: &str| match foreground_color {
|
||||||
|
Some(foreground_color) => {
|
||||||
|
styled_text(foreground_color, GRAY_LIGHT, &bold(&underline(c)))
|
||||||
|
},
|
||||||
|
None => styled_text_background(GRAY_LIGHT, &bold(&underline(c))),
|
||||||
|
};
|
||||||
|
|
||||||
|
let truncate_positions =
|
||||||
|
self.truncate_line_with_indices(line_to_render, indices, max_width);
|
||||||
|
let truncate_start_position = truncate_positions.map(|p| p.0).unwrap_or(0);
|
||||||
|
let truncate_end_position = truncate_positions
|
||||||
|
.map(|p| p.1)
|
||||||
|
.unwrap_or(line_to_render.chars().count());
|
||||||
|
let mut visible_portion = String::new();
|
||||||
|
for (i, character) in line_to_render.chars().enumerate() {
|
||||||
|
if i >= truncate_start_position && i <= truncate_end_position {
|
||||||
|
if indices.contains(&i) {
|
||||||
|
visible_portion.push_str(&index_character_style(&character.to_string()));
|
||||||
|
} else {
|
||||||
|
visible_portion.push_str(&non_index_character_style(&character.to_string()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if truncate_positions.is_some() {
|
||||||
|
let left_truncate_sign = if truncate_start_position == 0 {
|
||||||
|
""
|
||||||
|
} else {
|
||||||
|
".."
|
||||||
|
};
|
||||||
|
let right_truncate_sign = if truncate_end_position == line_to_render.chars().count() {
|
||||||
|
""
|
||||||
|
} else {
|
||||||
|
".."
|
||||||
|
};
|
||||||
|
format!(
|
||||||
|
"{}{}{}",
|
||||||
|
non_index_character_style(left_truncate_sign),
|
||||||
|
visible_portion,
|
||||||
|
non_index_character_style(right_truncate_sign)
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
visible_portion
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fn truncate_line_with_indices(
|
||||||
|
&self,
|
||||||
|
line_to_render: &String,
|
||||||
|
indices: &Vec<usize>,
|
||||||
|
max_width: usize,
|
||||||
|
) -> Option<(usize, usize)> {
|
||||||
|
let first_index = indices.get(0).copied().unwrap_or(0);
|
||||||
|
let last_index = indices
|
||||||
|
.last()
|
||||||
|
.copied()
|
||||||
|
.unwrap_or_else(|| std::cmp::min(line_to_render.chars().count(), max_width));
|
||||||
|
if line_to_render.width() <= max_width {
|
||||||
|
// there's enough room, no need to truncate
|
||||||
|
None
|
||||||
|
} else if last_index.saturating_sub(first_index) < max_width {
|
||||||
|
// truncate around the indices
|
||||||
|
let mut width_remaining = max_width
|
||||||
|
.saturating_sub(1)
|
||||||
|
.saturating_sub(last_index.saturating_sub(first_index));
|
||||||
|
|
||||||
|
let mut string_start_position = first_index;
|
||||||
|
let mut string_end_position = last_index;
|
||||||
|
|
||||||
|
let mut i = 0;
|
||||||
|
loop {
|
||||||
|
if i >= width_remaining {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if string_start_position > 0 && string_end_position < line_to_render.chars().count()
|
||||||
|
{
|
||||||
|
let take_from_start = i % 2 == 0;
|
||||||
|
if take_from_start {
|
||||||
|
string_start_position -= 1;
|
||||||
|
if string_start_position == 0 {
|
||||||
|
width_remaining += 2; // no need for truncating dots
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
string_end_position += 1;
|
||||||
|
if string_end_position == line_to_render.chars().count() {
|
||||||
|
width_remaining += 2; // no need for truncating dots
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if string_end_position < line_to_render.chars().count() {
|
||||||
|
string_end_position += 1;
|
||||||
|
if string_end_position == line_to_render.chars().count() {
|
||||||
|
width_remaining += 2; // no need for truncating dots
|
||||||
|
}
|
||||||
|
} else if string_start_position > 0 {
|
||||||
|
string_start_position -= 1;
|
||||||
|
if string_start_position == 0 {
|
||||||
|
width_remaining += 2; // no need for truncating dots
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
i += 1;
|
||||||
|
}
|
||||||
|
Some((string_start_position, string_end_position))
|
||||||
|
} else if !indices.is_empty() {
|
||||||
|
// no room for all indices, remove the last one and try again
|
||||||
|
let mut new_indices = indices.clone();
|
||||||
|
new_indices.pop();
|
||||||
|
self.truncate_line_with_indices(line_to_render, &new_indices, max_width)
|
||||||
|
} else {
|
||||||
|
Some((first_index, last_index))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
241
default-plugins/strider/src/search/search_state.rs
Normal file
241
default-plugins/strider/src/search/search_state.rs
Normal file
|
|
@ -0,0 +1,241 @@
|
||||||
|
use crate::search::search_results::SearchResult;
|
||||||
|
use crate::search::{MessageToSearch, ResultsOfSearch};
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use std::path::PathBuf;
|
||||||
|
use zellij_tile::prelude::{
|
||||||
|
hide_self, open_file, open_file_floating, open_file_with_line, open_file_with_line_floating,
|
||||||
|
open_terminal, open_terminal_floating, post_message_to, Key,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub const CURRENT_SEARCH_TERM: &str = "/data/current_search_term";
|
||||||
|
|
||||||
|
#[derive(Default)]
|
||||||
|
pub struct SearchState {
|
||||||
|
pub search_term: String,
|
||||||
|
pub file_name_search_results: Vec<SearchResult>,
|
||||||
|
pub file_contents_search_results: Vec<SearchResult>,
|
||||||
|
pub loading: bool,
|
||||||
|
pub loading_animation_offset: u8,
|
||||||
|
pub selected_search_result: usize,
|
||||||
|
pub should_open_floating: bool,
|
||||||
|
pub search_filter: SearchType,
|
||||||
|
pub display_rows: usize,
|
||||||
|
pub display_columns: usize,
|
||||||
|
pub displayed_search_results: (usize, Vec<SearchResult>), // usize is selected index
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SearchState {
|
||||||
|
pub fn handle_key(&mut self, key: Key) {
|
||||||
|
match key {
|
||||||
|
Key::Down => self.move_search_selection_down(),
|
||||||
|
Key::Up => self.move_search_selection_up(),
|
||||||
|
Key::Char('\n') => self.open_search_result_in_editor(),
|
||||||
|
Key::BackTab => self.open_search_result_in_terminal(),
|
||||||
|
Key::Ctrl('f') => {
|
||||||
|
self.should_open_floating = !self.should_open_floating;
|
||||||
|
},
|
||||||
|
Key::Ctrl('r') => self.toggle_search_filter(),
|
||||||
|
Key::Esc => {
|
||||||
|
hide_self();
|
||||||
|
self.clear_state();
|
||||||
|
},
|
||||||
|
_ => self.append_to_search_term(key),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pub fn update_file_name_search_results(&mut self, mut results_of_search: ResultsOfSearch) {
|
||||||
|
if self.search_term == results_of_search.search_term {
|
||||||
|
self.file_name_search_results = results_of_search.search_results.drain(..).collect();
|
||||||
|
self.update_displayed_search_results();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pub fn update_file_contents_search_results(&mut self, mut results_of_search: ResultsOfSearch) {
|
||||||
|
if self.search_term == results_of_search.search_term {
|
||||||
|
self.file_contents_search_results =
|
||||||
|
results_of_search.search_results.drain(..).collect();
|
||||||
|
self.update_displayed_search_results();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pub fn change_size(&mut self, rows: usize, cols: usize) {
|
||||||
|
self.display_rows = rows;
|
||||||
|
self.display_columns = cols;
|
||||||
|
}
|
||||||
|
pub fn progress_animation(&mut self) {
|
||||||
|
if self.loading_animation_offset == u8::MAX {
|
||||||
|
self.loading_animation_offset = 0;
|
||||||
|
} else {
|
||||||
|
self.loading_animation_offset = self.loading_animation_offset.saturating_add(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pub fn number_of_lines_in_displayed_search_results(&self) -> usize {
|
||||||
|
self.displayed_search_results
|
||||||
|
.1
|
||||||
|
.iter()
|
||||||
|
.map(|l| l.rendered_height())
|
||||||
|
.sum()
|
||||||
|
}
|
||||||
|
fn move_search_selection_down(&mut self) {
|
||||||
|
if self.displayed_search_results.0 < self.max_search_selection_index() {
|
||||||
|
self.displayed_search_results.0 += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fn move_search_selection_up(&mut self) {
|
||||||
|
self.displayed_search_results.0 = self.displayed_search_results.0.saturating_sub(1);
|
||||||
|
}
|
||||||
|
fn open_search_result_in_editor(&mut self) {
|
||||||
|
match self.selected_search_result_entry() {
|
||||||
|
Some(SearchResult::File { path, .. }) => {
|
||||||
|
if self.should_open_floating {
|
||||||
|
open_file_floating(&PathBuf::from(path))
|
||||||
|
} else {
|
||||||
|
open_file(&PathBuf::from(path));
|
||||||
|
}
|
||||||
|
},
|
||||||
|
Some(SearchResult::LineInFile {
|
||||||
|
path, line_number, ..
|
||||||
|
}) => {
|
||||||
|
if self.should_open_floating {
|
||||||
|
open_file_with_line_floating(&PathBuf::from(path), line_number);
|
||||||
|
} else {
|
||||||
|
open_file_with_line(&PathBuf::from(path), line_number);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
None => eprintln!("Search results not found"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fn open_search_result_in_terminal(&mut self) {
|
||||||
|
let dir_path_of_result = |path: &str| -> PathBuf {
|
||||||
|
let file_path = PathBuf::from(path);
|
||||||
|
let mut dir_path = file_path.components();
|
||||||
|
dir_path.next_back(); // remove file name to stay with just the folder
|
||||||
|
dir_path.as_path().into()
|
||||||
|
};
|
||||||
|
let selected_search_result_entry = self.selected_search_result_entry();
|
||||||
|
if let Some(SearchResult::File { path, .. }) | Some(SearchResult::LineInFile { path, .. }) =
|
||||||
|
selected_search_result_entry
|
||||||
|
{
|
||||||
|
let dir_path = dir_path_of_result(&path);
|
||||||
|
if self.should_open_floating {
|
||||||
|
open_terminal_floating(&dir_path);
|
||||||
|
} else {
|
||||||
|
open_terminal(&dir_path);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fn toggle_search_filter(&mut self) {
|
||||||
|
self.search_filter.progress();
|
||||||
|
self.send_search_query();
|
||||||
|
}
|
||||||
|
fn clear_state(&mut self) {
|
||||||
|
self.file_name_search_results.clear();
|
||||||
|
self.file_contents_search_results.clear();
|
||||||
|
self.displayed_search_results = (0, vec![]);
|
||||||
|
self.search_term.clear();
|
||||||
|
}
|
||||||
|
fn append_to_search_term(&mut self, key: Key) {
|
||||||
|
match key {
|
||||||
|
Key::Char(character) => {
|
||||||
|
self.search_term.push(character);
|
||||||
|
},
|
||||||
|
Key::Backspace => {
|
||||||
|
self.search_term.pop();
|
||||||
|
if self.search_term.len() == 0 {
|
||||||
|
self.clear_state();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
_ => {},
|
||||||
|
}
|
||||||
|
self.send_search_query();
|
||||||
|
}
|
||||||
|
fn send_search_query(&mut self) {
|
||||||
|
match std::fs::write(CURRENT_SEARCH_TERM, &self.search_term) {
|
||||||
|
Ok(_) => {
|
||||||
|
if !self.search_term.is_empty() {
|
||||||
|
post_message_to(
|
||||||
|
"file_name_search",
|
||||||
|
serde_json::to_string(&MessageToSearch::Search).unwrap(),
|
||||||
|
"".to_owned(),
|
||||||
|
);
|
||||||
|
post_message_to(
|
||||||
|
"file_contents_search",
|
||||||
|
serde_json::to_string(&MessageToSearch::Search).unwrap(),
|
||||||
|
"".to_owned(),
|
||||||
|
);
|
||||||
|
self.file_name_search_results.clear();
|
||||||
|
self.file_contents_search_results.clear();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
Err(e) => eprintln!("Failed to write search term to HD, aborting search: {}", e),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fn max_search_selection_index(&self) -> usize {
|
||||||
|
self.displayed_search_results.1.len().saturating_sub(1)
|
||||||
|
}
|
||||||
|
fn update_displayed_search_results(&mut self) {
|
||||||
|
if self.search_term.is_empty() {
|
||||||
|
self.clear_state();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let mut search_results_of_interest = match self.search_filter {
|
||||||
|
SearchType::NamesAndContents => {
|
||||||
|
let mut all_search_results = self.file_name_search_results.clone();
|
||||||
|
all_search_results.append(&mut self.file_contents_search_results.clone());
|
||||||
|
all_search_results.sort_by(|a, b| b.score().cmp(&a.score()));
|
||||||
|
all_search_results
|
||||||
|
},
|
||||||
|
SearchType::Names => self.file_name_search_results.clone(),
|
||||||
|
SearchType::Contents => self.file_contents_search_results.clone(),
|
||||||
|
};
|
||||||
|
let mut height_taken_up_by_results = 0;
|
||||||
|
let mut displayed_search_results = vec![];
|
||||||
|
for search_result in search_results_of_interest.drain(..) {
|
||||||
|
if height_taken_up_by_results + search_result.rendered_height()
|
||||||
|
> self.rows_for_results()
|
||||||
|
{
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
height_taken_up_by_results += search_result.rendered_height();
|
||||||
|
displayed_search_results.push(search_result);
|
||||||
|
}
|
||||||
|
let new_index = self
|
||||||
|
.selected_search_result_entry()
|
||||||
|
.and_then(|currently_selected_search_result| {
|
||||||
|
displayed_search_results
|
||||||
|
.iter()
|
||||||
|
.position(|r| r.is_same_entry(¤tly_selected_search_result))
|
||||||
|
})
|
||||||
|
.unwrap_or(0);
|
||||||
|
self.displayed_search_results = (new_index, displayed_search_results);
|
||||||
|
}
|
||||||
|
fn selected_search_result_entry(&self) -> Option<SearchResult> {
|
||||||
|
self.displayed_search_results
|
||||||
|
.1
|
||||||
|
.get(self.displayed_search_results.0)
|
||||||
|
.cloned()
|
||||||
|
}
|
||||||
|
pub fn rows_for_results(&self) -> usize {
|
||||||
|
self.display_rows.saturating_sub(3) // search line and 2 controls lines
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize)]
|
||||||
|
pub enum SearchType {
|
||||||
|
NamesAndContents,
|
||||||
|
Names,
|
||||||
|
Contents,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SearchType {
|
||||||
|
pub fn progress(&mut self) {
|
||||||
|
match &self {
|
||||||
|
&SearchType::NamesAndContents => *self = SearchType::Names,
|
||||||
|
&SearchType::Names => *self = SearchType::Contents,
|
||||||
|
&SearchType::Contents => *self = SearchType::NamesAndContents,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for SearchType {
|
||||||
|
fn default() -> Self {
|
||||||
|
SearchType::NamesAndContents
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,61 @@
|
||||||
|
use crate::search::ui::{bold, styled_text_foreground, ORANGE};
|
||||||
|
|
||||||
|
pub struct SelectionControlsArea {
|
||||||
|
display_lines: usize,
|
||||||
|
display_columns: usize,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SelectionControlsArea {
|
||||||
|
pub fn new(display_lines: usize, display_columns: usize) -> Self {
|
||||||
|
SelectionControlsArea {
|
||||||
|
display_lines,
|
||||||
|
display_columns,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pub fn render(&self, result_count: usize) -> String {
|
||||||
|
let mut to_render = String::new();
|
||||||
|
let padding = self.display_lines.saturating_sub(result_count);
|
||||||
|
for _ in 0..padding {
|
||||||
|
to_render.push_str(&self.render_padding_line());
|
||||||
|
}
|
||||||
|
let selection_controls = self.render_selection_controls();
|
||||||
|
to_render.push_str(&selection_controls);
|
||||||
|
to_render
|
||||||
|
}
|
||||||
|
pub fn render_empty_lines(&self) -> String {
|
||||||
|
let mut to_render = String::new();
|
||||||
|
for _ in 0..self.display_lines {
|
||||||
|
to_render.push_str("\n");
|
||||||
|
}
|
||||||
|
to_render
|
||||||
|
}
|
||||||
|
fn render_padding_line(&self) -> String {
|
||||||
|
format!("│\n")
|
||||||
|
}
|
||||||
|
fn render_selection_controls(&self) -> String {
|
||||||
|
if self.display_columns >= self.full_selection_controls_len() {
|
||||||
|
self.render_full_selection_controls()
|
||||||
|
} else {
|
||||||
|
self.render_truncated_selection_controls()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fn full_selection_controls_len(&self) -> usize {
|
||||||
|
62
|
||||||
|
}
|
||||||
|
fn render_full_selection_controls(&self) -> String {
|
||||||
|
let arrow_tail = "└ ";
|
||||||
|
let enter = styled_text_foreground(ORANGE, &bold("<ENTER>"));
|
||||||
|
let enter_tip = bold(" - open in editor. ");
|
||||||
|
let tab = styled_text_foreground(ORANGE, &bold("<TAB>"));
|
||||||
|
let tab_tip = bold(" - open terminal at location.");
|
||||||
|
format!("{}{}{}{}{}", arrow_tail, enter, enter_tip, tab, tab_tip)
|
||||||
|
}
|
||||||
|
fn render_truncated_selection_controls(&self) -> String {
|
||||||
|
let arrow_tail = "└ ";
|
||||||
|
let enter = styled_text_foreground(ORANGE, &bold("<ENTER>"));
|
||||||
|
let enter_tip = bold(" - edit. ");
|
||||||
|
let tab = styled_text_foreground(ORANGE, &bold("<TAB>"));
|
||||||
|
let tab_tip = bold(" - terminal.");
|
||||||
|
format!("{}{}{}{}{}", arrow_tail, enter, enter_tip, tab, tab_tip)
|
||||||
|
}
|
||||||
|
}
|
||||||
120
default-plugins/strider/src/search/ui.rs
Normal file
120
default-plugins/strider/src/search/ui.rs
Normal file
|
|
@ -0,0 +1,120 @@
|
||||||
|
use crate::search::controls_line::{Control, ControlsLine};
|
||||||
|
use crate::search::search_state::SearchState;
|
||||||
|
use crate::search::selection_controls_area::SelectionControlsArea;
|
||||||
|
use std::fmt::{Display, Formatter, Result};
|
||||||
|
|
||||||
|
pub const CYAN: u8 = 51;
|
||||||
|
pub const GRAY_LIGHT: u8 = 238;
|
||||||
|
pub const GRAY_DARK: u8 = 245;
|
||||||
|
pub const WHITE: u8 = 15;
|
||||||
|
pub const BLACK: u8 = 16;
|
||||||
|
pub const RED: u8 = 124;
|
||||||
|
pub const GREEN: u8 = 154;
|
||||||
|
pub const ORANGE: u8 = 166;
|
||||||
|
|
||||||
|
impl Display for SearchState {
|
||||||
|
fn fmt(&self, f: &mut Formatter) -> Result {
|
||||||
|
write!(f, "{}", self.render_search_line())?;
|
||||||
|
write!(f, "{}", self.render_search_results())?;
|
||||||
|
write!(f, "{}", self.render_selection_control_area())?;
|
||||||
|
write!(f, "{}", self.render_controls_line())?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SearchState {
|
||||||
|
pub fn render_search_line(&self) -> String {
|
||||||
|
format!(
|
||||||
|
"{}{}\n",
|
||||||
|
styled_text_foreground(CYAN, &bold("SEARCH: ")),
|
||||||
|
self.search_term
|
||||||
|
)
|
||||||
|
}
|
||||||
|
pub fn render_search_results(&self) -> String {
|
||||||
|
let mut space_for_results = self.display_rows.saturating_sub(3); // title and both controls lines
|
||||||
|
let mut to_render = String::new();
|
||||||
|
for (i, search_result) in self.displayed_search_results.1.iter().enumerate() {
|
||||||
|
let result_height = search_result.rendered_height();
|
||||||
|
if space_for_results < result_height {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
space_for_results -= result_height;
|
||||||
|
let index_of_selected_result = self.displayed_search_results.0;
|
||||||
|
let is_selected = i == index_of_selected_result;
|
||||||
|
let is_below_search_result = i > index_of_selected_result;
|
||||||
|
let rendered_result =
|
||||||
|
search_result.render(self.display_columns, is_selected, is_below_search_result);
|
||||||
|
to_render.push_str(&format!("{}", rendered_result));
|
||||||
|
to_render.push('\n')
|
||||||
|
}
|
||||||
|
to_render
|
||||||
|
}
|
||||||
|
pub fn render_selection_control_area(&self) -> String {
|
||||||
|
let rows_for_results = self.rows_for_results();
|
||||||
|
if !self.displayed_search_results.1.is_empty() {
|
||||||
|
format!(
|
||||||
|
"{}\n",
|
||||||
|
SelectionControlsArea::new(rows_for_results, self.display_columns)
|
||||||
|
.render(self.number_of_lines_in_displayed_search_results())
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
format!(
|
||||||
|
"{}\n",
|
||||||
|
SelectionControlsArea::new(rows_for_results, self.display_columns)
|
||||||
|
.render_empty_lines()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pub fn render_controls_line(&self) -> String {
|
||||||
|
let has_results = !self.displayed_search_results.1.is_empty();
|
||||||
|
let tiled_floating_control =
|
||||||
|
Control::new_floating_control("Ctrl f", self.should_open_floating);
|
||||||
|
let names_contents_control = Control::new_filter_control("Ctrl r", &self.search_filter);
|
||||||
|
if self.loading {
|
||||||
|
ControlsLine::new(
|
||||||
|
vec![tiled_floating_control, names_contents_control],
|
||||||
|
Some(vec!["Scanning folder", "Scanning", "S"]),
|
||||||
|
)
|
||||||
|
.with_animation_offset(self.loading_animation_offset)
|
||||||
|
.render(self.display_columns, has_results)
|
||||||
|
} else {
|
||||||
|
ControlsLine::new(vec![tiled_floating_control, names_contents_control], None)
|
||||||
|
.render(self.display_columns, has_results)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn bold(text: &str) -> String {
|
||||||
|
format!("\u{1b}[1m{}\u{1b}[m", text)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn underline(text: &str) -> String {
|
||||||
|
format!("\u{1b}[4m{}\u{1b}[m", text)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn styled_text(foreground_color: u8, background_color: u8, text: &str) -> String {
|
||||||
|
format!(
|
||||||
|
"\u{1b}[38;5;{};48;5;{}m{}\u{1b}[m",
|
||||||
|
foreground_color, background_color, text
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn styled_text_foreground(foreground_color: u8, text: &str) -> String {
|
||||||
|
format!("\u{1b}[38;5;{}m{}\u{1b}[m", foreground_color, text)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn styled_text_background(background_color: u8, text: &str) -> String {
|
||||||
|
format!("\u{1b}[48;5;{}m{}\u{1b}[m", background_color, text)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn color_line_to_end(background_color: u8) -> String {
|
||||||
|
format!("\u{1b}[48;5;{}m\u{1b}[0K", background_color)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn arrow(foreground: u8, background: u8) -> String {
|
||||||
|
format!("\u{1b}[38;5;{}m\u{1b}[48;5;{}m", foreground, background)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn dot(foreground: u8, background: u8) -> String {
|
||||||
|
format!("\u{1b}[38;5;{};48;5;{}m•", foreground, background)
|
||||||
|
}
|
||||||
|
|
@ -1,4 +1,6 @@
|
||||||
use crate::search::SearchResult;
|
use crate::search::search_results::SearchResult;
|
||||||
|
use crate::search::search_state::SearchState;
|
||||||
|
use crate::search::search_state::SearchType;
|
||||||
use pretty_bytes::converter as pb;
|
use pretty_bytes::converter as pb;
|
||||||
use std::{
|
use std::{
|
||||||
collections::{HashMap, VecDeque},
|
collections::{HashMap, VecDeque},
|
||||||
|
|
@ -18,65 +20,29 @@ pub struct State {
|
||||||
pub cursor_hist: HashMap<PathBuf, (usize, usize)>,
|
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 ev_history: VecDeque<(Event, Instant)>, // stores last event, can be expanded in future
|
||||||
|
pub search_state: SearchState,
|
||||||
pub search_paths: Vec<String>,
|
pub search_paths: Vec<String>,
|
||||||
pub search_term: Option<String>,
|
pub search_term: Option<String>,
|
||||||
pub search_results: Vec<SearchResult>,
|
pub file_name_search_results: Vec<SearchResult>,
|
||||||
|
pub file_contents_search_results: Vec<SearchResult>,
|
||||||
pub loading: bool,
|
pub loading: bool,
|
||||||
pub loading_animation_offset: u8,
|
pub loading_animation_offset: u8,
|
||||||
pub typing_search_term: bool,
|
pub typing_search_term: bool,
|
||||||
pub exploring_search_results: bool,
|
|
||||||
pub selected_search_result: usize,
|
pub selected_search_result: usize,
|
||||||
|
pub processed_search_index: usize,
|
||||||
|
pub should_open_floating: bool,
|
||||||
|
pub search_filter: SearchType,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl State {
|
impl State {
|
||||||
pub fn append_to_search_term(&mut self, key: Key) {
|
|
||||||
match key {
|
|
||||||
Key::Char(character) => {
|
|
||||||
if let Some(search_term) = self.search_term.as_mut() {
|
|
||||||
search_term.push(character);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
Key::Backspace => {
|
|
||||||
if let Some(search_term) = self.search_term.as_mut() {
|
|
||||||
search_term.pop();
|
|
||||||
if search_term.len() == 0 {
|
|
||||||
self.search_term = None;
|
|
||||||
self.typing_search_term = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
_ => {},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
pub fn accept_search_term(&mut self) {
|
|
||||||
self.typing_search_term = false;
|
|
||||||
self.exploring_search_results = true;
|
|
||||||
}
|
|
||||||
pub fn typing_search_term(&self) -> bool {
|
pub fn typing_search_term(&self) -> bool {
|
||||||
self.typing_search_term
|
self.typing_search_term
|
||||||
}
|
}
|
||||||
pub fn exploring_search_results(&self) -> bool {
|
|
||||||
self.exploring_search_results
|
|
||||||
}
|
|
||||||
pub fn stop_exploring_search_results(&mut self) {
|
|
||||||
self.exploring_search_results = false;
|
|
||||||
}
|
|
||||||
pub fn start_typing_search_term(&mut self) {
|
pub fn start_typing_search_term(&mut self) {
|
||||||
if self.search_term.is_none() {
|
|
||||||
self.search_term = Some(String::new());
|
|
||||||
}
|
|
||||||
self.typing_search_term = true;
|
self.typing_search_term = true;
|
||||||
}
|
}
|
||||||
pub fn stop_typing_search_term(&mut self) {
|
pub fn stop_typing_search_term(&mut self) {
|
||||||
self.typing_search_term = true;
|
self.typing_search_term = false;
|
||||||
}
|
|
||||||
pub fn move_search_selection_up(&mut self) {
|
|
||||||
self.selected_search_result = self.selected_search_result.saturating_sub(1);
|
|
||||||
}
|
|
||||||
pub fn move_search_selection_down(&mut self) {
|
|
||||||
if self.selected_search_result < self.search_results.len() {
|
|
||||||
self.selected_search_result = self.selected_search_result.saturating_add(1);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
pub fn selected_mut(&mut self) -> &mut usize {
|
pub fn selected_mut(&mut self) -> &mut usize {
|
||||||
&mut self.cursor_hist.entry(self.path.clone()).or_default().0
|
&mut self.cursor_hist.entry(self.path.clone()).or_default().0
|
||||||
|
|
@ -104,32 +70,6 @@ impl State {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
pub fn open_search_result(&mut self) {
|
|
||||||
match self.search_results.get(self.selected_search_result) {
|
|
||||||
Some(SearchResult::File {
|
|
||||||
path,
|
|
||||||
score,
|
|
||||||
indices,
|
|
||||||
}) => {
|
|
||||||
let file_path = PathBuf::from(path);
|
|
||||||
open_file(file_path.strip_prefix(ROOT).unwrap());
|
|
||||||
},
|
|
||||||
Some(SearchResult::LineInFile {
|
|
||||||
path,
|
|
||||||
score,
|
|
||||||
indices,
|
|
||||||
line,
|
|
||||||
line_number,
|
|
||||||
}) => {
|
|
||||||
let file_path = PathBuf::from(path);
|
|
||||||
open_file_with_line(file_path.strip_prefix(ROOT).unwrap(), *line_number);
|
|
||||||
// open_file_with_line(&file_path, *line_number); // TODO: no!!
|
|
||||||
},
|
|
||||||
None => {
|
|
||||||
eprintln!("Search result not found");
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(PartialEq, Eq, PartialOrd, Ord, Clone)]
|
#[derive(PartialEq, Eq, PartialOrd, Ord, Clone)]
|
||||||
|
|
|
||||||
|
|
@ -32,7 +32,7 @@ use wasmer::Store;
|
||||||
use crate::{
|
use crate::{
|
||||||
os_input_output::ServerOsApi,
|
os_input_output::ServerOsApi,
|
||||||
plugins::{plugin_thread_main, PluginInstruction},
|
plugins::{plugin_thread_main, PluginInstruction},
|
||||||
pty::{pty_thread_main, Pty, PtyInstruction},
|
pty::{get_default_shell, pty_thread_main, Pty, PtyInstruction},
|
||||||
screen::{screen_thread_main, ScreenInstruction},
|
screen::{screen_thread_main, ScreenInstruction},
|
||||||
thread_bus::{Bus, ThreadSenders},
|
thread_bus::{Bus, ThreadSenders},
|
||||||
};
|
};
|
||||||
|
|
@ -705,6 +705,10 @@ fn init_session(
|
||||||
..Default::default()
|
..Default::default()
|
||||||
})
|
})
|
||||||
});
|
});
|
||||||
|
let path_to_default_shell = config_options
|
||||||
|
.default_shell
|
||||||
|
.clone()
|
||||||
|
.unwrap_or_else(|| get_default_shell());
|
||||||
|
|
||||||
let pty_thread = thread::Builder::new()
|
let pty_thread = thread::Builder::new()
|
||||||
.name("pty".to_string())
|
.name("pty".to_string())
|
||||||
|
|
@ -757,6 +761,7 @@ fn init_session(
|
||||||
})
|
})
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
|
let zellij_cwd = std::env::current_dir().unwrap_or_else(|_| std::path::PathBuf::from("."));
|
||||||
let plugin_thread = thread::Builder::new()
|
let plugin_thread = thread::Builder::new()
|
||||||
.name("wasm".to_string())
|
.name("wasm".to_string())
|
||||||
.spawn({
|
.spawn({
|
||||||
|
|
@ -780,6 +785,8 @@ fn init_session(
|
||||||
data_dir,
|
data_dir,
|
||||||
plugins.unwrap_or_default(),
|
plugins.unwrap_or_default(),
|
||||||
layout,
|
layout,
|
||||||
|
path_to_default_shell,
|
||||||
|
zellij_cwd,
|
||||||
)
|
)
|
||||||
.fatal()
|
.fatal()
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -25,7 +25,7 @@ use zellij_utils::{
|
||||||
data::{ModeInfo, Style},
|
data::{ModeInfo, Style},
|
||||||
errors::prelude::*,
|
errors::prelude::*,
|
||||||
input::command::RunCommand,
|
input::command::RunCommand,
|
||||||
input::layout::FloatingPaneLayout,
|
input::layout::{FloatingPaneLayout, Run, RunPlugin},
|
||||||
pane_size::{Dimension, Offset, PaneGeom, Size, SizeInPixels, Viewport},
|
pane_size::{Dimension, Offset, PaneGeom, Size, SizeInPixels, Viewport},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -870,4 +870,19 @@ impl FloatingPanes {
|
||||||
self.focus_pane_for_all_clients(active_pane_id);
|
self.focus_pane_for_all_clients(active_pane_id);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
pub fn get_plugin_pane_id(&self, run_plugin: &RunPlugin) -> Option<PaneId> {
|
||||||
|
let run = Some(Run::Plugin(run_plugin.clone()));
|
||||||
|
self.panes
|
||||||
|
.iter()
|
||||||
|
.find(|(_id, s_p)| s_p.invoked_with() == &run)
|
||||||
|
.map(|(id, _)| *id)
|
||||||
|
}
|
||||||
|
pub fn focus_pane_if_exists(&mut self, pane_id: PaneId, client_id: ClientId) -> Result<()> {
|
||||||
|
if self.panes.get(&pane_id).is_some() {
|
||||||
|
self.focus_pane(pane_id, client_id);
|
||||||
|
Ok(())
|
||||||
|
} else {
|
||||||
|
Err(anyhow!("Pane not found"))
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -537,7 +537,7 @@ impl Pane for PluginPane {
|
||||||
self.pane_title = title;
|
self.pane_title = title;
|
||||||
}
|
}
|
||||||
fn update_loading_indication(&mut self, loading_indication: LoadingIndication) {
|
fn update_loading_indication(&mut self, loading_indication: LoadingIndication) {
|
||||||
if self.loading_indication.ended {
|
if self.loading_indication.ended && !loading_indication.is_error() {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
self.loading_indication.merge(loading_indication);
|
self.loading_indication.merge(loading_indication);
|
||||||
|
|
|
||||||
|
|
@ -20,7 +20,10 @@ use stacked_panes::StackedPanes;
|
||||||
use zellij_utils::{
|
use zellij_utils::{
|
||||||
data::{Direction, ModeInfo, ResizeStrategy, Style},
|
data::{Direction, ModeInfo, ResizeStrategy, Style},
|
||||||
errors::prelude::*,
|
errors::prelude::*,
|
||||||
input::{command::RunCommand, layout::SplitDirection},
|
input::{
|
||||||
|
command::RunCommand,
|
||||||
|
layout::{Run, RunPlugin, SplitDirection},
|
||||||
|
},
|
||||||
pane_size::{Offset, PaneGeom, Size, SizeInPixels, Viewport},
|
pane_size::{Offset, PaneGeom, Size, SizeInPixels, Viewport},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -529,6 +532,14 @@ impl TiledPanes {
|
||||||
}
|
}
|
||||||
self.reset_boundaries();
|
self.reset_boundaries();
|
||||||
}
|
}
|
||||||
|
pub fn focus_pane_if_exists(&mut self, pane_id: PaneId, client_id: ClientId) -> Result<()> {
|
||||||
|
if self.panes.get(&pane_id).is_some() {
|
||||||
|
self.focus_pane(pane_id, client_id);
|
||||||
|
Ok(())
|
||||||
|
} else {
|
||||||
|
Err(anyhow!("Pane not found"))
|
||||||
|
}
|
||||||
|
}
|
||||||
pub fn focus_pane_at_position(&mut self, position_and_size: PaneGeom, client_id: ClientId) {
|
pub fn focus_pane_at_position(&mut self, position_and_size: PaneGeom, client_id: ClientId) {
|
||||||
if let Some(pane_id) = self
|
if let Some(pane_id) = self
|
||||||
.panes
|
.panes
|
||||||
|
|
@ -1691,6 +1702,13 @@ impl TiledPanes {
|
||||||
fn reset_boundaries(&mut self) {
|
fn reset_boundaries(&mut self) {
|
||||||
self.client_id_to_boundaries.clear();
|
self.client_id_to_boundaries.clear();
|
||||||
}
|
}
|
||||||
|
pub fn get_plugin_pane_id(&self, run_plugin: &RunPlugin) -> Option<PaneId> {
|
||||||
|
let run = Some(Run::Plugin(run_plugin.clone()));
|
||||||
|
self.panes
|
||||||
|
.iter()
|
||||||
|
.find(|(_id, s_p)| s_p.invoked_with() == &run)
|
||||||
|
.map(|(id, _)| *id)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[allow(clippy::borrowed_box)]
|
#[allow(clippy::borrowed_box)]
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,8 @@
|
||||||
mod plugin_loader;
|
mod plugin_loader;
|
||||||
mod plugin_map;
|
mod plugin_map;
|
||||||
|
mod plugin_worker;
|
||||||
mod wasm_bridge;
|
mod wasm_bridge;
|
||||||
|
mod watch_filesystem;
|
||||||
mod zellij_exports;
|
mod zellij_exports;
|
||||||
use log::info;
|
use log::info;
|
||||||
use std::{collections::HashMap, fs, path::PathBuf};
|
use std::{collections::HashMap, fs, path::PathBuf};
|
||||||
|
|
@ -104,13 +106,22 @@ pub(crate) fn plugin_thread_main(
|
||||||
data_dir: PathBuf,
|
data_dir: PathBuf,
|
||||||
plugins: PluginsConfig,
|
plugins: PluginsConfig,
|
||||||
layout: Box<Layout>,
|
layout: Box<Layout>,
|
||||||
|
path_to_default_shell: PathBuf,
|
||||||
|
zellij_cwd: PathBuf,
|
||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
info!("Wasm main thread starts");
|
info!("Wasm main thread starts");
|
||||||
|
|
||||||
let plugin_dir = data_dir.join("plugins/");
|
let plugin_dir = data_dir.join("plugins/");
|
||||||
let plugin_global_data_dir = plugin_dir.join("data");
|
let plugin_global_data_dir = plugin_dir.join("data");
|
||||||
|
|
||||||
let mut wasm_bridge = WasmBridge::new(plugins, bus.senders.clone(), store, plugin_dir);
|
let mut wasm_bridge = WasmBridge::new(
|
||||||
|
plugins,
|
||||||
|
bus.senders.clone(),
|
||||||
|
store,
|
||||||
|
plugin_dir,
|
||||||
|
path_to_default_shell,
|
||||||
|
zellij_cwd,
|
||||||
|
);
|
||||||
|
|
||||||
loop {
|
loop {
|
||||||
let (event, mut err_ctx) = bus.recv().expect("failed to receive event on channel");
|
let (event, mut err_ctx) = bus.recv().expect("failed to receive event on channel");
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,5 @@
|
||||||
use crate::plugins::plugin_map::{
|
use crate::plugins::plugin_map::{PluginEnv, PluginMap, RunningPlugin, Subscriptions};
|
||||||
PluginEnv, PluginMap, RunningPlugin, RunningWorker, Subscriptions,
|
use crate::plugins::plugin_worker::{plugin_worker, RunningWorker};
|
||||||
};
|
|
||||||
use crate::plugins::zellij_exports::{wasi_read_string, zellij_exports};
|
use crate::plugins::zellij_exports::{wasi_read_string, zellij_exports};
|
||||||
use crate::plugins::PluginId;
|
use crate::plugins::PluginId;
|
||||||
use highway::{HighwayHash, PortableHash};
|
use highway::{HighwayHash, PortableHash};
|
||||||
|
|
@ -164,6 +163,8 @@ pub struct PluginLoader<'a> {
|
||||||
plugin_own_data_dir: PathBuf,
|
plugin_own_data_dir: PathBuf,
|
||||||
size: Size,
|
size: Size,
|
||||||
wasm_blob_on_hd: Option<(Vec<u8>, PathBuf)>,
|
wasm_blob_on_hd: Option<(Vec<u8>, PathBuf)>,
|
||||||
|
path_to_default_shell: PathBuf,
|
||||||
|
zellij_cwd: PathBuf,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'a> PluginLoader<'a> {
|
impl<'a> PluginLoader<'a> {
|
||||||
|
|
@ -176,6 +177,8 @@ impl<'a> PluginLoader<'a> {
|
||||||
plugin_map: Arc<Mutex<PluginMap>>,
|
plugin_map: Arc<Mutex<PluginMap>>,
|
||||||
connected_clients: Arc<Mutex<Vec<ClientId>>>,
|
connected_clients: Arc<Mutex<Vec<ClientId>>>,
|
||||||
loading_indication: &mut LoadingIndication,
|
loading_indication: &mut LoadingIndication,
|
||||||
|
path_to_default_shell: PathBuf,
|
||||||
|
zellij_cwd: PathBuf,
|
||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
let err_context = || format!("failed to reload plugin {plugin_id} from memory");
|
let err_context = || format!("failed to reload plugin {plugin_id} from memory");
|
||||||
let mut connected_clients: Vec<ClientId> =
|
let mut connected_clients: Vec<ClientId> =
|
||||||
|
|
@ -194,6 +197,8 @@ impl<'a> PluginLoader<'a> {
|
||||||
first_client_id,
|
first_client_id,
|
||||||
&store,
|
&store,
|
||||||
&plugin_dir,
|
&plugin_dir,
|
||||||
|
path_to_default_shell,
|
||||||
|
zellij_cwd,
|
||||||
)?;
|
)?;
|
||||||
plugin_loader
|
plugin_loader
|
||||||
.load_module_from_memory()
|
.load_module_from_memory()
|
||||||
|
|
@ -227,6 +232,8 @@ impl<'a> PluginLoader<'a> {
|
||||||
size: Size,
|
size: Size,
|
||||||
connected_clients: Arc<Mutex<Vec<ClientId>>>,
|
connected_clients: Arc<Mutex<Vec<ClientId>>>,
|
||||||
loading_indication: &mut LoadingIndication,
|
loading_indication: &mut LoadingIndication,
|
||||||
|
path_to_default_shell: PathBuf,
|
||||||
|
zellij_cwd: PathBuf,
|
||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
let err_context = || format!("failed to start plugin {plugin_id} for client {client_id}");
|
let err_context = || format!("failed to start plugin {plugin_id} for client {client_id}");
|
||||||
let mut plugin_loader = PluginLoader::new(
|
let mut plugin_loader = PluginLoader::new(
|
||||||
|
|
@ -240,6 +247,8 @@ impl<'a> PluginLoader<'a> {
|
||||||
&plugin_dir,
|
&plugin_dir,
|
||||||
tab_index,
|
tab_index,
|
||||||
size,
|
size,
|
||||||
|
path_to_default_shell,
|
||||||
|
zellij_cwd,
|
||||||
)?;
|
)?;
|
||||||
plugin_loader
|
plugin_loader
|
||||||
.load_module_from_memory()
|
.load_module_from_memory()
|
||||||
|
|
@ -273,6 +282,8 @@ impl<'a> PluginLoader<'a> {
|
||||||
plugin_map: Arc<Mutex<PluginMap>>,
|
plugin_map: Arc<Mutex<PluginMap>>,
|
||||||
connected_clients: Arc<Mutex<Vec<ClientId>>>,
|
connected_clients: Arc<Mutex<Vec<ClientId>>>,
|
||||||
loading_indication: &mut LoadingIndication,
|
loading_indication: &mut LoadingIndication,
|
||||||
|
path_to_default_shell: PathBuf,
|
||||||
|
zellij_cwd: PathBuf,
|
||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
let mut new_plugins = HashSet::new();
|
let mut new_plugins = HashSet::new();
|
||||||
for plugin_id in plugin_map.lock().unwrap().plugin_ids() {
|
for plugin_id in plugin_map.lock().unwrap().plugin_ids() {
|
||||||
|
|
@ -288,6 +299,8 @@ impl<'a> PluginLoader<'a> {
|
||||||
existing_client_id,
|
existing_client_id,
|
||||||
&store,
|
&store,
|
||||||
&plugin_dir,
|
&plugin_dir,
|
||||||
|
path_to_default_shell.clone(),
|
||||||
|
zellij_cwd.clone(),
|
||||||
)?;
|
)?;
|
||||||
plugin_loader
|
plugin_loader
|
||||||
.load_module_from_memory()
|
.load_module_from_memory()
|
||||||
|
|
@ -314,6 +327,8 @@ impl<'a> PluginLoader<'a> {
|
||||||
plugin_map: Arc<Mutex<PluginMap>>,
|
plugin_map: Arc<Mutex<PluginMap>>,
|
||||||
connected_clients: Arc<Mutex<Vec<ClientId>>>,
|
connected_clients: Arc<Mutex<Vec<ClientId>>>,
|
||||||
loading_indication: &mut LoadingIndication,
|
loading_indication: &mut LoadingIndication,
|
||||||
|
path_to_default_shell: PathBuf,
|
||||||
|
zellij_cwd: PathBuf,
|
||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
let err_context = || format!("failed to reload plugin id {plugin_id}");
|
let err_context = || format!("failed to reload plugin id {plugin_id}");
|
||||||
|
|
||||||
|
|
@ -333,6 +348,8 @@ impl<'a> PluginLoader<'a> {
|
||||||
first_client_id,
|
first_client_id,
|
||||||
&store,
|
&store,
|
||||||
&plugin_dir,
|
&plugin_dir,
|
||||||
|
path_to_default_shell,
|
||||||
|
zellij_cwd,
|
||||||
)?;
|
)?;
|
||||||
plugin_loader
|
plugin_loader
|
||||||
.compile_module()
|
.compile_module()
|
||||||
|
|
@ -363,6 +380,8 @@ impl<'a> PluginLoader<'a> {
|
||||||
plugin_dir: &'a PathBuf,
|
plugin_dir: &'a PathBuf,
|
||||||
tab_index: usize,
|
tab_index: usize,
|
||||||
size: Size,
|
size: Size,
|
||||||
|
path_to_default_shell: PathBuf,
|
||||||
|
zellij_cwd: PathBuf,
|
||||||
) -> Result<Self> {
|
) -> Result<Self> {
|
||||||
let plugin_own_data_dir = ZELLIJ_SESSION_CACHE_DIR
|
let plugin_own_data_dir = ZELLIJ_SESSION_CACHE_DIR
|
||||||
.join(Url::from(&plugin.location).to_string())
|
.join(Url::from(&plugin.location).to_string())
|
||||||
|
|
@ -383,6 +402,8 @@ impl<'a> PluginLoader<'a> {
|
||||||
plugin_own_data_dir,
|
plugin_own_data_dir,
|
||||||
size,
|
size,
|
||||||
wasm_blob_on_hd: None,
|
wasm_blob_on_hd: None,
|
||||||
|
path_to_default_shell,
|
||||||
|
zellij_cwd,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
pub fn new_from_existing_plugin_attributes(
|
pub fn new_from_existing_plugin_attributes(
|
||||||
|
|
@ -394,6 +415,8 @@ impl<'a> PluginLoader<'a> {
|
||||||
client_id: ClientId,
|
client_id: ClientId,
|
||||||
store: &Store,
|
store: &Store,
|
||||||
plugin_dir: &'a PathBuf,
|
plugin_dir: &'a PathBuf,
|
||||||
|
path_to_default_shell: PathBuf,
|
||||||
|
zellij_cwd: PathBuf,
|
||||||
) -> Result<Self> {
|
) -> Result<Self> {
|
||||||
let err_context = || "Failed to find existing plugin";
|
let err_context = || "Failed to find existing plugin";
|
||||||
let (running_plugin, _subscriptions, _workers) = {
|
let (running_plugin, _subscriptions, _workers) = {
|
||||||
|
|
@ -421,6 +444,8 @@ impl<'a> PluginLoader<'a> {
|
||||||
plugin_dir,
|
plugin_dir,
|
||||||
tab_index,
|
tab_index,
|
||||||
size,
|
size,
|
||||||
|
path_to_default_shell,
|
||||||
|
zellij_cwd,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
pub fn new_from_different_client_id(
|
pub fn new_from_different_client_id(
|
||||||
|
|
@ -432,6 +457,8 @@ impl<'a> PluginLoader<'a> {
|
||||||
client_id: ClientId,
|
client_id: ClientId,
|
||||||
store: &Store,
|
store: &Store,
|
||||||
plugin_dir: &'a PathBuf,
|
plugin_dir: &'a PathBuf,
|
||||||
|
path_to_default_shell: PathBuf,
|
||||||
|
zellij_cwd: PathBuf,
|
||||||
) -> Result<Self> {
|
) -> Result<Self> {
|
||||||
let err_context = || "Failed to find existing plugin";
|
let err_context = || "Failed to find existing plugin";
|
||||||
let running_plugin = {
|
let running_plugin = {
|
||||||
|
|
@ -460,6 +487,8 @@ impl<'a> PluginLoader<'a> {
|
||||||
plugin_dir,
|
plugin_dir,
|
||||||
tab_index,
|
tab_index,
|
||||||
size,
|
size,
|
||||||
|
path_to_default_shell,
|
||||||
|
zellij_cwd,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
pub fn load_module_from_memory(&mut self) -> Result<Module> {
|
pub fn load_module_from_memory(&mut self) -> Result<Module> {
|
||||||
|
|
@ -625,7 +654,8 @@ impl<'a> PluginLoader<'a> {
|
||||||
|
|
||||||
let worker =
|
let worker =
|
||||||
RunningWorker::new(instance, &function_name, plugin_config, plugin_env);
|
RunningWorker::new(instance, &function_name, plugin_config, plugin_env);
|
||||||
workers.insert(function_name.into(), Arc::new(Mutex::new(worker)));
|
let worker_sender = plugin_worker(worker);
|
||||||
|
workers.insert(function_name.into(), worker_sender);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
start_function.call(&[]).with_context(err_context)?;
|
start_function.call(&[]).with_context(err_context)?;
|
||||||
|
|
@ -689,6 +719,8 @@ impl<'a> PluginLoader<'a> {
|
||||||
*client_id,
|
*client_id,
|
||||||
&self.store,
|
&self.store,
|
||||||
&self.plugin_dir,
|
&self.plugin_dir,
|
||||||
|
self.path_to_default_shell.clone(),
|
||||||
|
self.zellij_cwd.clone(),
|
||||||
)?;
|
)?;
|
||||||
plugin_loader_for_client
|
plugin_loader_for_client
|
||||||
.load_module_from_memory()
|
.load_module_from_memory()
|
||||||
|
|
@ -746,7 +778,7 @@ impl<'a> PluginLoader<'a> {
|
||||||
};
|
};
|
||||||
let mut wasi_env = WasiState::new("Zellij")
|
let mut wasi_env = WasiState::new("Zellij")
|
||||||
.env("CLICOLOR_FORCE", "1")
|
.env("CLICOLOR_FORCE", "1")
|
||||||
.map_dir("/host", ".")
|
.map_dir("/host", self.zellij_cwd.clone())
|
||||||
.and_then(|wasi| wasi.map_dir("/data", &self.plugin_own_data_dir))
|
.and_then(|wasi| wasi.map_dir("/data", &self.plugin_own_data_dir))
|
||||||
.and_then(|wasi| wasi.map_dir("/tmp", ZELLIJ_TMP_DIR.as_path()))
|
.and_then(|wasi| wasi.map_dir("/tmp", ZELLIJ_TMP_DIR.as_path()))
|
||||||
.and_then(|wasi| {
|
.and_then(|wasi| {
|
||||||
|
|
@ -771,6 +803,7 @@ impl<'a> PluginLoader<'a> {
|
||||||
wasi_env,
|
wasi_env,
|
||||||
plugin_own_data_dir: self.plugin_own_data_dir.clone(),
|
plugin_own_data_dir: self.plugin_own_data_dir.clone(),
|
||||||
tab_index: self.tab_index,
|
tab_index: self.tab_index,
|
||||||
|
path_to_default_shell: self.path_to_default_shell.clone(),
|
||||||
};
|
};
|
||||||
|
|
||||||
let subscriptions = Arc::new(Mutex::new(HashSet::new()));
|
let subscriptions = Arc::new(Mutex::new(HashSet::new()));
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,4 @@
|
||||||
use crate::plugins::plugin_loader::{PluginLoader, VersionMismatchError};
|
use crate::plugins::plugin_worker::MessageToWorker;
|
||||||
use crate::plugins::zellij_exports::wasi_write_object;
|
|
||||||
use crate::plugins::PluginId;
|
use crate::plugins::PluginId;
|
||||||
use std::{
|
use std::{
|
||||||
collections::{HashMap, HashSet},
|
collections::{HashMap, HashSet},
|
||||||
|
|
@ -11,10 +10,10 @@ use wasmer_wasi::WasiEnv;
|
||||||
|
|
||||||
use crate::{thread_bus::ThreadSenders, ClientId};
|
use crate::{thread_bus::ThreadSenders, ClientId};
|
||||||
|
|
||||||
|
use zellij_utils::async_channel::Sender;
|
||||||
use zellij_utils::errors::prelude::*;
|
use zellij_utils::errors::prelude::*;
|
||||||
use zellij_utils::{
|
use zellij_utils::{
|
||||||
consts::VERSION, data::EventType, input::layout::RunPluginLocation,
|
data::EventType, input::layout::RunPluginLocation, input::plugins::PluginConfig,
|
||||||
input::plugins::PluginConfig,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// the idea here is to provide atomicity when adding/removing plugins from the map (eg. when a new
|
// the idea here is to provide atomicity when adding/removing plugins from the map (eg. when a new
|
||||||
|
|
@ -29,7 +28,7 @@ pub struct PluginMap {
|
||||||
(
|
(
|
||||||
Arc<Mutex<RunningPlugin>>,
|
Arc<Mutex<RunningPlugin>>,
|
||||||
Arc<Mutex<Subscriptions>>,
|
Arc<Mutex<Subscriptions>>,
|
||||||
HashMap<String, Arc<Mutex<RunningWorker>>>,
|
HashMap<String, Sender<MessageToWorker>>,
|
||||||
),
|
),
|
||||||
>,
|
>,
|
||||||
}
|
}
|
||||||
|
|
@ -41,7 +40,7 @@ impl PluginMap {
|
||||||
) -> Vec<(
|
) -> Vec<(
|
||||||
Arc<Mutex<RunningPlugin>>,
|
Arc<Mutex<RunningPlugin>>,
|
||||||
Arc<Mutex<Subscriptions>>,
|
Arc<Mutex<Subscriptions>>,
|
||||||
HashMap<String, Arc<Mutex<RunningWorker>>>,
|
HashMap<String, Sender<MessageToWorker>>,
|
||||||
)> {
|
)> {
|
||||||
let mut removed = vec![];
|
let mut removed = vec![];
|
||||||
let ids_in_plugin_map: Vec<(PluginId, ClientId)> =
|
let ids_in_plugin_map: Vec<(PluginId, ClientId)> =
|
||||||
|
|
@ -62,7 +61,7 @@ impl PluginMap {
|
||||||
) -> Option<(
|
) -> Option<(
|
||||||
Arc<Mutex<RunningPlugin>>,
|
Arc<Mutex<RunningPlugin>>,
|
||||||
Arc<Mutex<Subscriptions>>,
|
Arc<Mutex<Subscriptions>>,
|
||||||
HashMap<String, Arc<Mutex<RunningWorker>>>,
|
HashMap<String, Sender<MessageToWorker>>,
|
||||||
)> {
|
)> {
|
||||||
self.plugin_assets.remove(&(plugin_id, client_id))
|
self.plugin_assets.remove(&(plugin_id, client_id))
|
||||||
}
|
}
|
||||||
|
|
@ -132,12 +131,12 @@ impl PluginMap {
|
||||||
.and_then(|(_, (running_plugin, _, _))| Some(running_plugin.clone())),
|
.and_then(|(_, (running_plugin, _, _))| Some(running_plugin.clone())),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
pub fn clone_worker(
|
pub fn worker_sender(
|
||||||
&self,
|
&self,
|
||||||
plugin_id: PluginId,
|
plugin_id: PluginId,
|
||||||
client_id: ClientId,
|
client_id: ClientId,
|
||||||
worker_name: &str,
|
worker_name: &str,
|
||||||
) -> Option<Arc<Mutex<RunningWorker>>> {
|
) -> Option<Sender<MessageToWorker>> {
|
||||||
self.plugin_assets
|
self.plugin_assets
|
||||||
.iter()
|
.iter()
|
||||||
.find(|((p_id, c_id), _)| p_id == &plugin_id && c_id == &client_id)
|
.find(|((p_id, c_id), _)| p_id == &plugin_id && c_id == &client_id)
|
||||||
|
|
@ -174,7 +173,7 @@ impl PluginMap {
|
||||||
client_id: ClientId,
|
client_id: ClientId,
|
||||||
running_plugin: Arc<Mutex<RunningPlugin>>,
|
running_plugin: Arc<Mutex<RunningPlugin>>,
|
||||||
subscriptions: Arc<Mutex<Subscriptions>>,
|
subscriptions: Arc<Mutex<Subscriptions>>,
|
||||||
running_workers: HashMap<String, Arc<Mutex<RunningWorker>>>,
|
running_workers: HashMap<String, Sender<MessageToWorker>>,
|
||||||
) {
|
) {
|
||||||
self.plugin_assets.insert(
|
self.plugin_assets.insert(
|
||||||
(plugin_id, client_id),
|
(plugin_id, client_id),
|
||||||
|
|
@ -195,6 +194,7 @@ pub struct PluginEnv {
|
||||||
pub client_id: ClientId,
|
pub client_id: ClientId,
|
||||||
#[allow(dead_code)]
|
#[allow(dead_code)]
|
||||||
pub plugin_own_data_dir: PathBuf,
|
pub plugin_own_data_dir: PathBuf,
|
||||||
|
pub path_to_default_shell: PathBuf,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl PluginEnv {
|
impl PluginEnv {
|
||||||
|
|
@ -256,53 +256,3 @@ impl RunningPlugin {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct RunningWorker {
|
|
||||||
pub instance: Instance,
|
|
||||||
pub name: String,
|
|
||||||
pub plugin_config: PluginConfig,
|
|
||||||
pub plugin_env: PluginEnv,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl RunningWorker {
|
|
||||||
pub fn new(
|
|
||||||
instance: Instance,
|
|
||||||
name: &str,
|
|
||||||
plugin_config: PluginConfig,
|
|
||||||
plugin_env: PluginEnv,
|
|
||||||
) -> Self {
|
|
||||||
RunningWorker {
|
|
||||||
instance,
|
|
||||||
name: name.into(),
|
|
||||||
plugin_config,
|
|
||||||
plugin_env,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
pub fn send_message(&self, message: String, payload: String) -> Result<()> {
|
|
||||||
let err_context = || format!("Failed to send message to worker");
|
|
||||||
|
|
||||||
let work_function = self
|
|
||||||
.instance
|
|
||||||
.exports
|
|
||||||
.get_function(&self.name)
|
|
||||||
.with_context(err_context)?;
|
|
||||||
wasi_write_object(&self.plugin_env.wasi_env, &(message, payload))
|
|
||||||
.with_context(err_context)?;
|
|
||||||
work_function.call(&[]).or_else::<anyError, _>(|e| {
|
|
||||||
match e.downcast::<serde_json::Error>() {
|
|
||||||
Ok(_) => panic!(
|
|
||||||
"{}",
|
|
||||||
anyError::new(VersionMismatchError::new(
|
|
||||||
VERSION,
|
|
||||||
"Unavailable",
|
|
||||||
&self.plugin_config.path,
|
|
||||||
self.plugin_config.is_builtin(),
|
|
||||||
))
|
|
||||||
),
|
|
||||||
Err(e) => Err(e).with_context(err_context),
|
|
||||||
}
|
|
||||||
})?;
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
||||||
89
zellij-server/src/plugins/plugin_worker.rs
Normal file
89
zellij-server/src/plugins/plugin_worker.rs
Normal file
|
|
@ -0,0 +1,89 @@
|
||||||
|
use crate::plugins::plugin_loader::VersionMismatchError;
|
||||||
|
use crate::plugins::plugin_map::PluginEnv;
|
||||||
|
use crate::plugins::zellij_exports::wasi_write_object;
|
||||||
|
use wasmer::Instance;
|
||||||
|
|
||||||
|
use zellij_utils::async_channel::{unbounded, Receiver, Sender};
|
||||||
|
use zellij_utils::async_std::task;
|
||||||
|
use zellij_utils::errors::prelude::*;
|
||||||
|
use zellij_utils::{consts::VERSION, input::plugins::PluginConfig};
|
||||||
|
|
||||||
|
pub struct RunningWorker {
|
||||||
|
pub instance: Instance,
|
||||||
|
pub name: String,
|
||||||
|
pub plugin_config: PluginConfig,
|
||||||
|
pub plugin_env: PluginEnv,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl RunningWorker {
|
||||||
|
pub fn new(
|
||||||
|
instance: Instance,
|
||||||
|
name: &str,
|
||||||
|
plugin_config: PluginConfig,
|
||||||
|
plugin_env: PluginEnv,
|
||||||
|
) -> Self {
|
||||||
|
RunningWorker {
|
||||||
|
instance,
|
||||||
|
name: name.into(),
|
||||||
|
plugin_config,
|
||||||
|
plugin_env,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pub fn send_message(&self, message: String, payload: String) -> Result<()> {
|
||||||
|
let err_context = || format!("Failed to send message to worker");
|
||||||
|
|
||||||
|
let work_function = self
|
||||||
|
.instance
|
||||||
|
.exports
|
||||||
|
.get_function(&self.name)
|
||||||
|
.with_context(err_context)?;
|
||||||
|
wasi_write_object(&self.plugin_env.wasi_env, &(message, payload))
|
||||||
|
.with_context(err_context)?;
|
||||||
|
work_function.call(&[]).or_else::<anyError, _>(|e| {
|
||||||
|
match e.downcast::<serde_json::Error>() {
|
||||||
|
Ok(_) => panic!(
|
||||||
|
"{}",
|
||||||
|
anyError::new(VersionMismatchError::new(
|
||||||
|
VERSION,
|
||||||
|
"Unavailable",
|
||||||
|
&self.plugin_config.path,
|
||||||
|
self.plugin_config.is_builtin(),
|
||||||
|
))
|
||||||
|
),
|
||||||
|
Err(e) => Err(e).with_context(err_context),
|
||||||
|
}
|
||||||
|
})?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub enum MessageToWorker {
|
||||||
|
Message(String, String), // message, payload
|
||||||
|
Exit,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn plugin_worker(worker: RunningWorker) -> Sender<MessageToWorker> {
|
||||||
|
let (sender, receiver): (Sender<MessageToWorker>, Receiver<MessageToWorker>) = unbounded();
|
||||||
|
task::spawn({
|
||||||
|
async move {
|
||||||
|
loop {
|
||||||
|
match receiver.recv().await {
|
||||||
|
Ok(MessageToWorker::Message(message, payload)) => {
|
||||||
|
if let Err(e) = worker.send_message(message, payload) {
|
||||||
|
log::error!("Failed to send message to worker: {:?}", e);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
Ok(MessageToWorker::Exit) => {
|
||||||
|
break;
|
||||||
|
},
|
||||||
|
Err(e) => {
|
||||||
|
log::error!("Failed to receive worker message on channel: {:?}", e);
|
||||||
|
break;
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
sender
|
||||||
|
}
|
||||||
|
|
@ -50,11 +50,14 @@ macro_rules! log_actions_in_thread {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
fn create_plugin_thread() -> (
|
fn create_plugin_thread(
|
||||||
|
zellij_cwd: Option<PathBuf>,
|
||||||
|
) -> (
|
||||||
SenderWithContext<PluginInstruction>,
|
SenderWithContext<PluginInstruction>,
|
||||||
Receiver<(ScreenInstruction, ErrorContext)>,
|
Receiver<(ScreenInstruction, ErrorContext)>,
|
||||||
Box<dyn FnMut()>,
|
Box<dyn FnMut()>,
|
||||||
) {
|
) {
|
||||||
|
let zellij_cwd = zellij_cwd.unwrap_or_else(|| PathBuf::from("."));
|
||||||
let (to_server, _server_receiver): ChannelWithContext<ServerInstruction> =
|
let (to_server, _server_receiver): ChannelWithContext<ServerInstruction> =
|
||||||
channels::bounded(50);
|
channels::bounded(50);
|
||||||
let to_server = SenderWithContext::new(to_server);
|
let to_server = SenderWithContext::new(to_server);
|
||||||
|
|
@ -88,6 +91,7 @@ fn create_plugin_thread() -> (
|
||||||
.should_silently_fail();
|
.should_silently_fail();
|
||||||
let store = Store::new(&wasmer::Universal::new(wasmer::Singlepass::default()).engine());
|
let store = Store::new(&wasmer::Universal::new(wasmer::Singlepass::default()).engine());
|
||||||
let data_dir = PathBuf::from(tempdir().unwrap().path());
|
let data_dir = PathBuf::from(tempdir().unwrap().path());
|
||||||
|
let default_shell = PathBuf::from(".");
|
||||||
let _plugin_thread = std::thread::Builder::new()
|
let _plugin_thread = std::thread::Builder::new()
|
||||||
.name("plugin_thread".to_string())
|
.name("plugin_thread".to_string())
|
||||||
.spawn(move || {
|
.spawn(move || {
|
||||||
|
|
@ -98,6 +102,8 @@ fn create_plugin_thread() -> (
|
||||||
data_dir,
|
data_dir,
|
||||||
PluginsConfig::default(),
|
PluginsConfig::default(),
|
||||||
Box::new(Layout::default()),
|
Box::new(Layout::default()),
|
||||||
|
default_shell,
|
||||||
|
zellij_cwd,
|
||||||
)
|
)
|
||||||
.expect("TEST")
|
.expect("TEST")
|
||||||
})
|
})
|
||||||
|
|
@ -134,7 +140,7 @@ pub fn load_new_plugin_from_hd() {
|
||||||
// message (this is what the fixture plugin does)
|
// message (this is what the fixture plugin does)
|
||||||
// we then listen on our mock screen receiver to make sure we got a PluginBytes instruction
|
// we then listen on our mock screen receiver to make sure we got a PluginBytes instruction
|
||||||
// that contains said render, and assert against it
|
// that contains said render, and assert against it
|
||||||
let (plugin_thread_sender, screen_receiver, mut teardown) = create_plugin_thread();
|
let (plugin_thread_sender, screen_receiver, mut teardown) = create_plugin_thread(None);
|
||||||
let plugin_should_float = Some(false);
|
let plugin_should_float = Some(false);
|
||||||
let plugin_title = Some("test_plugin".to_owned());
|
let plugin_title = Some("test_plugin".to_owned());
|
||||||
let run_plugin = RunPlugin {
|
let run_plugin = RunPlugin {
|
||||||
|
|
@ -192,7 +198,7 @@ pub fn load_new_plugin_from_hd() {
|
||||||
#[test]
|
#[test]
|
||||||
#[ignore]
|
#[ignore]
|
||||||
pub fn plugin_workers() {
|
pub fn plugin_workers() {
|
||||||
let (plugin_thread_sender, screen_receiver, mut teardown) = create_plugin_thread();
|
let (plugin_thread_sender, screen_receiver, mut teardown) = create_plugin_thread(None);
|
||||||
let plugin_should_float = Some(false);
|
let plugin_should_float = Some(false);
|
||||||
let plugin_title = Some("test_plugin".to_owned());
|
let plugin_title = Some("test_plugin".to_owned());
|
||||||
let run_plugin = RunPlugin {
|
let run_plugin = RunPlugin {
|
||||||
|
|
@ -253,7 +259,7 @@ pub fn plugin_workers() {
|
||||||
#[test]
|
#[test]
|
||||||
#[ignore]
|
#[ignore]
|
||||||
pub fn plugin_workers_persist_state() {
|
pub fn plugin_workers_persist_state() {
|
||||||
let (plugin_thread_sender, screen_receiver, mut teardown) = create_plugin_thread();
|
let (plugin_thread_sender, screen_receiver, mut teardown) = create_plugin_thread(None);
|
||||||
let plugin_should_float = Some(false);
|
let plugin_should_float = Some(false);
|
||||||
let plugin_title = Some("test_plugin".to_owned());
|
let plugin_title = Some("test_plugin".to_owned());
|
||||||
let run_plugin = RunPlugin {
|
let run_plugin = RunPlugin {
|
||||||
|
|
@ -318,3 +324,71 @@ pub fn plugin_workers_persist_state() {
|
||||||
});
|
});
|
||||||
assert_snapshot!(format!("{:#?}", plugin_bytes_event));
|
assert_snapshot!(format!("{:#?}", plugin_bytes_event));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
#[ignore]
|
||||||
|
pub fn can_subscribe_to_hd_events() {
|
||||||
|
let temp_folder = tempdir().unwrap(); // placed explicitly in the test scope because its
|
||||||
|
// destructor removes the directory
|
||||||
|
let plugin_host_folder = PathBuf::from(temp_folder.path());
|
||||||
|
let (plugin_thread_sender, screen_receiver, mut teardown) =
|
||||||
|
create_plugin_thread(Some(plugin_host_folder));
|
||||||
|
let plugin_should_float = Some(false);
|
||||||
|
let plugin_title = Some("test_plugin".to_owned());
|
||||||
|
let run_plugin = RunPlugin {
|
||||||
|
_allow_exec_host_cmd: false,
|
||||||
|
location: RunPluginLocation::File(PathBuf::from(&*PLUGIN_FIXTURE)),
|
||||||
|
};
|
||||||
|
let tab_index = 1;
|
||||||
|
let client_id = 1;
|
||||||
|
let size = Size {
|
||||||
|
cols: 121,
|
||||||
|
rows: 20,
|
||||||
|
};
|
||||||
|
let received_screen_instructions = Arc::new(Mutex::new(vec![]));
|
||||||
|
let screen_thread = log_actions_in_thread!(
|
||||||
|
received_screen_instructions,
|
||||||
|
ScreenInstruction::PluginBytes,
|
||||||
|
screen_receiver,
|
||||||
|
3
|
||||||
|
);
|
||||||
|
|
||||||
|
let _ = plugin_thread_sender.send(PluginInstruction::AddClient(client_id));
|
||||||
|
let _ = plugin_thread_sender.send(PluginInstruction::Load(
|
||||||
|
plugin_should_float,
|
||||||
|
plugin_title,
|
||||||
|
run_plugin,
|
||||||
|
tab_index,
|
||||||
|
client_id,
|
||||||
|
size,
|
||||||
|
));
|
||||||
|
let _ = plugin_thread_sender.send(PluginInstruction::Update(vec![(
|
||||||
|
None,
|
||||||
|
Some(client_id),
|
||||||
|
Event::InputReceived,
|
||||||
|
)])); // will be cached and sent to the plugin once it's loaded
|
||||||
|
std::thread::sleep(std::time::Duration::from_millis(100));
|
||||||
|
std::fs::OpenOptions::new()
|
||||||
|
.create(true)
|
||||||
|
.write(true)
|
||||||
|
.open(PathBuf::from(temp_folder.path()).join("test1"))
|
||||||
|
.unwrap();
|
||||||
|
screen_thread.join().unwrap(); // this might take a while if the cache is cold
|
||||||
|
teardown();
|
||||||
|
let plugin_bytes_event = received_screen_instructions
|
||||||
|
.lock()
|
||||||
|
.unwrap()
|
||||||
|
.iter()
|
||||||
|
.find_map(|i| {
|
||||||
|
if let ScreenInstruction::PluginBytes(plugin_bytes) = i {
|
||||||
|
for (plugin_id, client_id, plugin_bytes) in plugin_bytes {
|
||||||
|
let plugin_bytes = String::from_utf8_lossy(plugin_bytes).to_string();
|
||||||
|
if plugin_bytes.contains("FileSystem") {
|
||||||
|
return Some((*plugin_id, *client_id, plugin_bytes));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None
|
||||||
|
});
|
||||||
|
assert_snapshot!(format!("{:#?}", plugin_bytes_event));
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,12 @@
|
||||||
|
---
|
||||||
|
source: zellij-server/src/plugins/./unit/plugin_tests.rs
|
||||||
|
assertion_line: 387
|
||||||
|
expression: "format!(\"{:#?}\", plugin_bytes_event)"
|
||||||
|
---
|
||||||
|
Some(
|
||||||
|
(
|
||||||
|
0,
|
||||||
|
1,
|
||||||
|
"Rows: 20, Cols: 121, Received events: [InputReceived, FileSystemRead([\"/host/test1\"])]\n\r",
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
@ -1,24 +1,24 @@
|
||||||
use super::{PluginId, PluginInstruction};
|
use super::{PluginId, PluginInstruction};
|
||||||
use crate::plugins::plugin_loader::{PluginLoader, VersionMismatchError};
|
use crate::plugins::plugin_loader::{PluginLoader, VersionMismatchError};
|
||||||
use crate::plugins::plugin_map::{
|
use crate::plugins::plugin_map::{AtomicEvent, PluginEnv, PluginMap, RunningPlugin, Subscriptions};
|
||||||
AtomicEvent, PluginEnv, PluginMap, RunningPlugin, RunningWorker, Subscriptions,
|
use crate::plugins::plugin_worker::MessageToWorker;
|
||||||
};
|
use crate::plugins::watch_filesystem::watch_filesystem;
|
||||||
use crate::plugins::zellij_exports::{wasi_read_string, wasi_write_object};
|
use crate::plugins::zellij_exports::{wasi_read_string, wasi_write_object};
|
||||||
use log::info;
|
use log::info;
|
||||||
use std::{
|
use std::{
|
||||||
collections::{HashMap, HashSet},
|
collections::{HashMap, HashSet},
|
||||||
path::PathBuf,
|
path::PathBuf,
|
||||||
str::FromStr,
|
str::FromStr,
|
||||||
sync::{Arc, Mutex, TryLockError},
|
sync::{Arc, Mutex},
|
||||||
};
|
};
|
||||||
use wasmer::{Instance, Module, Store, Value};
|
use wasmer::{Instance, Module, Store, Value};
|
||||||
use zellij_utils::async_std::task::{self, JoinHandle};
|
use zellij_utils::async_std::task::{self, JoinHandle};
|
||||||
|
use zellij_utils::notify::{RecommendedWatcher, Watcher};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
background_jobs::BackgroundJob, screen::ScreenInstruction, thread_bus::ThreadSenders,
|
background_jobs::BackgroundJob, screen::ScreenInstruction, thread_bus::ThreadSenders,
|
||||||
ui::loading_indication::LoadingIndication, ClientId,
|
ui::loading_indication::LoadingIndication, ClientId,
|
||||||
};
|
};
|
||||||
|
|
||||||
use zellij_utils::{
|
use zellij_utils::{
|
||||||
consts::VERSION,
|
consts::VERSION,
|
||||||
data::{Event, EventType},
|
data::{Event, EventType},
|
||||||
|
|
@ -30,8 +30,6 @@ use zellij_utils::{
|
||||||
pane_size::Size,
|
pane_size::Size,
|
||||||
};
|
};
|
||||||
|
|
||||||
const RETRY_INTERVAL_MS: u64 = 100;
|
|
||||||
|
|
||||||
pub struct WasmBridge {
|
pub struct WasmBridge {
|
||||||
connected_clients: Arc<Mutex<Vec<ClientId>>>,
|
connected_clients: Arc<Mutex<Vec<ClientId>>>,
|
||||||
plugins: PluginsConfig,
|
plugins: PluginsConfig,
|
||||||
|
|
@ -49,6 +47,9 @@ pub struct WasmBridge {
|
||||||
// payload>
|
// payload>
|
||||||
loading_plugins: HashMap<(PluginId, RunPlugin), JoinHandle<()>>, // plugin_id to join-handle
|
loading_plugins: HashMap<(PluginId, RunPlugin), JoinHandle<()>>, // plugin_id to join-handle
|
||||||
pending_plugin_reloads: HashSet<RunPlugin>,
|
pending_plugin_reloads: HashSet<RunPlugin>,
|
||||||
|
path_to_default_shell: PathBuf,
|
||||||
|
watcher: Option<RecommendedWatcher>,
|
||||||
|
zellij_cwd: PathBuf,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl WasmBridge {
|
impl WasmBridge {
|
||||||
|
|
@ -57,11 +58,20 @@ impl WasmBridge {
|
||||||
senders: ThreadSenders,
|
senders: ThreadSenders,
|
||||||
store: Store,
|
store: Store,
|
||||||
plugin_dir: PathBuf,
|
plugin_dir: PathBuf,
|
||||||
|
path_to_default_shell: PathBuf,
|
||||||
|
zellij_cwd: PathBuf,
|
||||||
) -> Self {
|
) -> Self {
|
||||||
let plugin_map = Arc::new(Mutex::new(PluginMap::default()));
|
let plugin_map = Arc::new(Mutex::new(PluginMap::default()));
|
||||||
let connected_clients: Arc<Mutex<Vec<ClientId>>> = Arc::new(Mutex::new(vec![]));
|
let connected_clients: Arc<Mutex<Vec<ClientId>>> = Arc::new(Mutex::new(vec![]));
|
||||||
let plugin_cache: Arc<Mutex<HashMap<PathBuf, Module>>> =
|
let plugin_cache: Arc<Mutex<HashMap<PathBuf, Module>>> =
|
||||||
Arc::new(Mutex::new(HashMap::new()));
|
Arc::new(Mutex::new(HashMap::new()));
|
||||||
|
let watcher = match watch_filesystem(senders.clone(), &zellij_cwd) {
|
||||||
|
Ok(watcher) => Some(watcher),
|
||||||
|
Err(e) => {
|
||||||
|
log::error!("Failed to watch filesystem: {:?}", e);
|
||||||
|
None
|
||||||
|
},
|
||||||
|
};
|
||||||
WasmBridge {
|
WasmBridge {
|
||||||
connected_clients,
|
connected_clients,
|
||||||
plugins,
|
plugins,
|
||||||
|
|
@ -70,12 +80,15 @@ impl WasmBridge {
|
||||||
plugin_dir,
|
plugin_dir,
|
||||||
plugin_cache,
|
plugin_cache,
|
||||||
plugin_map,
|
plugin_map,
|
||||||
|
path_to_default_shell,
|
||||||
|
watcher,
|
||||||
next_plugin_id: 0,
|
next_plugin_id: 0,
|
||||||
cached_events_for_pending_plugins: HashMap::new(),
|
cached_events_for_pending_plugins: HashMap::new(),
|
||||||
cached_resizes_for_pending_plugins: HashMap::new(),
|
cached_resizes_for_pending_plugins: HashMap::new(),
|
||||||
cached_worker_messages: HashMap::new(),
|
cached_worker_messages: HashMap::new(),
|
||||||
loading_plugins: HashMap::new(),
|
loading_plugins: HashMap::new(),
|
||||||
pending_plugin_reloads: HashSet::new(),
|
pending_plugin_reloads: HashSet::new(),
|
||||||
|
zellij_cwd,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
pub fn load_plugin(
|
pub fn load_plugin(
|
||||||
|
|
@ -122,6 +135,8 @@ impl WasmBridge {
|
||||||
let store = self.store.clone();
|
let store = self.store.clone();
|
||||||
let plugin_map = self.plugin_map.clone();
|
let plugin_map = self.plugin_map.clone();
|
||||||
let connected_clients = self.connected_clients.clone();
|
let connected_clients = self.connected_clients.clone();
|
||||||
|
let path_to_default_shell = self.path_to_default_shell.clone();
|
||||||
|
let zellij_cwd = self.zellij_cwd.clone();
|
||||||
async move {
|
async move {
|
||||||
let _ =
|
let _ =
|
||||||
senders.send_to_background_jobs(BackgroundJob::AnimatePluginLoading(plugin_id));
|
senders.send_to_background_jobs(BackgroundJob::AnimatePluginLoading(plugin_id));
|
||||||
|
|
@ -139,6 +154,8 @@ impl WasmBridge {
|
||||||
size,
|
size,
|
||||||
connected_clients.clone(),
|
connected_clients.clone(),
|
||||||
&mut loading_indication,
|
&mut loading_indication,
|
||||||
|
path_to_default_shell,
|
||||||
|
zellij_cwd.clone(),
|
||||||
) {
|
) {
|
||||||
Ok(_) => handle_plugin_successful_loading(&senders, plugin_id),
|
Ok(_) => handle_plugin_successful_loading(&senders, plugin_id),
|
||||||
Err(e) => handle_plugin_loading_failure(
|
Err(e) => handle_plugin_loading_failure(
|
||||||
|
|
@ -160,7 +177,10 @@ impl WasmBridge {
|
||||||
pub fn unload_plugin(&mut self, pid: PluginId) -> Result<()> {
|
pub fn unload_plugin(&mut self, pid: PluginId) -> Result<()> {
|
||||||
info!("Bye from plugin {}", &pid);
|
info!("Bye from plugin {}", &pid);
|
||||||
let mut plugin_map = self.plugin_map.lock().unwrap();
|
let mut plugin_map = self.plugin_map.lock().unwrap();
|
||||||
for (running_plugin, _, _) in plugin_map.remove_plugins(pid) {
|
for (running_plugin, _, workers) in plugin_map.remove_plugins(pid) {
|
||||||
|
for (_worker_name, worker_sender) in workers {
|
||||||
|
drop(worker_sender.send(MessageToWorker::Exit));
|
||||||
|
}
|
||||||
let running_plugin = running_plugin.lock().unwrap();
|
let running_plugin = running_plugin.lock().unwrap();
|
||||||
let cache_dir = running_plugin.plugin_env.plugin_own_data_dir.clone();
|
let cache_dir = running_plugin.plugin_env.plugin_own_data_dir.clone();
|
||||||
if let Err(e) = std::fs::remove_dir_all(cache_dir) {
|
if let Err(e) = std::fs::remove_dir_all(cache_dir) {
|
||||||
|
|
@ -195,6 +215,8 @@ impl WasmBridge {
|
||||||
let store = self.store.clone();
|
let store = self.store.clone();
|
||||||
let plugin_map = self.plugin_map.clone();
|
let plugin_map = self.plugin_map.clone();
|
||||||
let connected_clients = self.connected_clients.clone();
|
let connected_clients = self.connected_clients.clone();
|
||||||
|
let path_to_default_shell = self.path_to_default_shell.clone();
|
||||||
|
let zellij_cwd = self.zellij_cwd.clone();
|
||||||
async move {
|
async move {
|
||||||
match PluginLoader::reload_plugin(
|
match PluginLoader::reload_plugin(
|
||||||
first_plugin_id,
|
first_plugin_id,
|
||||||
|
|
@ -205,6 +227,8 @@ impl WasmBridge {
|
||||||
plugin_map.clone(),
|
plugin_map.clone(),
|
||||||
connected_clients.clone(),
|
connected_clients.clone(),
|
||||||
&mut loading_indication,
|
&mut loading_indication,
|
||||||
|
path_to_default_shell.clone(),
|
||||||
|
zellij_cwd.clone(),
|
||||||
) {
|
) {
|
||||||
Ok(_) => {
|
Ok(_) => {
|
||||||
handle_plugin_successful_loading(&senders, first_plugin_id);
|
handle_plugin_successful_loading(&senders, first_plugin_id);
|
||||||
|
|
@ -223,6 +247,8 @@ impl WasmBridge {
|
||||||
plugin_map.clone(),
|
plugin_map.clone(),
|
||||||
connected_clients.clone(),
|
connected_clients.clone(),
|
||||||
&mut loading_indication,
|
&mut loading_indication,
|
||||||
|
path_to_default_shell.clone(),
|
||||||
|
zellij_cwd.clone(),
|
||||||
) {
|
) {
|
||||||
Ok(_) => handle_plugin_successful_loading(&senders, *plugin_id),
|
Ok(_) => handle_plugin_successful_loading(&senders, *plugin_id),
|
||||||
Err(e) => handle_plugin_loading_failure(
|
Err(e) => handle_plugin_loading_failure(
|
||||||
|
|
@ -263,6 +289,8 @@ impl WasmBridge {
|
||||||
self.plugin_map.clone(),
|
self.plugin_map.clone(),
|
||||||
self.connected_clients.clone(),
|
self.connected_clients.clone(),
|
||||||
&mut loading_indication,
|
&mut loading_indication,
|
||||||
|
self.path_to_default_shell.clone(),
|
||||||
|
self.zellij_cwd.clone(),
|
||||||
) {
|
) {
|
||||||
Ok(_) => {
|
Ok(_) => {
|
||||||
let _ = self
|
let _ = self
|
||||||
|
|
@ -414,7 +442,17 @@ impl WasmBridge {
|
||||||
));
|
));
|
||||||
},
|
},
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
log::error!("{}", e);
|
log::error!("{:?}", e);
|
||||||
|
|
||||||
|
// https://stackoverflow.com/questions/66450942/in-rust-is-there-a-way-to-make-literal-newlines-in-r-using-windows-c
|
||||||
|
let stringified_error =
|
||||||
|
format!("{:?}", e).replace("\n", "\n\r");
|
||||||
|
|
||||||
|
handle_plugin_crash(
|
||||||
|
plugin_id,
|
||||||
|
stringified_error,
|
||||||
|
senders.clone(),
|
||||||
|
);
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -460,6 +498,7 @@ impl WasmBridge {
|
||||||
for plugin_id in &plugin_ids {
|
for plugin_id in &plugin_ids {
|
||||||
drop(self.unload_plugin(*plugin_id));
|
drop(self.unload_plugin(*plugin_id));
|
||||||
}
|
}
|
||||||
|
drop(self.watcher.as_mut().map(|w| w.unwatch(&self.zellij_cwd)));
|
||||||
}
|
}
|
||||||
fn run_plugin_of_plugin_id(&self, plugin_id: PluginId) -> Option<&RunPlugin> {
|
fn run_plugin_of_plugin_id(&self, plugin_id: PluginId) -> Option<&RunPlugin> {
|
||||||
self.loading_plugins
|
self.loading_plugins
|
||||||
|
|
@ -599,36 +638,23 @@ impl WasmBridge {
|
||||||
self.plugin_map
|
self.plugin_map
|
||||||
.lock()
|
.lock()
|
||||||
.unwrap()
|
.unwrap()
|
||||||
.clone_worker(plugin_id, client_id, &worker_name);
|
.worker_sender(plugin_id, client_id, &worker_name);
|
||||||
let mut cache_messages = || {
|
match worker {
|
||||||
|
Some(worker) => {
|
||||||
|
for (message, payload) in messages.drain(..) {
|
||||||
|
if let Err(e) = worker.try_send(MessageToWorker::Message(message, payload)) {
|
||||||
|
log::error!("Failed to send message to worker: {:?}", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
None => {
|
||||||
|
log::warn!("Worker {worker_name} not found, caching messages");
|
||||||
for (message, payload) in messages.drain(..) {
|
for (message, payload) in messages.drain(..) {
|
||||||
self.cached_worker_messages
|
self.cached_worker_messages
|
||||||
.entry(plugin_id)
|
.entry(plugin_id)
|
||||||
.or_default()
|
.or_default()
|
||||||
.push((client_id, worker_name.clone(), message, payload));
|
.push((client_id, worker_name.clone(), message, payload));
|
||||||
}
|
}
|
||||||
};
|
|
||||||
match worker {
|
|
||||||
Some(worker) => {
|
|
||||||
let worker_is_busy = { worker.try_lock().is_err() };
|
|
||||||
if worker_is_busy {
|
|
||||||
// most messages will be caught here, we do this once before the async task to
|
|
||||||
// bulk most messages together and prevent them from cascading
|
|
||||||
cache_messages();
|
|
||||||
} else {
|
|
||||||
async_send_messages_to_worker(
|
|
||||||
self.senders.clone(),
|
|
||||||
messages,
|
|
||||||
worker,
|
|
||||||
plugin_id,
|
|
||||||
client_id,
|
|
||||||
worker_name,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
None => {
|
|
||||||
log::warn!("Worker {worker_name} not found, placing message in cache");
|
|
||||||
cache_messages();
|
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
|
|
@ -708,52 +734,12 @@ pub fn apply_event_to_plugin(
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn async_send_messages_to_worker(
|
pub fn handle_plugin_crash(plugin_id: PluginId, message: String, senders: ThreadSenders) {
|
||||||
senders: ThreadSenders,
|
let mut loading_indication = LoadingIndication::new("Panic!".to_owned());
|
||||||
mut messages: Vec<(String, String)>,
|
loading_indication.indicate_loading_error(message);
|
||||||
worker: Arc<Mutex<RunningWorker>>,
|
let _ = senders.send_to_screen(ScreenInstruction::UpdatePluginLoadingStage(
|
||||||
plugin_id: PluginId,
|
|
||||||
client_id: ClientId,
|
|
||||||
worker_name: String,
|
|
||||||
) {
|
|
||||||
task::spawn({
|
|
||||||
async move {
|
|
||||||
match worker.try_lock() {
|
|
||||||
Ok(worker) => {
|
|
||||||
for (message, payload) in messages.drain(..) {
|
|
||||||
worker.send_message(message, payload).ok();
|
|
||||||
}
|
|
||||||
let _ = senders
|
|
||||||
.send_to_plugin(PluginInstruction::ApplyCachedWorkerMessages(plugin_id));
|
|
||||||
},
|
|
||||||
Err(TryLockError::WouldBlock) => {
|
|
||||||
task::spawn({
|
|
||||||
async move {
|
|
||||||
log::warn!(
|
|
||||||
"Worker {} busy, retrying sending message after: {}ms",
|
|
||||||
worker_name,
|
|
||||||
RETRY_INTERVAL_MS
|
|
||||||
);
|
|
||||||
task::sleep(std::time::Duration::from_millis(RETRY_INTERVAL_MS)).await;
|
|
||||||
let _ = senders.send_to_plugin(
|
|
||||||
PluginInstruction::PostMessagesToPluginWorker(
|
|
||||||
plugin_id,
|
plugin_id,
|
||||||
client_id,
|
loading_indication,
|
||||||
worker_name,
|
));
|
||||||
messages,
|
let _ = senders.send_to_plugin(PluginInstruction::Unload(plugin_id));
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
},
|
|
||||||
Err(e) => {
|
|
||||||
log::error!(
|
|
||||||
"Failed to send message to worker \"{}\": {:?}",
|
|
||||||
worker_name,
|
|
||||||
e
|
|
||||||
);
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
63
zellij-server/src/plugins/watch_filesystem.rs
Normal file
63
zellij-server/src/plugins/watch_filesystem.rs
Normal file
|
|
@ -0,0 +1,63 @@
|
||||||
|
use super::PluginInstruction;
|
||||||
|
use std::path::PathBuf;
|
||||||
|
|
||||||
|
use crate::thread_bus::ThreadSenders;
|
||||||
|
use std::path::Path;
|
||||||
|
|
||||||
|
use zellij_utils::{data::Event, errors::prelude::*};
|
||||||
|
|
||||||
|
use zellij_utils::notify::{self, EventKind, RecommendedWatcher, RecursiveMode, Watcher};
|
||||||
|
pub fn watch_filesystem(senders: ThreadSenders, zellij_cwd: &Path) -> Result<RecommendedWatcher> {
|
||||||
|
let path_prefix_in_plugins = PathBuf::from("/host");
|
||||||
|
let current_dir = PathBuf::from(zellij_cwd);
|
||||||
|
let mut watcher = notify::recommended_watcher({
|
||||||
|
move |res: notify::Result<notify::Event>| match res {
|
||||||
|
Ok(event) => {
|
||||||
|
let paths: Vec<PathBuf> = event
|
||||||
|
.paths
|
||||||
|
.iter()
|
||||||
|
.map(|p| {
|
||||||
|
let stripped_prefix_path =
|
||||||
|
p.strip_prefix(¤t_dir).unwrap_or_else(|_| p);
|
||||||
|
path_prefix_in_plugins.join(stripped_prefix_path)
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
match event.kind {
|
||||||
|
EventKind::Access(_) => {
|
||||||
|
let _ = senders.send_to_plugin(PluginInstruction::Update(vec![(
|
||||||
|
None,
|
||||||
|
None,
|
||||||
|
Event::FileSystemRead(paths),
|
||||||
|
)]));
|
||||||
|
},
|
||||||
|
EventKind::Create(_) => {
|
||||||
|
let _ = senders.send_to_plugin(PluginInstruction::Update(vec![(
|
||||||
|
None,
|
||||||
|
None,
|
||||||
|
Event::FileSystemCreate(paths),
|
||||||
|
)]));
|
||||||
|
},
|
||||||
|
EventKind::Modify(_) => {
|
||||||
|
let _ = senders.send_to_plugin(PluginInstruction::Update(vec![(
|
||||||
|
None,
|
||||||
|
None,
|
||||||
|
Event::FileSystemUpdate(paths),
|
||||||
|
)]));
|
||||||
|
},
|
||||||
|
EventKind::Remove(_) => {
|
||||||
|
let _ = senders.send_to_plugin(PluginInstruction::Update(vec![(
|
||||||
|
None,
|
||||||
|
None,
|
||||||
|
Event::FileSystemDelete(paths),
|
||||||
|
)]));
|
||||||
|
},
|
||||||
|
_ => {},
|
||||||
|
}
|
||||||
|
},
|
||||||
|
Err(e) => log::error!("watch error: {:?}", e),
|
||||||
|
}
|
||||||
|
})?;
|
||||||
|
|
||||||
|
watcher.watch(zellij_cwd, RecursiveMode::Recursive)?;
|
||||||
|
Ok(watcher)
|
||||||
|
}
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
use super::PluginInstruction;
|
use super::PluginInstruction;
|
||||||
use crate::plugins::plugin_map::{PluginEnv, Subscriptions};
|
use crate::plugins::plugin_map::{PluginEnv, Subscriptions};
|
||||||
|
use crate::plugins::wasm_bridge::handle_plugin_crash;
|
||||||
use log::{debug, warn};
|
use log::{debug, warn};
|
||||||
use serde::{de::DeserializeOwned, Serialize};
|
use serde::{de::DeserializeOwned, Serialize};
|
||||||
use std::{
|
use std::{
|
||||||
|
|
@ -23,7 +24,10 @@ use zellij_utils::{
|
||||||
consts::VERSION,
|
consts::VERSION,
|
||||||
data::{Event, EventType, PluginIds},
|
data::{Event, EventType, PluginIds},
|
||||||
errors::prelude::*,
|
errors::prelude::*,
|
||||||
input::{command::TerminalAction, plugins::PluginType},
|
input::{
|
||||||
|
command::{RunCommand, TerminalAction},
|
||||||
|
plugins::PluginType,
|
||||||
|
},
|
||||||
serde,
|
serde,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -50,13 +54,18 @@ pub fn zellij_exports(
|
||||||
host_get_plugin_ids,
|
host_get_plugin_ids,
|
||||||
host_get_zellij_version,
|
host_get_zellij_version,
|
||||||
host_open_file,
|
host_open_file,
|
||||||
|
host_open_file_floating,
|
||||||
host_open_file_with_line,
|
host_open_file_with_line,
|
||||||
|
host_open_file_with_line_floating,
|
||||||
|
host_open_terminal,
|
||||||
|
host_open_terminal_floating,
|
||||||
host_switch_tab_to,
|
host_switch_tab_to,
|
||||||
host_set_timeout,
|
host_set_timeout,
|
||||||
host_exec_cmd,
|
host_exec_cmd,
|
||||||
host_report_panic,
|
host_report_panic,
|
||||||
host_post_message_to,
|
host_post_message_to,
|
||||||
host_post_message_to_plugin,
|
host_post_message_to_plugin,
|
||||||
|
host_hide_self,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -160,9 +169,30 @@ fn host_open_file(env: &ForeignFunctionEnv) {
|
||||||
.senders
|
.senders
|
||||||
.send_to_pty(PtyInstruction::SpawnTerminal(
|
.send_to_pty(PtyInstruction::SpawnTerminal(
|
||||||
Some(TerminalAction::OpenFile(path, None, None)),
|
Some(TerminalAction::OpenFile(path, None, None)),
|
||||||
|
Some(false),
|
||||||
None,
|
None,
|
||||||
|
ClientOrTabIndex::ClientId(env.plugin_env.client_id),
|
||||||
|
))
|
||||||
|
})
|
||||||
|
.with_context(|| {
|
||||||
|
format!(
|
||||||
|
"failed to open file on host from plugin {}",
|
||||||
|
env.plugin_env.name()
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.non_fatal();
|
||||||
|
}
|
||||||
|
|
||||||
|
fn host_open_file_floating(env: &ForeignFunctionEnv) {
|
||||||
|
wasi_read_object::<PathBuf>(&env.plugin_env.wasi_env)
|
||||||
|
.and_then(|path| {
|
||||||
|
env.plugin_env
|
||||||
|
.senders
|
||||||
|
.send_to_pty(PtyInstruction::SpawnTerminal(
|
||||||
|
Some(TerminalAction::OpenFile(path, None, None)),
|
||||||
|
Some(true),
|
||||||
None,
|
None,
|
||||||
ClientOrTabIndex::TabIndex(env.plugin_env.tab_index),
|
ClientOrTabIndex::ClientId(env.plugin_env.client_id),
|
||||||
))
|
))
|
||||||
})
|
})
|
||||||
.with_context(|| {
|
.with_context(|| {
|
||||||
|
|
@ -181,9 +211,9 @@ fn host_open_file_with_line(env: &ForeignFunctionEnv) {
|
||||||
.senders
|
.senders
|
||||||
.send_to_pty(PtyInstruction::SpawnTerminal(
|
.send_to_pty(PtyInstruction::SpawnTerminal(
|
||||||
Some(TerminalAction::OpenFile(path, Some(line), None)), // TODO: add cwd
|
Some(TerminalAction::OpenFile(path, Some(line), None)), // TODO: add cwd
|
||||||
|
Some(false),
|
||||||
None,
|
None,
|
||||||
None,
|
ClientOrTabIndex::ClientId(env.plugin_env.client_id),
|
||||||
ClientOrTabIndex::TabIndex(env.plugin_env.tab_index),
|
|
||||||
))
|
))
|
||||||
})
|
})
|
||||||
.with_context(|| {
|
.with_context(|| {
|
||||||
|
|
@ -195,6 +225,75 @@ fn host_open_file_with_line(env: &ForeignFunctionEnv) {
|
||||||
.non_fatal();
|
.non_fatal();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn host_open_file_with_line_floating(env: &ForeignFunctionEnv) {
|
||||||
|
wasi_read_object::<(PathBuf, usize)>(&env.plugin_env.wasi_env)
|
||||||
|
.and_then(|(path, line)| {
|
||||||
|
env.plugin_env
|
||||||
|
.senders
|
||||||
|
.send_to_pty(PtyInstruction::SpawnTerminal(
|
||||||
|
Some(TerminalAction::OpenFile(path, Some(line), None)), // TODO: add cwd
|
||||||
|
Some(true),
|
||||||
|
None,
|
||||||
|
ClientOrTabIndex::ClientId(env.plugin_env.client_id),
|
||||||
|
))
|
||||||
|
})
|
||||||
|
.with_context(|| {
|
||||||
|
format!(
|
||||||
|
"failed to open file on host from plugin {}",
|
||||||
|
env.plugin_env.name()
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.non_fatal();
|
||||||
|
}
|
||||||
|
|
||||||
|
fn host_open_terminal(env: &ForeignFunctionEnv) {
|
||||||
|
wasi_read_object::<PathBuf>(&env.plugin_env.wasi_env)
|
||||||
|
.and_then(|path| {
|
||||||
|
env.plugin_env
|
||||||
|
.senders
|
||||||
|
.send_to_pty(PtyInstruction::SpawnTerminal(
|
||||||
|
Some(TerminalAction::RunCommand(
|
||||||
|
RunCommand::new(env.plugin_env.path_to_default_shell.clone())
|
||||||
|
.with_cwd(path),
|
||||||
|
)),
|
||||||
|
Some(false),
|
||||||
|
None,
|
||||||
|
ClientOrTabIndex::ClientId(env.plugin_env.client_id),
|
||||||
|
))
|
||||||
|
})
|
||||||
|
.with_context(|| {
|
||||||
|
format!(
|
||||||
|
"failed to open terminal on host from plugin {}",
|
||||||
|
env.plugin_env.name()
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.non_fatal();
|
||||||
|
}
|
||||||
|
|
||||||
|
fn host_open_terminal_floating(env: &ForeignFunctionEnv) {
|
||||||
|
wasi_read_object::<PathBuf>(&env.plugin_env.wasi_env)
|
||||||
|
.and_then(|path| {
|
||||||
|
env.plugin_env
|
||||||
|
.senders
|
||||||
|
.send_to_pty(PtyInstruction::SpawnTerminal(
|
||||||
|
Some(TerminalAction::RunCommand(
|
||||||
|
RunCommand::new(env.plugin_env.path_to_default_shell.clone())
|
||||||
|
.with_cwd(path),
|
||||||
|
)),
|
||||||
|
Some(true),
|
||||||
|
None,
|
||||||
|
ClientOrTabIndex::ClientId(env.plugin_env.client_id),
|
||||||
|
))
|
||||||
|
})
|
||||||
|
.with_context(|| {
|
||||||
|
format!(
|
||||||
|
"failed to open terminal on host from plugin {}",
|
||||||
|
env.plugin_env.name()
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.non_fatal();
|
||||||
|
}
|
||||||
|
|
||||||
fn host_switch_tab_to(env: &ForeignFunctionEnv, tab_idx: u32) {
|
fn host_switch_tab_to(env: &ForeignFunctionEnv, tab_idx: u32) {
|
||||||
env.plugin_env
|
env.plugin_env
|
||||||
.senders
|
.senders
|
||||||
|
|
@ -314,6 +413,17 @@ fn host_post_message_to_plugin(env: &ForeignFunctionEnv) {
|
||||||
.fatal();
|
.fatal();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn host_hide_self(env: &ForeignFunctionEnv) {
|
||||||
|
env.plugin_env
|
||||||
|
.senders
|
||||||
|
.send_to_screen(ScreenInstruction::SuppressPane(
|
||||||
|
PaneId::Plugin(env.plugin_env.plugin_id),
|
||||||
|
env.plugin_env.client_id,
|
||||||
|
))
|
||||||
|
.with_context(|| format!("failed to hide self"))
|
||||||
|
.fatal();
|
||||||
|
}
|
||||||
|
|
||||||
// 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
|
||||||
|
|
@ -328,7 +438,12 @@ fn host_report_panic(env: &ForeignFunctionEnv) {
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
.fatal();
|
.fatal();
|
||||||
panic!("{}", msg);
|
log::error!("PANIC IN PLUGIN! {}", msg);
|
||||||
|
handle_plugin_crash(
|
||||||
|
env.plugin_env.plugin_id,
|
||||||
|
msg,
|
||||||
|
env.plugin_env.senders.clone(),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Helper Functions ---------------------------------------------------------------------------------------------------
|
// Helper Functions ---------------------------------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,7 @@ use crate::{
|
||||||
ClientId, ServerInstruction,
|
ClientId, ServerInstruction,
|
||||||
};
|
};
|
||||||
use async_std::task::{self, JoinHandle};
|
use async_std::task::{self, JoinHandle};
|
||||||
use std::{collections::HashMap, env, os::unix::io::RawFd, path::PathBuf};
|
use std::{collections::HashMap, os::unix::io::RawFd, path::PathBuf};
|
||||||
use zellij_utils::nix::unistd::Pid;
|
use zellij_utils::nix::unistd::Pid;
|
||||||
use zellij_utils::{
|
use zellij_utils::{
|
||||||
async_std,
|
async_std,
|
||||||
|
|
@ -468,10 +468,7 @@ impl Pty {
|
||||||
default_shell
|
default_shell
|
||||||
},
|
},
|
||||||
None => {
|
None => {
|
||||||
let shell = PathBuf::from(env::var("SHELL").unwrap_or_else(|_| {
|
let shell = get_default_shell();
|
||||||
log::warn!("Cannot read SHELL env, falling back to use /bin/sh");
|
|
||||||
"/bin/sh".to_string()
|
|
||||||
}));
|
|
||||||
TerminalAction::RunCommand(RunCommand {
|
TerminalAction::RunCommand(RunCommand {
|
||||||
args: vec![],
|
args: vec![],
|
||||||
command: shell,
|
command: shell,
|
||||||
|
|
@ -1048,3 +1045,10 @@ fn send_command_not_found_to_screen(
|
||||||
.with_context(err_context)?;
|
.with_context(err_context)?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn get_default_shell() -> PathBuf {
|
||||||
|
PathBuf::from(std::env::var("SHELL").unwrap_or_else(|_| {
|
||||||
|
log::warn!("Cannot read SHELL env, falling back to use /bin/sh");
|
||||||
|
"/bin/sh".to_string()
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -702,6 +702,16 @@ pub(crate) fn route_action(
|
||||||
))
|
))
|
||||||
.with_context(err_context)?;
|
.with_context(err_context)?;
|
||||||
},
|
},
|
||||||
|
Action::LaunchOrFocusPlugin(run_plugin, should_float) => {
|
||||||
|
session
|
||||||
|
.senders
|
||||||
|
.send_to_screen(ScreenInstruction::LaunchOrFocusPlugin(
|
||||||
|
run_plugin,
|
||||||
|
should_float,
|
||||||
|
client_id,
|
||||||
|
))
|
||||||
|
.with_context(err_context)?;
|
||||||
|
},
|
||||||
}
|
}
|
||||||
Ok(should_break)
|
Ok(should_break)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -273,6 +273,8 @@ pub enum ScreenInstruction {
|
||||||
StartPluginLoadingIndication(u32, LoadingIndication), // u32 - plugin_id
|
StartPluginLoadingIndication(u32, LoadingIndication), // u32 - plugin_id
|
||||||
ProgressPluginLoadingOffset(u32), // u32 - plugin id
|
ProgressPluginLoadingOffset(u32), // u32 - plugin id
|
||||||
RequestStateUpdateForPlugins,
|
RequestStateUpdateForPlugins,
|
||||||
|
LaunchOrFocusPlugin(RunPlugin, bool, ClientId), // bool is should_float
|
||||||
|
SuppressPane(PaneId, ClientId),
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<&ScreenInstruction> for ScreenContext {
|
impl From<&ScreenInstruction> for ScreenContext {
|
||||||
|
|
@ -435,6 +437,8 @@ impl From<&ScreenInstruction> for ScreenContext {
|
||||||
ScreenInstruction::RequestStateUpdateForPlugins => {
|
ScreenInstruction::RequestStateUpdateForPlugins => {
|
||||||
ScreenContext::RequestStateUpdateForPlugins
|
ScreenContext::RequestStateUpdateForPlugins
|
||||||
},
|
},
|
||||||
|
ScreenInstruction::LaunchOrFocusPlugin(..) => ScreenContext::LaunchOrFocusPlugin,
|
||||||
|
ScreenInstruction::SuppressPane(..) => ScreenContext::SuppressPane,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1462,6 +1466,24 @@ impl Screen {
|
||||||
self.render()
|
self.render()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn focus_plugin_pane(
|
||||||
|
&mut self,
|
||||||
|
run_plugin: &RunPlugin,
|
||||||
|
should_float: bool,
|
||||||
|
client_id: ClientId,
|
||||||
|
) -> Result<bool> {
|
||||||
|
// true => found and focused, false => not
|
||||||
|
let all_tabs = self.get_tabs_mut();
|
||||||
|
for tab in all_tabs.values_mut() {
|
||||||
|
if let Some(plugin_pane_id) = tab.find_plugin(&run_plugin) {
|
||||||
|
tab.focus_pane_with_id(plugin_pane_id, should_float, client_id)
|
||||||
|
.context("failed to focus plugin pane")?;
|
||||||
|
return Ok(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(false)
|
||||||
|
}
|
||||||
|
|
||||||
fn unblock_input(&self) -> Result<()> {
|
fn unblock_input(&self) -> Result<()> {
|
||||||
self.bus
|
self.bus
|
||||||
.senders
|
.senders
|
||||||
|
|
@ -1557,6 +1579,7 @@ pub(crate) fn screen_thread_main(
|
||||||
client_id: ClientId| tab .new_pane(pid,
|
client_id: ClientId| tab .new_pane(pid,
|
||||||
initial_pane_title,
|
initial_pane_title,
|
||||||
should_float,
|
should_float,
|
||||||
|
None,
|
||||||
Some(client_id)),
|
Some(client_id)),
|
||||||
?);
|
?);
|
||||||
if let Some(hold_for_command) = hold_for_command {
|
if let Some(hold_for_command) = hold_for_command {
|
||||||
|
|
@ -1575,7 +1598,13 @@ pub(crate) fn screen_thread_main(
|
||||||
},
|
},
|
||||||
ClientOrTabIndex::TabIndex(tab_index) => {
|
ClientOrTabIndex::TabIndex(tab_index) => {
|
||||||
if let Some(active_tab) = screen.tabs.get_mut(&tab_index) {
|
if let Some(active_tab) = screen.tabs.get_mut(&tab_index) {
|
||||||
active_tab.new_pane(pid, initial_pane_title, should_float, None)?;
|
active_tab.new_pane(
|
||||||
|
pid,
|
||||||
|
initial_pane_title,
|
||||||
|
should_float,
|
||||||
|
None,
|
||||||
|
None,
|
||||||
|
)?;
|
||||||
if let Some(hold_for_command) = hold_for_command {
|
if let Some(hold_for_command) = hold_for_command {
|
||||||
let is_first_run = true;
|
let is_first_run = true;
|
||||||
active_tab.hold_pane(pid, None, is_first_run, hold_for_command);
|
active_tab.hold_pane(pid, None, is_first_run, hold_for_command);
|
||||||
|
|
@ -2558,11 +2587,11 @@ pub(crate) fn screen_thread_main(
|
||||||
pane_title.unwrap_or_else(|| run_plugin_location.location.to_string());
|
pane_title.unwrap_or_else(|| run_plugin_location.location.to_string());
|
||||||
let run_plugin = Run::Plugin(run_plugin_location);
|
let run_plugin = Run::Plugin(run_plugin_location);
|
||||||
if let Some(active_tab) = screen.tabs.get_mut(&tab_index) {
|
if let Some(active_tab) = screen.tabs.get_mut(&tab_index) {
|
||||||
active_tab.new_plugin_pane(
|
active_tab.new_pane(
|
||||||
PaneId::Plugin(plugin_id),
|
PaneId::Plugin(plugin_id),
|
||||||
pane_title,
|
Some(pane_title),
|
||||||
should_float,
|
should_float,
|
||||||
run_plugin,
|
Some(run_plugin),
|
||||||
None,
|
None,
|
||||||
)?;
|
)?;
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -2608,6 +2637,46 @@ pub(crate) fn screen_thread_main(
|
||||||
screen.update_tabs()?;
|
screen.update_tabs()?;
|
||||||
screen.render()?;
|
screen.render()?;
|
||||||
},
|
},
|
||||||
|
ScreenInstruction::LaunchOrFocusPlugin(run_plugin, should_float, client_id) => {
|
||||||
|
let client_id = if screen.active_tab_indices.contains_key(&client_id) {
|
||||||
|
Some(client_id)
|
||||||
|
} else {
|
||||||
|
screen.get_first_client_id()
|
||||||
|
};
|
||||||
|
let client_id_and_focused_tab = client_id.and_then(|client_id| {
|
||||||
|
screen
|
||||||
|
.active_tab_indices
|
||||||
|
.get(&client_id)
|
||||||
|
.map(|tab_index| (*tab_index, client_id))
|
||||||
|
});
|
||||||
|
match client_id_and_focused_tab {
|
||||||
|
Some((tab_index, client_id)) => {
|
||||||
|
if screen.focus_plugin_pane(&run_plugin, should_float, client_id)? {
|
||||||
|
screen.render()?;
|
||||||
|
} else {
|
||||||
|
screen.bus.senders.send_to_plugin(PluginInstruction::Load(
|
||||||
|
Some(should_float),
|
||||||
|
None,
|
||||||
|
run_plugin,
|
||||||
|
tab_index,
|
||||||
|
client_id,
|
||||||
|
Size::default(),
|
||||||
|
))?;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
None => log::error!("No connected clients found - cannot load or focus plugin"),
|
||||||
|
}
|
||||||
|
},
|
||||||
|
ScreenInstruction::SuppressPane(pane_id, client_id) => {
|
||||||
|
let all_tabs = screen.get_tabs_mut();
|
||||||
|
for tab in all_tabs.values_mut() {
|
||||||
|
if tab.has_pane_with_pid(&pane_id) {
|
||||||
|
tab.suppress_pane(pane_id, client_id);
|
||||||
|
drop(screen.render());
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
|
|
|
||||||
|
|
@ -47,8 +47,8 @@ use zellij_utils::{
|
||||||
input::{
|
input::{
|
||||||
command::TerminalAction,
|
command::TerminalAction,
|
||||||
layout::{
|
layout::{
|
||||||
FloatingPaneLayout, Run, RunPluginLocation, SwapFloatingLayout, SwapTiledLayout,
|
FloatingPaneLayout, Run, RunPlugin, RunPluginLocation, SwapFloatingLayout,
|
||||||
TiledPaneLayout,
|
SwapTiledLayout, TiledPaneLayout,
|
||||||
},
|
},
|
||||||
parse_keys,
|
parse_keys,
|
||||||
},
|
},
|
||||||
|
|
@ -901,7 +901,6 @@ impl Tab {
|
||||||
pub fn toggle_pane_embed_or_floating(&mut self, client_id: ClientId) -> Result<()> {
|
pub fn toggle_pane_embed_or_floating(&mut self, client_id: ClientId) -> Result<()> {
|
||||||
let err_context =
|
let err_context =
|
||||||
|| format!("failed to toggle embedded/floating pane for client {client_id}");
|
|| format!("failed to toggle embedded/floating pane for client {client_id}");
|
||||||
|
|
||||||
if self.tiled_panes.fullscreen_is_active() {
|
if self.tiled_panes.fullscreen_is_active() {
|
||||||
self.tiled_panes.unset_fullscreen();
|
self.tiled_panes.unset_fullscreen();
|
||||||
}
|
}
|
||||||
|
|
@ -914,55 +913,24 @@ impl Tab {
|
||||||
"failed to find floating pane (ID: {focused_floating_pane_id:?}) to embed for client {client_id}",
|
"failed to find floating pane (ID: {focused_floating_pane_id:?}) to embed for client {client_id}",
|
||||||
))
|
))
|
||||||
.with_context(err_context)?;
|
.with_context(err_context)?;
|
||||||
self.tiled_panes
|
|
||||||
.insert_pane(focused_floating_pane_id, floating_pane_to_embed);
|
|
||||||
self.should_clear_display_before_rendering = true;
|
|
||||||
self.tiled_panes
|
|
||||||
.focus_pane(focused_floating_pane_id, client_id);
|
|
||||||
self.hide_floating_panes();
|
self.hide_floating_panes();
|
||||||
if self.auto_layout && !self.swap_layouts.is_tiled_damaged() {
|
self.add_tiled_pane(
|
||||||
// only do this if we're already in this layout, otherwise it might be
|
floating_pane_to_embed,
|
||||||
// confusing and not what the user intends
|
focused_floating_pane_id,
|
||||||
self.swap_layouts.set_is_tiled_damaged(); // we do this so that we won't skip to the
|
Some(client_id),
|
||||||
// next layout
|
)?;
|
||||||
self.next_swap_layout(Some(client_id), true)?;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else if let Some(focused_pane_id) = self.tiled_panes.focused_pane_id(client_id) {
|
} else if let Some(focused_pane_id) = self.tiled_panes.focused_pane_id(client_id) {
|
||||||
if let Some(new_pane_geom) = self.floating_panes.find_room_for_new_pane() {
|
|
||||||
if self.get_selectable_tiled_panes().count() <= 1 {
|
if self.get_selectable_tiled_panes().count() <= 1 {
|
||||||
// don't close the only pane on screen...
|
// don't close the only pane on screen...
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
if let Some(mut embedded_pane_to_float) =
|
if let Some(embedded_pane_to_float) =
|
||||||
self.close_pane(focused_pane_id, true, Some(client_id))
|
self.close_pane(focused_pane_id, true, Some(client_id))
|
||||||
{
|
{
|
||||||
if !embedded_pane_to_float.borderless() {
|
|
||||||
// floating panes always have a frame unless they're explicitly borderless
|
|
||||||
embedded_pane_to_float.set_content_offset(Offset::frame(1));
|
|
||||||
}
|
|
||||||
embedded_pane_to_float.set_geom(new_pane_geom);
|
|
||||||
resize_pty!(
|
|
||||||
embedded_pane_to_float,
|
|
||||||
self.os_api,
|
|
||||||
self.senders,
|
|
||||||
self.character_cell_size
|
|
||||||
)
|
|
||||||
.with_context(err_context)?;
|
|
||||||
embedded_pane_to_float.set_active_at(Instant::now());
|
|
||||||
self.floating_panes
|
|
||||||
.add_pane(focused_pane_id, embedded_pane_to_float);
|
|
||||||
self.floating_panes.focus_pane(focused_pane_id, client_id);
|
|
||||||
self.show_floating_panes();
|
self.show_floating_panes();
|
||||||
if self.auto_layout && !self.swap_layouts.is_floating_damaged() {
|
self.add_floating_pane(embedded_pane_to_float, focused_pane_id, Some(client_id))?;
|
||||||
// only do this if we're already in this layout, otherwise it might be
|
|
||||||
// confusing and not what the user intends
|
|
||||||
self.swap_layouts.set_is_floating_damaged(); // we do this so that we won't skip to the
|
|
||||||
// next layout
|
|
||||||
self.next_swap_layout(Some(client_id), true)?;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
|
|
@ -1018,10 +986,10 @@ impl Tab {
|
||||||
pid: PaneId,
|
pid: PaneId,
|
||||||
initial_pane_title: Option<String>,
|
initial_pane_title: Option<String>,
|
||||||
should_float: Option<bool>,
|
should_float: Option<bool>,
|
||||||
|
run_plugin: Option<Run>, // only relevant if this is a plugin pane
|
||||||
client_id: Option<ClientId>,
|
client_id: Option<ClientId>,
|
||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
let err_context = || format!("failed to create new pane with id {pid:?}");
|
let err_context = || format!("failed to create new pane with id {pid:?}");
|
||||||
|
|
||||||
match should_float {
|
match should_float {
|
||||||
Some(true) => self.show_floating_panes(),
|
Some(true) => self.show_floating_panes(),
|
||||||
Some(false) => self.hide_floating_panes(),
|
Some(false) => self.hide_floating_panes(),
|
||||||
|
|
@ -1029,13 +997,12 @@ impl Tab {
|
||||||
};
|
};
|
||||||
self.close_down_to_max_terminals()
|
self.close_down_to_max_terminals()
|
||||||
.with_context(err_context)?;
|
.with_context(err_context)?;
|
||||||
if self.floating_panes.panes_are_visible() {
|
let new_pane = match pid {
|
||||||
if let Some(new_pane_geom) = self.floating_panes.find_room_for_new_pane() {
|
PaneId::Terminal(term_pid) => {
|
||||||
let next_terminal_position = self.get_next_terminal_position();
|
let next_terminal_position = self.get_next_terminal_position();
|
||||||
if let PaneId::Terminal(term_pid) = pid {
|
Box::new(TerminalPane::new(
|
||||||
let mut new_pane = TerminalPane::new(
|
|
||||||
term_pid,
|
term_pid,
|
||||||
new_pane_geom,
|
PaneGeom::default(), // this will be filled out later
|
||||||
self.style,
|
self.style,
|
||||||
next_terminal_position,
|
next_terminal_position,
|
||||||
String::new(),
|
String::new(),
|
||||||
|
|
@ -1046,181 +1013,35 @@ impl Tab {
|
||||||
self.terminal_emulator_color_codes.clone(),
|
self.terminal_emulator_color_codes.clone(),
|
||||||
initial_pane_title,
|
initial_pane_title,
|
||||||
None,
|
None,
|
||||||
);
|
)) as Box<dyn Pane>
|
||||||
new_pane.set_active_at(Instant::now());
|
},
|
||||||
new_pane.set_content_offset(Offset::frame(1)); // floating panes always have a frame
|
PaneId::Plugin(plugin_pid) => {
|
||||||
resize_pty!(
|
Box::new(PluginPane::new(
|
||||||
new_pane,
|
plugin_pid,
|
||||||
self.os_api,
|
PaneGeom::default(), // this will be filled out later
|
||||||
self.senders,
|
self.senders
|
||||||
self.character_cell_size
|
.to_plugin
|
||||||
)
|
.as_ref()
|
||||||
.with_context(err_context)?;
|
.with_context(err_context)?
|
||||||
self.floating_panes.add_pane(pid, Box::new(new_pane));
|
.clone(),
|
||||||
self.floating_panes.focus_pane_for_all_clients(pid);
|
initial_pane_title.unwrap_or("".to_owned()),
|
||||||
}
|
|
||||||
if self.auto_layout && !self.swap_layouts.is_floating_damaged() {
|
|
||||||
// only do this if we're already in this layout, otherwise it might be
|
|
||||||
// confusing and not what the user intends
|
|
||||||
self.swap_layouts.set_is_floating_damaged(); // we do this so that we won't skip to the
|
|
||||||
// next layout
|
|
||||||
self.next_swap_layout(client_id, true)?;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if self.tiled_panes.fullscreen_is_active() {
|
|
||||||
self.tiled_panes.unset_fullscreen();
|
|
||||||
}
|
|
||||||
let should_auto_layout = self.auto_layout && !self.swap_layouts.is_tiled_damaged();
|
|
||||||
if self.tiled_panes.has_room_for_new_pane() {
|
|
||||||
if let PaneId::Terminal(term_pid) = pid {
|
|
||||||
let next_terminal_position = self.get_next_terminal_position();
|
|
||||||
let mut new_terminal = TerminalPane::new(
|
|
||||||
term_pid,
|
|
||||||
PaneGeom::default(), // the initial size will be set later
|
|
||||||
self.style,
|
|
||||||
next_terminal_position,
|
|
||||||
String::new(),
|
String::new(),
|
||||||
self.link_handler.clone(),
|
|
||||||
self.character_cell_size.clone(),
|
|
||||||
self.sixel_image_store.clone(),
|
self.sixel_image_store.clone(),
|
||||||
self.terminal_emulator_colors.clone(),
|
self.terminal_emulator_colors.clone(),
|
||||||
self.terminal_emulator_color_codes.clone(),
|
self.terminal_emulator_color_codes.clone(),
|
||||||
initial_pane_title,
|
self.link_handler.clone(),
|
||||||
None,
|
self.character_cell_size.clone(),
|
||||||
);
|
self.connected_clients.borrow().iter().copied().collect(),
|
||||||
new_terminal.set_active_at(Instant::now());
|
self.style,
|
||||||
if should_auto_layout {
|
run_plugin,
|
||||||
// no need to relayout here, we'll do it when reapplying the swap layout
|
)) as Box<dyn Pane>
|
||||||
// below
|
},
|
||||||
self.tiled_panes
|
|
||||||
.insert_pane_without_relayout(pid, Box::new(new_terminal));
|
|
||||||
} else {
|
|
||||||
self.tiled_panes.insert_pane(pid, Box::new(new_terminal));
|
|
||||||
}
|
|
||||||
self.should_clear_display_before_rendering = true;
|
|
||||||
if let Some(client_id) = client_id {
|
|
||||||
self.tiled_panes.focus_pane(pid, client_id);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if should_auto_layout {
|
|
||||||
// only do this if we're already in this layout, otherwise it might be
|
|
||||||
// confusing and not what the user intends
|
|
||||||
self.swap_layouts.set_is_tiled_damaged(); // we do this so that we won't skip to the
|
|
||||||
// next layout
|
|
||||||
self.next_swap_layout(client_id, true)?;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
pub fn new_plugin_pane(
|
|
||||||
&mut self,
|
|
||||||
pid: PaneId,
|
|
||||||
initial_pane_title: String,
|
|
||||||
should_float: Option<bool>,
|
|
||||||
run_plugin: Run,
|
|
||||||
client_id: Option<ClientId>,
|
|
||||||
) -> Result<()> {
|
|
||||||
let err_context = || format!("failed to create new pane with id {pid:?}");
|
|
||||||
|
|
||||||
match should_float {
|
|
||||||
Some(true) => self.show_floating_panes(),
|
|
||||||
Some(false) => self.hide_floating_panes(),
|
|
||||||
None => {},
|
|
||||||
};
|
};
|
||||||
if self.floating_panes.panes_are_visible() {
|
if self.floating_panes.panes_are_visible() {
|
||||||
if let Some(new_pane_geom) = self.floating_panes.find_room_for_new_pane() {
|
self.add_floating_pane(new_pane, pid, client_id)
|
||||||
if let PaneId::Plugin(plugin_pid) = pid {
|
|
||||||
let mut new_pane = PluginPane::new(
|
|
||||||
plugin_pid,
|
|
||||||
new_pane_geom,
|
|
||||||
self.senders
|
|
||||||
.to_plugin
|
|
||||||
.as_ref()
|
|
||||||
.with_context(err_context)?
|
|
||||||
.clone(),
|
|
||||||
initial_pane_title,
|
|
||||||
String::new(),
|
|
||||||
self.sixel_image_store.clone(),
|
|
||||||
self.terminal_emulator_colors.clone(),
|
|
||||||
self.terminal_emulator_color_codes.clone(),
|
|
||||||
self.link_handler.clone(),
|
|
||||||
self.character_cell_size.clone(),
|
|
||||||
self.connected_clients.borrow().iter().copied().collect(),
|
|
||||||
self.style,
|
|
||||||
Some(run_plugin),
|
|
||||||
);
|
|
||||||
new_pane.set_active_at(Instant::now());
|
|
||||||
new_pane.set_content_offset(Offset::frame(1)); // floating panes always have a frame
|
|
||||||
resize_pty!(
|
|
||||||
new_pane,
|
|
||||||
self.os_api,
|
|
||||||
self.senders,
|
|
||||||
self.character_cell_size
|
|
||||||
)
|
|
||||||
.with_context(err_context)?;
|
|
||||||
self.floating_panes.add_pane(pid, Box::new(new_pane));
|
|
||||||
self.floating_panes.focus_pane_for_all_clients(pid);
|
|
||||||
}
|
|
||||||
if self.auto_layout && !self.swap_layouts.is_floating_damaged() {
|
|
||||||
// only do this if we're already in this layout, otherwise it might be
|
|
||||||
// confusing and not what the user intends
|
|
||||||
self.swap_layouts.set_is_floating_damaged(); // we do this so that we won't skip to the
|
|
||||||
// next layout
|
|
||||||
self.next_swap_layout(client_id, true)?;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
if self.tiled_panes.fullscreen_is_active() {
|
self.add_tiled_pane(new_pane, pid, client_id)
|
||||||
self.tiled_panes.unset_fullscreen();
|
|
||||||
}
|
}
|
||||||
let should_auto_layout = self.auto_layout && !self.swap_layouts.is_tiled_damaged();
|
|
||||||
if self.tiled_panes.has_room_for_new_pane() {
|
|
||||||
if let PaneId::Plugin(plugin_pid) = pid {
|
|
||||||
let mut new_pane = PluginPane::new(
|
|
||||||
plugin_pid,
|
|
||||||
PaneGeom::default(), // the initial size will be set later
|
|
||||||
self.senders
|
|
||||||
.to_plugin
|
|
||||||
.as_ref()
|
|
||||||
.with_context(err_context)?
|
|
||||||
.clone(),
|
|
||||||
initial_pane_title,
|
|
||||||
String::new(),
|
|
||||||
self.sixel_image_store.clone(),
|
|
||||||
self.terminal_emulator_colors.clone(),
|
|
||||||
self.terminal_emulator_color_codes.clone(),
|
|
||||||
self.link_handler.clone(),
|
|
||||||
self.character_cell_size.clone(),
|
|
||||||
self.connected_clients.borrow().iter().copied().collect(),
|
|
||||||
self.style,
|
|
||||||
Some(run_plugin),
|
|
||||||
);
|
|
||||||
new_pane.set_active_at(Instant::now());
|
|
||||||
if should_auto_layout {
|
|
||||||
// no need to relayout here, we'll do it when reapplying the swap layout
|
|
||||||
// below
|
|
||||||
self.tiled_panes
|
|
||||||
.insert_pane_without_relayout(pid, Box::new(new_pane));
|
|
||||||
} else {
|
|
||||||
self.tiled_panes.insert_pane(pid, Box::new(new_pane));
|
|
||||||
}
|
|
||||||
self.should_clear_display_before_rendering = true;
|
|
||||||
if let Some(client_id) = client_id {
|
|
||||||
self.tiled_panes.focus_pane(pid, client_id);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if should_auto_layout {
|
|
||||||
// only do this if we're already in this layout, otherwise it might be
|
|
||||||
// confusing and not what the user intends
|
|
||||||
self.swap_layouts.set_is_tiled_damaged(); // we do this so that we won't skip to the
|
|
||||||
// next layout
|
|
||||||
self.next_swap_layout(client_id, true)?;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Ok(())
|
|
||||||
}
|
}
|
||||||
pub fn suppress_active_pane(&mut self, pid: PaneId, client_id: ClientId) -> Result<()> {
|
pub fn suppress_active_pane(&mut self, pid: PaneId, client_id: ClientId) -> Result<()> {
|
||||||
// this method creates a new pane from pid and replaces it with the active pane
|
// this method creates a new pane from pid and replaces it with the active pane
|
||||||
|
|
@ -3426,6 +3247,112 @@ impl Tab {
|
||||||
self.floating_panes.toggle_show_panes(false);
|
self.floating_panes.toggle_show_panes(false);
|
||||||
self.tiled_panes.focus_all_panes();
|
self.tiled_panes.focus_all_panes();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn find_plugin(&self, run_plugin: &RunPlugin) -> Option<PaneId> {
|
||||||
|
self.tiled_panes
|
||||||
|
.get_plugin_pane_id(run_plugin)
|
||||||
|
.or_else(|| self.floating_panes.get_plugin_pane_id(run_plugin))
|
||||||
|
.or_else(|| {
|
||||||
|
let run = Some(Run::Plugin(run_plugin.clone()));
|
||||||
|
self.suppressed_panes
|
||||||
|
.iter()
|
||||||
|
.find(|(_id, s_p)| s_p.invoked_with() == &run)
|
||||||
|
.map(|(id, _)| *id)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn focus_pane_with_id(
|
||||||
|
&mut self,
|
||||||
|
pane_id: PaneId,
|
||||||
|
should_float: bool,
|
||||||
|
client_id: ClientId,
|
||||||
|
) -> Result<()> {
|
||||||
|
self.tiled_panes
|
||||||
|
.focus_pane_if_exists(pane_id, client_id)
|
||||||
|
.or_else(|_| {
|
||||||
|
let focused_floating_pane =
|
||||||
|
self.floating_panes.focus_pane_if_exists(pane_id, client_id);
|
||||||
|
if focused_floating_pane.is_ok() {
|
||||||
|
self.show_floating_panes()
|
||||||
|
};
|
||||||
|
focused_floating_pane
|
||||||
|
})
|
||||||
|
.or_else(|_| match self.suppressed_panes.remove(&pane_id) {
|
||||||
|
Some(pane) => {
|
||||||
|
if should_float {
|
||||||
|
self.show_floating_panes();
|
||||||
|
self.add_floating_pane(pane, pane_id, Some(client_id))
|
||||||
|
} else {
|
||||||
|
self.hide_floating_panes();
|
||||||
|
self.add_tiled_pane(pane, pane_id, Some(client_id))
|
||||||
|
}
|
||||||
|
},
|
||||||
|
None => Ok(()),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
pub fn suppress_pane(&mut self, pane_id: PaneId, client_id: ClientId) {
|
||||||
|
if let Some(pane) = self.close_pane(pane_id, true, Some(client_id)) {
|
||||||
|
self.suppressed_panes.insert(pane_id, pane);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fn add_floating_pane(
|
||||||
|
&mut self,
|
||||||
|
mut pane: Box<dyn Pane>,
|
||||||
|
pane_id: PaneId,
|
||||||
|
client_id: Option<ClientId>,
|
||||||
|
) -> Result<()> {
|
||||||
|
let err_context = || format!("failed to add floating pane");
|
||||||
|
if let Some(new_pane_geom) = self.floating_panes.find_room_for_new_pane() {
|
||||||
|
pane.set_active_at(Instant::now());
|
||||||
|
pane.set_geom(new_pane_geom);
|
||||||
|
pane.set_content_offset(Offset::frame(1)); // floating panes always have a frame
|
||||||
|
resize_pty!(pane, self.os_api, self.senders, self.character_cell_size)
|
||||||
|
.with_context(err_context)?;
|
||||||
|
self.floating_panes.add_pane(pane_id, pane);
|
||||||
|
self.floating_panes.focus_pane_for_all_clients(pane_id);
|
||||||
|
}
|
||||||
|
if self.auto_layout && !self.swap_layouts.is_floating_damaged() {
|
||||||
|
// only do this if we're already in this layout, otherwise it might be
|
||||||
|
// confusing and not what the user intends
|
||||||
|
self.swap_layouts.set_is_floating_damaged(); // we do this so that we won't skip to the
|
||||||
|
// next layout
|
||||||
|
self.next_swap_layout(client_id, true)?;
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
fn add_tiled_pane(
|
||||||
|
&mut self,
|
||||||
|
mut pane: Box<dyn Pane>,
|
||||||
|
pane_id: PaneId,
|
||||||
|
client_id: Option<ClientId>,
|
||||||
|
) -> Result<()> {
|
||||||
|
if self.tiled_panes.fullscreen_is_active() {
|
||||||
|
self.tiled_panes.unset_fullscreen();
|
||||||
|
}
|
||||||
|
let should_auto_layout = self.auto_layout && !self.swap_layouts.is_tiled_damaged();
|
||||||
|
if self.tiled_panes.has_room_for_new_pane() {
|
||||||
|
pane.set_active_at(Instant::now());
|
||||||
|
if should_auto_layout {
|
||||||
|
// no need to relayout here, we'll do it when reapplying the swap layout
|
||||||
|
// below
|
||||||
|
self.tiled_panes.insert_pane_without_relayout(pane_id, pane);
|
||||||
|
} else {
|
||||||
|
self.tiled_panes.insert_pane(pane_id, pane);
|
||||||
|
}
|
||||||
|
self.should_clear_display_before_rendering = true;
|
||||||
|
if let Some(client_id) = client_id {
|
||||||
|
self.tiled_panes.focus_pane(pane_id, client_id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if should_auto_layout {
|
||||||
|
// only do this if we're already in this layout, otherwise it might be
|
||||||
|
// confusing and not what the user intends
|
||||||
|
self.swap_layouts.set_is_tiled_damaged(); // we do this so that we won't skip to the
|
||||||
|
// next layout
|
||||||
|
self.next_swap_layout(client_id, true)?;
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
|
|
|
||||||
File diff suppressed because it is too large
Load diff
|
|
@ -537,7 +537,8 @@ fn split_largest_pane() {
|
||||||
let mut tab = create_new_tab(size);
|
let mut tab = create_new_tab(size);
|
||||||
for i in 2..5 {
|
for i in 2..5 {
|
||||||
let new_pane_id = PaneId::Terminal(i);
|
let new_pane_id = PaneId::Terminal(i);
|
||||||
tab.new_pane(new_pane_id, None, None, Some(1)).unwrap();
|
tab.new_pane(new_pane_id, None, None, None, Some(1))
|
||||||
|
.unwrap();
|
||||||
}
|
}
|
||||||
assert_eq!(tab.tiled_panes.panes.len(), 4, "The tab has four panes");
|
assert_eq!(tab.tiled_panes.panes.len(), 4, "The tab has four panes");
|
||||||
|
|
||||||
|
|
@ -742,7 +743,7 @@ pub fn cannot_split_panes_horizontally_when_active_pane_is_too_small() {
|
||||||
pub fn cannot_split_largest_pane_when_there_is_no_room() {
|
pub fn cannot_split_largest_pane_when_there_is_no_room() {
|
||||||
let size = Size { cols: 8, rows: 4 };
|
let size = Size { cols: 8, rows: 4 };
|
||||||
let mut tab = create_new_tab(size);
|
let mut tab = create_new_tab(size);
|
||||||
tab.new_pane(PaneId::Terminal(2), None, None, Some(1))
|
tab.new_pane(PaneId::Terminal(2), None, None, None, Some(1))
|
||||||
.unwrap();
|
.unwrap();
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
tab.tiled_panes.panes.len(),
|
tab.tiled_panes.panes.len(),
|
||||||
|
|
@ -786,7 +787,8 @@ pub fn toggle_focused_pane_fullscreen() {
|
||||||
let mut tab = create_new_tab(size);
|
let mut tab = create_new_tab(size);
|
||||||
for i in 2..5 {
|
for i in 2..5 {
|
||||||
let new_pane_id = PaneId::Terminal(i);
|
let new_pane_id = PaneId::Terminal(i);
|
||||||
tab.new_pane(new_pane_id, None, None, Some(1)).unwrap();
|
tab.new_pane(new_pane_id, None, None, None, Some(1))
|
||||||
|
.unwrap();
|
||||||
}
|
}
|
||||||
tab.toggle_active_pane_fullscreen(1);
|
tab.toggle_active_pane_fullscreen(1);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
|
|
@ -860,16 +862,16 @@ fn switch_to_next_pane_fullscreen() {
|
||||||
let mut active_tab = create_new_tab(size);
|
let mut active_tab = create_new_tab(size);
|
||||||
|
|
||||||
active_tab
|
active_tab
|
||||||
.new_pane(PaneId::Terminal(1), None, None, Some(1))
|
.new_pane(PaneId::Terminal(1), None, None, None, Some(1))
|
||||||
.unwrap();
|
.unwrap();
|
||||||
active_tab
|
active_tab
|
||||||
.new_pane(PaneId::Terminal(2), None, None, Some(1))
|
.new_pane(PaneId::Terminal(2), None, None, None, Some(1))
|
||||||
.unwrap();
|
.unwrap();
|
||||||
active_tab
|
active_tab
|
||||||
.new_pane(PaneId::Terminal(3), None, None, Some(1))
|
.new_pane(PaneId::Terminal(3), None, None, None, Some(1))
|
||||||
.unwrap();
|
.unwrap();
|
||||||
active_tab
|
active_tab
|
||||||
.new_pane(PaneId::Terminal(4), None, None, Some(1))
|
.new_pane(PaneId::Terminal(4), None, None, None, Some(1))
|
||||||
.unwrap();
|
.unwrap();
|
||||||
active_tab.toggle_active_pane_fullscreen(1);
|
active_tab.toggle_active_pane_fullscreen(1);
|
||||||
|
|
||||||
|
|
@ -900,16 +902,16 @@ fn switch_to_prev_pane_fullscreen() {
|
||||||
//testing four consecutive switches in fullscreen mode
|
//testing four consecutive switches in fullscreen mode
|
||||||
|
|
||||||
active_tab
|
active_tab
|
||||||
.new_pane(PaneId::Terminal(1), None, None, Some(1))
|
.new_pane(PaneId::Terminal(1), None, None, None, Some(1))
|
||||||
.unwrap();
|
.unwrap();
|
||||||
active_tab
|
active_tab
|
||||||
.new_pane(PaneId::Terminal(2), None, None, Some(1))
|
.new_pane(PaneId::Terminal(2), None, None, None, Some(1))
|
||||||
.unwrap();
|
.unwrap();
|
||||||
active_tab
|
active_tab
|
||||||
.new_pane(PaneId::Terminal(3), None, None, Some(1))
|
.new_pane(PaneId::Terminal(3), None, None, None, Some(1))
|
||||||
.unwrap();
|
.unwrap();
|
||||||
active_tab
|
active_tab
|
||||||
.new_pane(PaneId::Terminal(4), None, None, Some(1))
|
.new_pane(PaneId::Terminal(4), None, None, None, Some(1))
|
||||||
.unwrap();
|
.unwrap();
|
||||||
active_tab.toggle_active_pane_fullscreen(1);
|
active_tab.toggle_active_pane_fullscreen(1);
|
||||||
// order is now 1 2 3 4
|
// order is now 1 2 3 4
|
||||||
|
|
@ -14391,7 +14393,7 @@ fn correctly_resize_frameless_panes_on_pane_close() {
|
||||||
let content_size = (pane.get_content_columns(), pane.get_content_rows());
|
let content_size = (pane.get_content_columns(), pane.get_content_rows());
|
||||||
assert_eq!(content_size, (cols, rows));
|
assert_eq!(content_size, (cols, rows));
|
||||||
|
|
||||||
tab.new_pane(PaneId::Terminal(2), None, None, Some(1))
|
tab.new_pane(PaneId::Terminal(2), None, None, None, Some(1))
|
||||||
.unwrap();
|
.unwrap();
|
||||||
tab.close_pane(PaneId::Terminal(2), true, None);
|
tab.close_pane(PaneId::Terminal(2), true, None);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -45,9 +45,15 @@ impl LoadingIndication {
|
||||||
pub fn merge(&mut self, other: LoadingIndication) {
|
pub fn merge(&mut self, other: LoadingIndication) {
|
||||||
let current_animation_offset = self.animation_offset;
|
let current_animation_offset = self.animation_offset;
|
||||||
let current_terminal_emulator_colors = self.terminal_emulator_colors.take();
|
let current_terminal_emulator_colors = self.terminal_emulator_colors.take();
|
||||||
|
let mut current_error = self.error.take();
|
||||||
drop(std::mem::replace(self, other));
|
drop(std::mem::replace(self, other));
|
||||||
self.animation_offset = current_animation_offset;
|
self.animation_offset = current_animation_offset;
|
||||||
self.terminal_emulator_colors = current_terminal_emulator_colors;
|
self.terminal_emulator_colors = current_terminal_emulator_colors;
|
||||||
|
if let Some(current_error) = current_error.take() {
|
||||||
|
// we do this so that only the first error (usually the root cause) will be shown
|
||||||
|
// when plugins support scrolling, we might want to do an append here
|
||||||
|
self.error = Some(current_error);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
pub fn indicate_loading_plugin_from_memory(&mut self) {
|
pub fn indicate_loading_plugin_from_memory(&mut self) {
|
||||||
self.loading_from_memory = Some(LoadingStatus::InProgress);
|
self.loading_from_memory = Some(LoadingStatus::InProgress);
|
||||||
|
|
@ -104,6 +110,9 @@ impl LoadingIndication {
|
||||||
pub fn indicate_loading_error(&mut self, error_text: String) {
|
pub fn indicate_loading_error(&mut self, error_text: String) {
|
||||||
self.error = Some(error_text);
|
self.error = Some(error_text);
|
||||||
}
|
}
|
||||||
|
pub fn is_error(&self) -> bool {
|
||||||
|
self.error.is_some()
|
||||||
|
}
|
||||||
fn started_loading(&self) -> bool {
|
fn started_loading(&self) -> bool {
|
||||||
self.loading_from_memory.is_some()
|
self.loading_from_memory.is_some()
|
||||||
|| self.loading_from_hd_cache.is_some()
|
|| self.loading_from_hd_cache.is_some()
|
||||||
|
|
@ -257,6 +266,13 @@ impl Display for LoadingIndication {
|
||||||
}
|
}
|
||||||
if let Some(error_text) = &self.error {
|
if let Some(error_text) = &self.error {
|
||||||
stringified.push_str(&format!("\n\r{} {error_text}", red.bold().paint("ERROR:")));
|
stringified.push_str(&format!("\n\r{} {error_text}", red.bold().paint("ERROR:")));
|
||||||
|
// we add this additional line explicitly to make it easier to realize when something
|
||||||
|
// is wrong in very small plugins (eg. the tab-bar and status-bar)
|
||||||
|
stringified.push_str(&format!(
|
||||||
|
"\n\r{}",
|
||||||
|
red.bold()
|
||||||
|
.paint("ERROR IN PLUGIN - check logs for more info")
|
||||||
|
));
|
||||||
}
|
}
|
||||||
write!(f, "{}", stringified)
|
write!(f, "{}", stringified)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -14,7 +14,9 @@ use zellij_utils::data::Resize;
|
||||||
use zellij_utils::errors::{prelude::*, ErrorContext};
|
use zellij_utils::errors::{prelude::*, ErrorContext};
|
||||||
use zellij_utils::input::actions::Action;
|
use zellij_utils::input::actions::Action;
|
||||||
use zellij_utils::input::command::{RunCommand, TerminalAction};
|
use zellij_utils::input::command::{RunCommand, TerminalAction};
|
||||||
use zellij_utils::input::layout::{Layout, SplitDirection, TiledPaneLayout};
|
use zellij_utils::input::layout::{
|
||||||
|
Layout, Run, RunPlugin, RunPluginLocation, SplitDirection, TiledPaneLayout,
|
||||||
|
};
|
||||||
use zellij_utils::input::options::Options;
|
use zellij_utils::input::options::Options;
|
||||||
use zellij_utils::ipc::IpcReceiverWithContext;
|
use zellij_utils::ipc::IpcReceiverWithContext;
|
||||||
use zellij_utils::pane_size::{Size, SizeInPixels};
|
use zellij_utils::pane_size::{Size, SizeInPixels};
|
||||||
|
|
@ -291,7 +293,11 @@ impl MockScreen {
|
||||||
let pane_layout = initial_layout.unwrap_or_default();
|
let pane_layout = initial_layout.unwrap_or_default();
|
||||||
let pane_count = pane_layout.extract_run_instructions().len();
|
let pane_count = pane_layout.extract_run_instructions().len();
|
||||||
let mut pane_ids = vec![];
|
let mut pane_ids = vec![];
|
||||||
let plugin_ids = HashMap::new();
|
let mut plugin_ids = HashMap::new();
|
||||||
|
plugin_ids.insert(
|
||||||
|
RunPluginLocation::File(PathBuf::from("/path/to/fake/plugin")),
|
||||||
|
vec![1],
|
||||||
|
);
|
||||||
for i in 0..pane_count {
|
for i in 0..pane_count {
|
||||||
pane_ids.push((i as u32, None));
|
pane_ids.push((i as u32, None));
|
||||||
}
|
}
|
||||||
|
|
@ -864,7 +870,7 @@ fn switch_to_tab_with_fullscreen() {
|
||||||
{
|
{
|
||||||
let active_tab = screen.get_active_tab_mut(1).unwrap();
|
let active_tab = screen.get_active_tab_mut(1).unwrap();
|
||||||
active_tab
|
active_tab
|
||||||
.new_pane(PaneId::Terminal(2), None, None, Some(1))
|
.new_pane(PaneId::Terminal(2), None, None, None, Some(1))
|
||||||
.unwrap();
|
.unwrap();
|
||||||
active_tab.toggle_active_pane_fullscreen(1);
|
active_tab.toggle_active_pane_fullscreen(1);
|
||||||
}
|
}
|
||||||
|
|
@ -979,7 +985,7 @@ fn attach_after_first_tab_closed() {
|
||||||
{
|
{
|
||||||
let active_tab = screen.get_active_tab_mut(1).unwrap();
|
let active_tab = screen.get_active_tab_mut(1).unwrap();
|
||||||
active_tab
|
active_tab
|
||||||
.new_pane(PaneId::Terminal(2), None, None, Some(1))
|
.new_pane(PaneId::Terminal(2), None, None, None, Some(1))
|
||||||
.unwrap();
|
.unwrap();
|
||||||
active_tab.toggle_active_pane_fullscreen(1);
|
active_tab.toggle_active_pane_fullscreen(1);
|
||||||
}
|
}
|
||||||
|
|
@ -2715,3 +2721,145 @@ pub fn send_cli_query_tab_names_action() {
|
||||||
.cloned();
|
.cloned();
|
||||||
assert_snapshot!(format!("{:#?}", log_tab_names_instruction));
|
assert_snapshot!(format!("{:#?}", log_tab_names_instruction));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
pub fn send_cli_launch_or_focus_plugin_action() {
|
||||||
|
let size = Size {
|
||||||
|
cols: 121,
|
||||||
|
rows: 20,
|
||||||
|
};
|
||||||
|
let client_id = 10; // fake client id should not appear in the screen's state
|
||||||
|
let mut mock_screen = MockScreen::new(size);
|
||||||
|
let plugin_receiver = mock_screen.plugin_receiver.take().unwrap();
|
||||||
|
let session_metadata = mock_screen.clone_session_metadata();
|
||||||
|
let screen_thread = mock_screen.run(None);
|
||||||
|
let received_plugin_instructions = Arc::new(Mutex::new(vec![]));
|
||||||
|
let plugin_thread = log_actions_in_thread!(
|
||||||
|
received_plugin_instructions,
|
||||||
|
PluginInstruction::Exit,
|
||||||
|
plugin_receiver
|
||||||
|
);
|
||||||
|
let cli_action = CliAction::LaunchOrFocusPlugin {
|
||||||
|
floating: true,
|
||||||
|
url: url::Url::parse("file:/path/to/fake/plugin").unwrap(),
|
||||||
|
};
|
||||||
|
send_cli_action_to_server(&session_metadata, cli_action, &mut mock_screen, client_id);
|
||||||
|
std::thread::sleep(std::time::Duration::from_millis(100)); // give time for actions to be
|
||||||
|
mock_screen.teardown(vec![plugin_thread, screen_thread]);
|
||||||
|
|
||||||
|
let plugin_load_instruction = received_plugin_instructions
|
||||||
|
.lock()
|
||||||
|
.unwrap()
|
||||||
|
.iter()
|
||||||
|
.find(|instruction| match instruction {
|
||||||
|
PluginInstruction::Load(..) => true,
|
||||||
|
_ => false,
|
||||||
|
})
|
||||||
|
.cloned();
|
||||||
|
|
||||||
|
assert_snapshot!(format!("{:#?}", plugin_load_instruction));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
pub fn send_cli_launch_or_focus_plugin_action_when_plugin_is_already_loaded() {
|
||||||
|
let size = Size {
|
||||||
|
cols: 121,
|
||||||
|
rows: 20,
|
||||||
|
};
|
||||||
|
let client_id = 10; // fake client id should not appear in the screen's state
|
||||||
|
let mut mock_screen = MockScreen::new(size);
|
||||||
|
let plugin_receiver = mock_screen.plugin_receiver.take().unwrap();
|
||||||
|
let session_metadata = mock_screen.clone_session_metadata();
|
||||||
|
let mut initial_layout = TiledPaneLayout::default();
|
||||||
|
let existing_plugin_pane = TiledPaneLayout {
|
||||||
|
run: Some(Run::Plugin(RunPlugin {
|
||||||
|
_allow_exec_host_cmd: false,
|
||||||
|
location: RunPluginLocation::File(PathBuf::from("/path/to/fake/plugin")),
|
||||||
|
})),
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
initial_layout.children_split_direction = SplitDirection::Vertical;
|
||||||
|
initial_layout.children = vec![TiledPaneLayout::default(), existing_plugin_pane];
|
||||||
|
let screen_thread = mock_screen.run(Some(initial_layout));
|
||||||
|
let received_plugin_instructions = Arc::new(Mutex::new(vec![]));
|
||||||
|
let plugin_thread = log_actions_in_thread!(
|
||||||
|
received_plugin_instructions,
|
||||||
|
PluginInstruction::Exit,
|
||||||
|
plugin_receiver
|
||||||
|
);
|
||||||
|
let received_server_instructions = Arc::new(Mutex::new(vec![]));
|
||||||
|
let server_receiver = mock_screen.server_receiver.take().unwrap();
|
||||||
|
let server_thread = log_actions_in_thread!(
|
||||||
|
received_server_instructions,
|
||||||
|
ServerInstruction::KillSession,
|
||||||
|
server_receiver
|
||||||
|
);
|
||||||
|
let cli_action = CliAction::LaunchOrFocusPlugin {
|
||||||
|
floating: true,
|
||||||
|
url: url::Url::parse("file:/path/to/fake/plugin").unwrap(),
|
||||||
|
};
|
||||||
|
send_cli_action_to_server(&session_metadata, cli_action, &mut mock_screen, client_id);
|
||||||
|
std::thread::sleep(std::time::Duration::from_millis(100)); // give time for actions to be
|
||||||
|
mock_screen.teardown(vec![plugin_thread, server_thread, screen_thread]);
|
||||||
|
|
||||||
|
let plugin_load_instruction_sent = received_plugin_instructions
|
||||||
|
.lock()
|
||||||
|
.unwrap()
|
||||||
|
.iter()
|
||||||
|
.find(|instruction| match instruction {
|
||||||
|
PluginInstruction::Load(..) => true,
|
||||||
|
_ => false,
|
||||||
|
})
|
||||||
|
.is_some();
|
||||||
|
assert!(
|
||||||
|
!plugin_load_instruction_sent,
|
||||||
|
"Plugin Load instruction should not be sent for an already loaded plugin"
|
||||||
|
);
|
||||||
|
let snapshots = take_snapshots_and_cursor_coordinates_from_render_events(
|
||||||
|
received_server_instructions.lock().unwrap().iter(),
|
||||||
|
size,
|
||||||
|
);
|
||||||
|
let snapshot_count = snapshots.len();
|
||||||
|
assert_eq!(
|
||||||
|
snapshot_count, 2,
|
||||||
|
"Another render was sent for focusing the already loaded plugin"
|
||||||
|
);
|
||||||
|
for (cursor_coordinates, _snapshot) in snapshots.iter().skip(1) {
|
||||||
|
assert!(
|
||||||
|
cursor_coordinates.is_none(),
|
||||||
|
"Cursor moved to existing plugin in final snapshot indicating focus changed"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
pub fn screen_can_suppress_pane() {
|
||||||
|
let size = Size { cols: 80, rows: 20 };
|
||||||
|
let mut initial_layout = TiledPaneLayout::default();
|
||||||
|
initial_layout.children_split_direction = SplitDirection::Vertical;
|
||||||
|
initial_layout.children = vec![TiledPaneLayout::default(), TiledPaneLayout::default()];
|
||||||
|
let mut mock_screen = MockScreen::new(size);
|
||||||
|
let screen_thread = mock_screen.run(Some(initial_layout));
|
||||||
|
let received_server_instructions = Arc::new(Mutex::new(vec![]));
|
||||||
|
let server_receiver = mock_screen.server_receiver.take().unwrap();
|
||||||
|
let server_thread = log_actions_in_thread!(
|
||||||
|
received_server_instructions,
|
||||||
|
ServerInstruction::KillSession,
|
||||||
|
server_receiver
|
||||||
|
);
|
||||||
|
let _ = mock_screen
|
||||||
|
.to_screen
|
||||||
|
.send(ScreenInstruction::SuppressPane(PaneId::Terminal(1), 1));
|
||||||
|
std::thread::sleep(std::time::Duration::from_millis(100));
|
||||||
|
mock_screen.teardown(vec![server_thread, screen_thread]);
|
||||||
|
|
||||||
|
let snapshots = take_snapshots_and_cursor_coordinates_from_render_events(
|
||||||
|
received_server_instructions.lock().unwrap().iter(),
|
||||||
|
size,
|
||||||
|
);
|
||||||
|
let snapshot_count = snapshots.len();
|
||||||
|
for (_cursor_coordinates, snapshot) in snapshots {
|
||||||
|
assert_snapshot!(format!("{}", snapshot));
|
||||||
|
}
|
||||||
|
assert_snapshot!(format!("{}", snapshot_count));
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,26 @@
|
||||||
|
---
|
||||||
|
source: zellij-server/src/./unit/screen_tests.rs
|
||||||
|
assertion_line: 2852
|
||||||
|
expression: "format!(\"{}\", snapshot)"
|
||||||
|
---
|
||||||
|
00 (C): ┌ Pane #1 ─────────────────────────────────────────────────────────────────────┐
|
||||||
|
01 (C): │ │
|
||||||
|
02 (C): │ │
|
||||||
|
03 (C): │ │
|
||||||
|
04 (C): │ │
|
||||||
|
05 (C): │ │
|
||||||
|
06 (C): │ │
|
||||||
|
07 (C): │ │
|
||||||
|
08 (C): │ │
|
||||||
|
09 (C): │ │
|
||||||
|
10 (C): │ │
|
||||||
|
11 (C): │ │
|
||||||
|
12 (C): │ │
|
||||||
|
13 (C): │ │
|
||||||
|
14 (C): │ │
|
||||||
|
15 (C): │ │
|
||||||
|
16 (C): │ │
|
||||||
|
17 (C): │ │
|
||||||
|
18 (C): │ │
|
||||||
|
19 (C): └──────────────────────────────────────────────────────────────────────────────┘
|
||||||
|
|
||||||
|
|
@ -0,0 +1,6 @@
|
||||||
|
---
|
||||||
|
source: zellij-server/src/./unit/screen_tests.rs
|
||||||
|
assertion_line: 2854
|
||||||
|
expression: "format!(\"{}\", snapshot_count)"
|
||||||
|
---
|
||||||
|
2
|
||||||
|
|
@ -0,0 +1,26 @@
|
||||||
|
---
|
||||||
|
source: zellij-server/src/./unit/screen_tests.rs
|
||||||
|
assertion_line: 2852
|
||||||
|
expression: "format!(\"{}\", snapshot)"
|
||||||
|
---
|
||||||
|
00 (C): ┌ Pane #1 ─────────────────────────────┐┌ Pane #2 ─────────────────────────────┐
|
||||||
|
01 (C): │ ││ │
|
||||||
|
02 (C): │ ││ │
|
||||||
|
03 (C): │ ││ │
|
||||||
|
04 (C): │ ││ │
|
||||||
|
05 (C): │ ││ │
|
||||||
|
06 (C): │ ││ │
|
||||||
|
07 (C): │ ││ │
|
||||||
|
08 (C): │ ││ │
|
||||||
|
09 (C): │ ││ │
|
||||||
|
10 (C): │ ││ │
|
||||||
|
11 (C): │ ││ │
|
||||||
|
12 (C): │ ││ │
|
||||||
|
13 (C): │ ││ │
|
||||||
|
14 (C): │ ││ │
|
||||||
|
15 (C): │ ││ │
|
||||||
|
16 (C): │ ││ │
|
||||||
|
17 (C): │ ││ │
|
||||||
|
18 (C): │ ││ │
|
||||||
|
19 (C): └──────────────────────────────────────┘└──────────────────────────────────────┘
|
||||||
|
|
||||||
|
|
@ -0,0 +1,25 @@
|
||||||
|
---
|
||||||
|
source: zellij-server/src/./unit/screen_tests.rs
|
||||||
|
assertion_line: 2758
|
||||||
|
expression: "format!(\"{:#?}\", plugin_load_instruction)"
|
||||||
|
---
|
||||||
|
Some(
|
||||||
|
Load(
|
||||||
|
Some(
|
||||||
|
true,
|
||||||
|
),
|
||||||
|
None,
|
||||||
|
RunPlugin {
|
||||||
|
_allow_exec_host_cmd: false,
|
||||||
|
location: File(
|
||||||
|
"/path/to/fake/plugin",
|
||||||
|
),
|
||||||
|
},
|
||||||
|
0,
|
||||||
|
1,
|
||||||
|
Size {
|
||||||
|
rows: 0,
|
||||||
|
cols: 0,
|
||||||
|
},
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
@ -14,7 +14,6 @@ pub trait ZellijPlugin {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[allow(unused_variables)]
|
#[allow(unused_variables)]
|
||||||
// TODO: can we get rid of the lifetime? maybe with generics?
|
|
||||||
pub trait ZellijWorker<'de>: Default + Serialize + Deserialize<'de> {
|
pub trait ZellijWorker<'de>: Default + Serialize + Deserialize<'de> {
|
||||||
fn on_message(&mut self, message: String, payload: String) {}
|
fn on_message(&mut self, message: String, payload: String) {}
|
||||||
}
|
}
|
||||||
|
|
@ -54,12 +53,13 @@ macro_rules! register_plugin {
|
||||||
|
|
||||||
#[no_mangle]
|
#[no_mangle]
|
||||||
pub fn update() -> bool {
|
pub fn update() -> bool {
|
||||||
|
STATE.with(|state| {
|
||||||
let object = $crate::shim::object_from_stdin()
|
let object = $crate::shim::object_from_stdin()
|
||||||
.context($crate::PLUGIN_MISMATCH)
|
.context($crate::PLUGIN_MISMATCH)
|
||||||
.to_stdout()
|
.to_stdout()
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
state.borrow_mut().update(object)
|
||||||
STATE.with(|state| state.borrow_mut().update(object))
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
#[no_mangle]
|
#[no_mangle]
|
||||||
|
|
@ -78,10 +78,14 @@ macro_rules! register_plugin {
|
||||||
|
|
||||||
#[macro_export]
|
#[macro_export]
|
||||||
macro_rules! register_worker {
|
macro_rules! register_worker {
|
||||||
($worker:ty, $worker_name:ident) => {
|
($worker:ty, $worker_name:ident, $worker_static_name:ident) => {
|
||||||
|
// persist worker state in memory in a static variable
|
||||||
|
thread_local! {
|
||||||
|
static $worker_static_name: std::cell::RefCell<$worker> = std::cell::RefCell::new(Default::default());
|
||||||
|
}
|
||||||
#[no_mangle]
|
#[no_mangle]
|
||||||
pub fn $worker_name() {
|
pub fn $worker_name() {
|
||||||
use serde_json::*;
|
|
||||||
let worker_display_name = std::stringify!($worker_name);
|
let worker_display_name = std::stringify!($worker_name);
|
||||||
|
|
||||||
// read message from STDIN
|
// read message from STDIN
|
||||||
|
|
@ -93,43 +97,10 @@ macro_rules! register_worker {
|
||||||
);
|
);
|
||||||
Default::default()
|
Default::default()
|
||||||
});
|
});
|
||||||
|
$worker_static_name.with(|worker_instance| {
|
||||||
// read previous worker state from HD if it exists
|
let mut worker_instance = worker_instance.borrow_mut();
|
||||||
let mut worker_instance = match std::fs::read(&format!("/data/{}", worker_display_name))
|
|
||||||
.map_err(|e| format!("Failed to read file: {:?}", e))
|
|
||||||
.and_then(|s| {
|
|
||||||
serde_json::from_str::<$worker>(&String::from_utf8_lossy(&s))
|
|
||||||
.map_err(|e| format!("Failed to deserialize: {:?}", e))
|
|
||||||
}) {
|
|
||||||
Ok(s) => s,
|
|
||||||
Err(e) => {
|
|
||||||
eprintln!(
|
|
||||||
"Failed to read existing state ({:?}), creating new state for worker",
|
|
||||||
e
|
|
||||||
);
|
|
||||||
<$worker>::default()
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
// invoke worker
|
|
||||||
worker_instance.on_message(message, payload);
|
worker_instance.on_message(message, payload);
|
||||||
|
});
|
||||||
// persist worker state to HD for next run
|
|
||||||
match serde_json::to_string(&worker_instance)
|
|
||||||
.map_err(|e| format!("Failed to serialize worker state"))
|
|
||||||
.and_then(|serialized_state| {
|
|
||||||
std::fs::write(
|
|
||||||
&format!("/data/{}", worker_display_name),
|
|
||||||
serialized_state.as_bytes(),
|
|
||||||
)
|
|
||||||
.map_err(|e| format!("Failed to persist state to HD: {:?}", e))
|
|
||||||
}) {
|
|
||||||
Ok(()) => {},
|
|
||||||
Err(e) => eprintln!(
|
|
||||||
"Failed to serialize and persist worker state to hd: {:?}",
|
|
||||||
e
|
|
||||||
),
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -39,11 +39,31 @@ pub fn open_file(path: &Path) {
|
||||||
unsafe { host_open_file() };
|
unsafe { host_open_file() };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn open_file_floating(path: &Path) {
|
||||||
|
object_to_stdout(&path);
|
||||||
|
unsafe { host_open_file_floating() };
|
||||||
|
}
|
||||||
|
|
||||||
pub fn open_file_with_line(path: &Path, line: usize) {
|
pub fn open_file_with_line(path: &Path, line: usize) {
|
||||||
object_to_stdout(&(path, line));
|
object_to_stdout(&(path, line));
|
||||||
unsafe { host_open_file_with_line() };
|
unsafe { host_open_file_with_line() };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn open_file_with_line_floating(path: &Path, line: usize) {
|
||||||
|
object_to_stdout(&(path, line));
|
||||||
|
unsafe { host_open_file_with_line_floating() };
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn open_terminal(path: &Path) {
|
||||||
|
object_to_stdout(&path);
|
||||||
|
unsafe { host_open_terminal() };
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn open_terminal_floating(path: &Path) {
|
||||||
|
object_to_stdout(&path);
|
||||||
|
unsafe { host_open_terminal_floating() };
|
||||||
|
}
|
||||||
|
|
||||||
pub fn switch_tab_to(tab_idx: u32) {
|
pub fn switch_tab_to(tab_idx: u32) {
|
||||||
unsafe { host_switch_tab_to(tab_idx) };
|
unsafe { host_switch_tab_to(tab_idx) };
|
||||||
}
|
}
|
||||||
|
|
@ -56,6 +76,10 @@ pub fn exec_cmd(cmd: &[&str]) {
|
||||||
unsafe { host_exec_cmd() };
|
unsafe { host_exec_cmd() };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn hide_self() {
|
||||||
|
unsafe { host_hide_self() };
|
||||||
|
}
|
||||||
|
|
||||||
pub fn report_panic(info: &std::panic::PanicInfo) {
|
pub fn report_panic(info: &std::panic::PanicInfo) {
|
||||||
println!("");
|
println!("");
|
||||||
println!("A panic occured in a plugin");
|
println!("A panic occured in a plugin");
|
||||||
|
|
@ -105,11 +129,16 @@ extern "C" {
|
||||||
fn host_get_plugin_ids();
|
fn host_get_plugin_ids();
|
||||||
fn host_get_zellij_version();
|
fn host_get_zellij_version();
|
||||||
fn host_open_file();
|
fn host_open_file();
|
||||||
|
fn host_open_file_floating();
|
||||||
fn host_open_file_with_line();
|
fn host_open_file_with_line();
|
||||||
|
fn host_open_file_with_line_floating();
|
||||||
|
fn host_open_terminal();
|
||||||
|
fn host_open_terminal_floating();
|
||||||
fn host_switch_tab_to(tab_idx: u32);
|
fn host_switch_tab_to(tab_idx: u32);
|
||||||
fn host_set_timeout(secs: f64);
|
fn host_set_timeout(secs: f64);
|
||||||
fn host_exec_cmd();
|
fn host_exec_cmd();
|
||||||
fn host_report_panic();
|
fn host_report_panic();
|
||||||
fn host_post_message_to();
|
fn host_post_message_to();
|
||||||
fn host_post_message_to_plugin();
|
fn host_post_message_to_plugin();
|
||||||
|
fn host_hide_self();
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -40,6 +40,8 @@ tempfile = "3.2.0"
|
||||||
kdl = { version = "4.5.0", features = ["span"] }
|
kdl = { version = "4.5.0", features = ["span"] }
|
||||||
shellexpand = "3.0.0"
|
shellexpand = "3.0.0"
|
||||||
uuid = { version = "0.8.2", features = ["serde", "v4"] }
|
uuid = { version = "0.8.2", features = ["serde", "v4"] }
|
||||||
|
notify = "6.0.0"
|
||||||
|
async-channel = "1.8.0"
|
||||||
|
|
||||||
#[cfg(not(target_family = "wasm"))]
|
#[cfg(not(target_family = "wasm"))]
|
||||||
[target.'cfg(not(target_family = "wasm"))'.dependencies]
|
[target.'cfg(not(target_family = "wasm"))'.dependencies]
|
||||||
|
|
|
||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
|
@ -377,4 +377,9 @@ pub enum CliAction {
|
||||||
StartOrReloadPlugin {
|
StartOrReloadPlugin {
|
||||||
url: Url,
|
url: Url,
|
||||||
},
|
},
|
||||||
|
LaunchOrFocusPlugin {
|
||||||
|
#[clap(short, long, value_parser)]
|
||||||
|
floating: bool,
|
||||||
|
url: Url,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@ use crate::input::config::ConversionError;
|
||||||
use clap::ArgEnum;
|
use clap::ArgEnum;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use std::fmt;
|
use std::fmt;
|
||||||
|
use std::path::PathBuf;
|
||||||
use std::str::FromStr;
|
use std::str::FromStr;
|
||||||
use strum_macros::{EnumDiscriminants, EnumIter, EnumString, ToString};
|
use strum_macros::{EnumDiscriminants, EnumIter, EnumString, ToString};
|
||||||
|
|
||||||
|
|
@ -473,6 +474,10 @@ pub enum Event {
|
||||||
String, // message
|
String, // message
|
||||||
String, // payload
|
String, // payload
|
||||||
),
|
),
|
||||||
|
FileSystemCreate(Vec<PathBuf>),
|
||||||
|
FileSystemRead(Vec<PathBuf>),
|
||||||
|
FileSystemUpdate(Vec<PathBuf>),
|
||||||
|
FileSystemDelete(Vec<PathBuf>),
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Describes the different input modes, which change the way that keystrokes will be interpreted.
|
/// Describes the different input modes, which change the way that keystrokes will be interpreted.
|
||||||
|
|
|
||||||
|
|
@ -333,6 +333,8 @@ pub enum ScreenContext {
|
||||||
ProgressPluginLoadingOffset,
|
ProgressPluginLoadingOffset,
|
||||||
StartPluginLoadingIndication,
|
StartPluginLoadingIndication,
|
||||||
RequestStateUpdateForPlugins,
|
RequestStateUpdateForPlugins,
|
||||||
|
LaunchOrFocusPlugin,
|
||||||
|
SuppressPane,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Stack call representations corresponding to the different types of [`PtyInstruction`]s.
|
/// Stack call representations corresponding to the different types of [`PtyInstruction`]s.
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@
|
||||||
|
|
||||||
use super::command::RunCommandAction;
|
use super::command::RunCommandAction;
|
||||||
use super::layout::{
|
use super::layout::{
|
||||||
FloatingPaneLayout, Layout, RunPluginLocation, SwapFloatingLayout, SwapTiledLayout,
|
FloatingPaneLayout, Layout, RunPlugin, RunPluginLocation, SwapFloatingLayout, SwapTiledLayout,
|
||||||
TiledPaneLayout,
|
TiledPaneLayout,
|
||||||
};
|
};
|
||||||
use crate::cli::CliAction;
|
use crate::cli::CliAction;
|
||||||
|
|
@ -205,6 +205,7 @@ pub enum Action {
|
||||||
LeftClick(Position),
|
LeftClick(Position),
|
||||||
RightClick(Position),
|
RightClick(Position),
|
||||||
MiddleClick(Position),
|
MiddleClick(Position),
|
||||||
|
LaunchOrFocusPlugin(RunPlugin, bool), // bool => should float
|
||||||
LeftMouseRelease(Position),
|
LeftMouseRelease(Position),
|
||||||
RightMouseRelease(Position),
|
RightMouseRelease(Position),
|
||||||
MiddleMouseRelease(Position),
|
MiddleMouseRelease(Position),
|
||||||
|
|
@ -474,6 +475,15 @@ impl Action {
|
||||||
CliAction::NextSwapLayout => Ok(vec![Action::NextSwapLayout]),
|
CliAction::NextSwapLayout => Ok(vec![Action::NextSwapLayout]),
|
||||||
CliAction::QueryTabNames => Ok(vec![Action::QueryTabNames]),
|
CliAction::QueryTabNames => Ok(vec![Action::QueryTabNames]),
|
||||||
CliAction::StartOrReloadPlugin { url } => Ok(vec![Action::StartOrReloadPlugin(url)]),
|
CliAction::StartOrReloadPlugin { url } => Ok(vec![Action::StartOrReloadPlugin(url)]),
|
||||||
|
CliAction::LaunchOrFocusPlugin { url, floating } => {
|
||||||
|
let run_plugin_location = RunPluginLocation::parse(url.as_str())
|
||||||
|
.map_err(|e| format!("Failed to parse plugin location: {}", e))?;
|
||||||
|
let run_plugin = RunPlugin {
|
||||||
|
location: run_plugin_location,
|
||||||
|
_allow_exec_host_cmd: false,
|
||||||
|
};
|
||||||
|
Ok(vec![Action::LaunchOrFocusPlugin(run_plugin, floating)])
|
||||||
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -68,3 +68,16 @@ impl From<RunCommandAction> for RunCommand {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl RunCommand {
|
||||||
|
pub fn new(command: PathBuf) -> Self {
|
||||||
|
RunCommand {
|
||||||
|
command,
|
||||||
|
..Default::default()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pub fn with_cwd(mut self, cwd: PathBuf) -> Self {
|
||||||
|
self.cwd = Some(cwd);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -876,6 +876,29 @@ impl TryFrom<(&KdlNode, &Options)> for Action {
|
||||||
};
|
};
|
||||||
Ok(Action::Run(run_command_action))
|
Ok(Action::Run(run_command_action))
|
||||||
},
|
},
|
||||||
|
"LaunchOrFocusPlugin" => {
|
||||||
|
let arguments = action_arguments.iter().copied();
|
||||||
|
let mut args = kdl_arguments_that_are_strings(arguments)?;
|
||||||
|
if args.is_empty() {
|
||||||
|
return Err(ConfigError::new_kdl_error(
|
||||||
|
"No plugin found to launch in LaunchOrFocusPlugin".into(),
|
||||||
|
kdl_action.span().offset(),
|
||||||
|
kdl_action.span().len(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
let plugin_path = args.remove(0);
|
||||||
|
|
||||||
|
let command_metadata = action_children.iter().next();
|
||||||
|
let should_float = command_metadata
|
||||||
|
.and_then(|c_m| kdl_child_bool_value_for_entry(c_m, "floating"))
|
||||||
|
.unwrap_or(false);
|
||||||
|
let location = RunPluginLocation::parse(&plugin_path)?;
|
||||||
|
let run_plugin = RunPlugin {
|
||||||
|
location,
|
||||||
|
_allow_exec_host_cmd: false,
|
||||||
|
};
|
||||||
|
Ok(Action::LaunchOrFocusPlugin(run_plugin, should_float))
|
||||||
|
},
|
||||||
"PreviousSwapLayout" => Ok(Action::PreviousSwapLayout),
|
"PreviousSwapLayout" => Ok(Action::PreviousSwapLayout),
|
||||||
"NextSwapLayout" => Ok(Action::NextSwapLayout),
|
"NextSwapLayout" => Ok(Action::NextSwapLayout),
|
||||||
_ => Err(ConfigError::new_kdl_error(
|
_ => Err(ConfigError::new_kdl_error(
|
||||||
|
|
|
||||||
|
|
@ -20,6 +20,6 @@ pub mod logging; // Requires log4rs
|
||||||
|
|
||||||
#[cfg(not(target_family = "wasm"))]
|
#[cfg(not(target_family = "wasm"))]
|
||||||
pub use ::{
|
pub use ::{
|
||||||
anyhow, async_std, clap, interprocess, lazy_static, libc, miette, nix, regex, serde,
|
anyhow, async_channel, async_std, clap, interprocess, lazy_static, libc, miette, nix, notify,
|
||||||
signal_hook, tempfile, termwiz, vte,
|
regex, serde, signal_hook, tempfile, termwiz, vte,
|
||||||
};
|
};
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue