feat: add plugin permission system (#2624)
* WIP: add exaple of permission ui * feat: add request permission ui * feat: add caching permission in memory * feat: add permission check * feat: add file caching * fix: changes request * feat(ui): new status bar mode (#2619) * supermode prototype * fix integration tests * fix tests * style(fmt): rustfmt * docs(changelog): status-bar supermode * fix(rendering): occasional glitches while resizing (#2621) * docs(changelog): resize glitches fix * chore(version): bump development version * Fix colored pane frames in mirrored sessions (#2625) * server/panes/tiled: Fix colored frames in mirrored sessions. Colored frames were previously ignored because they were treated like floating panes when rendering tiled panes. * CHANGELOG: Add PR #2625 * server/tab/unit: Fix unit tests for server. * fix(sessions): use custom lists of adjectives and nouns for generating session names (#2122) * Create custom lists of adjectives and nouns for generating session names * move word lists to const slices * add logic to retry name generation * refactor - reuse the name generator - iterator instead of for loop --------- Co-authored-by: Thomas Linford <linford.t@gmail.com> * docs(changelog): generate session names with custom words list * feat(plugins): make plugins configurable (#2646) * work * make every plugin entry point configurable * make integration tests pass * make e2e tests pass * add test for plugin configuration * add test snapshot * add plugin config parsing test * cleanups * style(fmt): rustfmt * style(comment): remove commented code * docs(changelog): configurable plugins * style(fmt): rustfmt * touch up ui * fix: don't save permission data in memory * feat: load cached permission * test: add example test (WIP) * fix: issue event are always denied * test: update snapshot * apply formatting * refactor: update default cache function * test: add more new test * apply formatting * Revert "apply formatting" This reverts commit a4e93703fbfdb6865131daa1c8b90fc5c36ab25e. * apply format * fix: update cache path * apply format * fix: cache path * fix: update log level * test for github workflow * Revert "test for github workflow" This reverts commit 01eff3bc5d1627a4e60bc6dac8ebe5500bc5b56e. * refactor: permission cache * fix(test): permission grant/deny race condition * style(fmt): rustfmt * style(fmt): rustfmt * configure permissions * permission denied test * snapshot * add ui for small plugins * style(fmt): rustfmt * some cleanups --------- Co-authored-by: Aram Drevekenin <aram@poor.dev> Co-authored-by: har7an <99636919+har7an@users.noreply.github.com> Co-authored-by: Kyle Sutherland-Cash <kyle.sutherlandcash@gmail.com> Co-authored-by: Thomas Linford <linford.t@gmail.com> Co-authored-by: Thomas Linford <tlinford@users.noreply.github.com>
This commit is contained in:
parent
a1903b6b04
commit
c8ddb23297
37 changed files with 1935 additions and 337 deletions
|
|
@ -39,6 +39,16 @@ register_worker!(TestWorker, test_worker, TEST_WORKER);
|
|||
|
||||
impl ZellijPlugin for State {
|
||||
fn load(&mut self, configuration: BTreeMap<String, String>) {
|
||||
request_permission(&[
|
||||
PermissionType::ChangeApplicationState,
|
||||
PermissionType::ReadApplicationState,
|
||||
PermissionType::ReadApplicationState,
|
||||
PermissionType::ChangeApplicationState,
|
||||
PermissionType::OpenFiles,
|
||||
PermissionType::RunCommands,
|
||||
PermissionType::OpenTerminalsOrPlugins,
|
||||
PermissionType::WriteToStdin,
|
||||
]);
|
||||
self.configuration = configuration;
|
||||
subscribe(&[
|
||||
EventType::InputReceived,
|
||||
|
|
@ -227,6 +237,9 @@ impl ZellijPlugin for State {
|
|||
Key::Ctrl('z') => {
|
||||
go_to_tab_name(&format!("{:?}", self.configuration));
|
||||
},
|
||||
Key::Ctrl('1') => {
|
||||
request_permission(&[PermissionType::ReadApplicationState]);
|
||||
},
|
||||
_ => {},
|
||||
},
|
||||
Event::CustomMessage(message, payload) => {
|
||||
|
|
|
|||
|
|
@ -30,6 +30,7 @@ impl ZellijPlugin for State {
|
|||
EventType::FileSystemCreate,
|
||||
EventType::FileSystemUpdate,
|
||||
EventType::FileSystemDelete,
|
||||
EventType::PermissionRequestResult,
|
||||
]);
|
||||
post_message_to(PluginMessage {
|
||||
worker_name: Some("file_name_search".into()),
|
||||
|
|
@ -54,6 +55,9 @@ impl ZellijPlugin for State {
|
|||
};
|
||||
self.ev_history.push_back((event.clone(), Instant::now()));
|
||||
match event {
|
||||
Event::PermissionRequestResult(_) => {
|
||||
should_render = true;
|
||||
},
|
||||
Event::Timer(_elapsed) => {
|
||||
if self.search_state.loading {
|
||||
set_timeout(0.5);
|
||||
|
|
|
|||
|
|
@ -295,7 +295,7 @@ impl FloatingPanes {
|
|||
pane.render_full_viewport();
|
||||
}
|
||||
}
|
||||
pub fn set_pane_frames(&mut self, os_api: &mut Box<dyn ServerOsApi>) -> Result<()> {
|
||||
pub fn set_pane_frames(&mut self, _os_api: &mut Box<dyn ServerOsApi>) -> Result<()> {
|
||||
let err_context =
|
||||
|pane_id: &PaneId| format!("failed to activate frame on pane {pane_id:?}");
|
||||
|
||||
|
|
@ -392,7 +392,7 @@ impl FloatingPanes {
|
|||
self.set_force_render();
|
||||
}
|
||||
|
||||
pub fn resize_pty_all_panes(&mut self, os_api: &mut Box<dyn ServerOsApi>) -> Result<()> {
|
||||
pub fn resize_pty_all_panes(&mut self, _os_api: &mut Box<dyn ServerOsApi>) -> Result<()> {
|
||||
for pane in self.panes.values_mut() {
|
||||
resize_pty!(pane, os_api, self.senders, self.character_cell_size)
|
||||
.with_context(|| format!("failed to resize PTY in pane {:?}", pane.pid()))?;
|
||||
|
|
@ -403,7 +403,7 @@ impl FloatingPanes {
|
|||
pub fn resize_active_pane(
|
||||
&mut self,
|
||||
client_id: ClientId,
|
||||
os_api: &mut Box<dyn ServerOsApi>,
|
||||
_os_api: &mut Box<dyn ServerOsApi>,
|
||||
strategy: &ResizeStrategy,
|
||||
) -> Result<bool> {
|
||||
// true => successfully resized
|
||||
|
|
@ -838,7 +838,7 @@ impl FloatingPanes {
|
|||
self.focus_pane_for_all_clients(focused_pane);
|
||||
}
|
||||
}
|
||||
pub fn switch_active_pane_with(&mut self, os_api: &mut Box<dyn ServerOsApi>, pane_id: PaneId) {
|
||||
pub fn switch_active_pane_with(&mut self, _os_api: &mut Box<dyn ServerOsApi>, pane_id: PaneId) {
|
||||
if let Some(active_pane_id) = self.first_active_floating_pane_id() {
|
||||
let current_position = self.panes.get(&active_pane_id).unwrap();
|
||||
let prev_geom = current_position.position_and_size();
|
||||
|
|
|
|||
|
|
@ -1,11 +1,11 @@
|
|||
use std::collections::HashMap;
|
||||
use std::collections::{BTreeSet, HashMap};
|
||||
use std::time::Instant;
|
||||
|
||||
use crate::output::{CharacterChunk, SixelImageChunk};
|
||||
use crate::panes::{grid::Grid, sixel::SixelImageStore, LinkHandler, PaneId};
|
||||
use crate::plugins::PluginInstruction;
|
||||
use crate::pty::VteBytes;
|
||||
use crate::tab::Pane;
|
||||
use crate::tab::{AdjustedInput, Pane};
|
||||
use crate::ui::{
|
||||
loading_indication::LoadingIndication,
|
||||
pane_boundaries_frame::{FrameParams, PaneFrame},
|
||||
|
|
@ -13,6 +13,7 @@ use crate::ui::{
|
|||
use crate::ClientId;
|
||||
use std::cell::RefCell;
|
||||
use std::rc::Rc;
|
||||
use zellij_utils::data::{PermissionStatus, PermissionType, PluginPermission};
|
||||
use zellij_utils::pane_size::{Offset, SizeInPixels};
|
||||
use zellij_utils::position::Position;
|
||||
use zellij_utils::{
|
||||
|
|
@ -25,6 +26,15 @@ use zellij_utils::{
|
|||
vte,
|
||||
};
|
||||
|
||||
macro_rules! style {
|
||||
($fg:expr) => {
|
||||
ansi_term::Style::new().fg(match $fg {
|
||||
PaletteColor::Rgb((r, g, b)) => ansi_term::Color::RGB(r, g, b),
|
||||
PaletteColor::EightBit(color) => ansi_term::Color::Fixed(color),
|
||||
})
|
||||
};
|
||||
}
|
||||
|
||||
macro_rules! get_or_create_grid {
|
||||
($self:ident, $client_id:ident) => {{
|
||||
let rows = $self.get_content_rows();
|
||||
|
|
@ -73,6 +83,7 @@ pub(crate) struct PluginPane {
|
|||
pane_frame_color_override: Option<(PaletteColor, Option<String>)>,
|
||||
invoked_with: Option<Run>,
|
||||
loading_indication: LoadingIndication,
|
||||
requesting_permissions: Option<PluginPermission>,
|
||||
debug: bool,
|
||||
}
|
||||
|
||||
|
|
@ -121,6 +132,7 @@ impl PluginPane {
|
|||
pane_frame_color_override: None,
|
||||
invoked_with,
|
||||
loading_indication,
|
||||
requesting_permissions: None,
|
||||
debug,
|
||||
};
|
||||
for client_id in currently_connected_clients {
|
||||
|
|
@ -181,6 +193,14 @@ impl Pane for PluginPane {
|
|||
}
|
||||
fn handle_plugin_bytes(&mut self, client_id: ClientId, bytes: VteBytes) {
|
||||
self.set_client_should_render(client_id, true);
|
||||
|
||||
let mut vte_bytes = bytes;
|
||||
if let Some(plugin_permission) = &self.requesting_permissions {
|
||||
vte_bytes = self
|
||||
.display_request_permission_message(plugin_permission)
|
||||
.into();
|
||||
}
|
||||
|
||||
let grid = get_or_create_grid!(self, client_id);
|
||||
|
||||
// this is part of the plugin contract, whenever we update the plugin and call its render function, we delete the existing viewport
|
||||
|
|
@ -193,14 +213,36 @@ impl Pane for PluginPane {
|
|||
.vte_parsers
|
||||
.entry(client_id)
|
||||
.or_insert_with(|| vte::Parser::new());
|
||||
for &byte in &bytes {
|
||||
|
||||
for &byte in &vte_bytes {
|
||||
vte_parser.advance(grid, byte);
|
||||
}
|
||||
|
||||
self.should_render.insert(client_id, true);
|
||||
}
|
||||
fn cursor_coordinates(&self) -> Option<(usize, usize)> {
|
||||
None
|
||||
}
|
||||
fn adjust_input_to_terminal(&mut self, input_bytes: Vec<u8>) -> Option<AdjustedInput> {
|
||||
if let Some(requesting_permissions) = &self.requesting_permissions {
|
||||
let permissions = requesting_permissions.permissions.clone();
|
||||
match input_bytes.as_slice() {
|
||||
// Y or y
|
||||
&[89] | &[121] => Some(AdjustedInput::PermissionRequestResult(
|
||||
permissions,
|
||||
PermissionStatus::Granted,
|
||||
)),
|
||||
// N or n
|
||||
&[78] | &[110] => Some(AdjustedInput::PermissionRequestResult(
|
||||
permissions,
|
||||
PermissionStatus::Denied,
|
||||
)),
|
||||
_ => None,
|
||||
}
|
||||
} else {
|
||||
Some(AdjustedInput::WriteBytesToTerminal(input_bytes))
|
||||
}
|
||||
}
|
||||
fn position_and_size(&self) -> PaneGeom {
|
||||
self.geom
|
||||
}
|
||||
|
|
@ -233,6 +275,9 @@ impl Pane for PluginPane {
|
|||
fn set_selectable(&mut self, selectable: bool) {
|
||||
self.selectable = selectable;
|
||||
}
|
||||
fn request_permissions_from_user(&mut self, permissions: Option<PluginPermission>) {
|
||||
self.requesting_permissions = permissions;
|
||||
}
|
||||
fn render(
|
||||
&mut self,
|
||||
client_id: Option<ClientId>,
|
||||
|
|
@ -595,4 +640,54 @@ impl PluginPane {
|
|||
self.handle_plugin_bytes(client_id, bytes.clone());
|
||||
}
|
||||
}
|
||||
fn display_request_permission_message(&self, plugin_permission: &PluginPermission) -> String {
|
||||
let bold_white = style!(self.style.colors.white).bold();
|
||||
let cyan = style!(self.style.colors.cyan).bold();
|
||||
let orange = style!(self.style.colors.orange).bold();
|
||||
let green = style!(self.style.colors.green).bold();
|
||||
|
||||
let mut messages = String::new();
|
||||
let permissions: BTreeSet<PermissionType> =
|
||||
plugin_permission.permissions.clone().into_iter().collect();
|
||||
|
||||
let min_row_count = permissions.len() + 4;
|
||||
|
||||
if self.rows() >= min_row_count {
|
||||
messages.push_str(&format!(
|
||||
"{} {} {}\n",
|
||||
bold_white.paint("Plugin"),
|
||||
cyan.paint(&plugin_permission.name),
|
||||
bold_white.paint("asks permission to:"),
|
||||
));
|
||||
permissions.iter().enumerate().for_each(|(i, p)| {
|
||||
messages.push_str(&format!(
|
||||
"\n\r{}. {}",
|
||||
bold_white.paint(&format!("{}", i + 1)),
|
||||
orange.paint(p.display_name())
|
||||
));
|
||||
});
|
||||
|
||||
messages.push_str(&format!(
|
||||
"\n\n\r{} {}",
|
||||
bold_white.paint("Allow?"),
|
||||
green.paint("(y/n)"),
|
||||
));
|
||||
} else {
|
||||
messages.push_str(&format!(
|
||||
"{} {}. {} {}\n",
|
||||
bold_white.paint("This plugin asks permission to:"),
|
||||
orange.paint(
|
||||
permissions
|
||||
.iter()
|
||||
.map(|p| p.to_string())
|
||||
.collect::<Vec<_>>()
|
||||
.join(", ")
|
||||
),
|
||||
bold_white.paint("Allow?"),
|
||||
green.paint("(y/n)"),
|
||||
));
|
||||
}
|
||||
|
||||
messages
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -68,7 +68,6 @@ pub struct TiledPanes {
|
|||
draw_pane_frames: bool,
|
||||
panes_to_hide: HashSet<PaneId>,
|
||||
fullscreen_is_active: bool,
|
||||
os_api: Box<dyn ServerOsApi>,
|
||||
senders: ThreadSenders,
|
||||
window_title: Option<String>,
|
||||
client_id_to_boundaries: HashMap<ClientId, Boundaries>,
|
||||
|
|
@ -105,7 +104,6 @@ impl TiledPanes {
|
|||
draw_pane_frames,
|
||||
panes_to_hide: HashSet::new(),
|
||||
fullscreen_is_active: false,
|
||||
os_api,
|
||||
senders,
|
||||
window_title: None,
|
||||
client_id_to_boundaries: HashMap::new(),
|
||||
|
|
|
|||
|
|
@ -18,7 +18,7 @@ use crate::{pty::PtyInstruction, thread_bus::Bus, ClientId, ServerInstruction};
|
|||
use wasm_bridge::WasmBridge;
|
||||
|
||||
use zellij_utils::{
|
||||
data::{Event, EventType, PluginCapabilities},
|
||||
data::{Event, EventType, PermissionStatus, PermissionType, PluginCapabilities},
|
||||
errors::{prelude::*, ContextType, PluginContext},
|
||||
input::{
|
||||
command::TerminalAction,
|
||||
|
|
@ -79,6 +79,13 @@ pub enum PluginInstruction {
|
|||
String, // serialized payload
|
||||
),
|
||||
PluginSubscribedToEvents(PluginId, ClientId, HashSet<EventType>),
|
||||
PermissionRequestResult(
|
||||
PluginId,
|
||||
Option<ClientId>,
|
||||
Vec<PermissionType>,
|
||||
PermissionStatus,
|
||||
Option<PathBuf>,
|
||||
),
|
||||
Exit,
|
||||
}
|
||||
|
||||
|
|
@ -105,6 +112,9 @@ impl From<&PluginInstruction> for PluginContext {
|
|||
PluginInstruction::PluginSubscribedToEvents(..) => {
|
||||
PluginContext::PluginSubscribedToEvents
|
||||
},
|
||||
PluginInstruction::PermissionRequestResult(..) => {
|
||||
PluginContext::PermissionRequestResult
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -287,6 +297,30 @@ pub(crate) fn plugin_thread_main(
|
|||
}
|
||||
}
|
||||
},
|
||||
PluginInstruction::PermissionRequestResult(
|
||||
plugin_id,
|
||||
client_id,
|
||||
permissions,
|
||||
status,
|
||||
cache_path,
|
||||
) => {
|
||||
if let Err(e) = wasm_bridge.cache_plugin_permissions(
|
||||
plugin_id,
|
||||
client_id,
|
||||
permissions,
|
||||
status,
|
||||
cache_path,
|
||||
) {
|
||||
log::error!("{}", e);
|
||||
}
|
||||
|
||||
let updates = vec![(
|
||||
Some(plugin_id),
|
||||
client_id,
|
||||
Event::PermissionRequestResult(status),
|
||||
)];
|
||||
wasm_bridge.update_plugins(updates)?;
|
||||
},
|
||||
PluginInstruction::Exit => {
|
||||
wasm_bridge.cleanup();
|
||||
break;
|
||||
|
|
|
|||
|
|
@ -188,6 +188,7 @@ impl<'a> PluginLoader<'a> {
|
|||
display_loading_stage!(end, loading_indication, senders, plugin_id);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn add_client(
|
||||
client_id: ClientId,
|
||||
plugin_dir: PathBuf,
|
||||
|
|
@ -613,6 +614,19 @@ impl<'a> PluginLoader<'a> {
|
|||
}
|
||||
start_function.call(&[]).with_context(err_context)?;
|
||||
|
||||
plugin_map.lock().unwrap().insert(
|
||||
self.plugin_id,
|
||||
self.client_id,
|
||||
Arc::new(Mutex::new(RunningPlugin::new(
|
||||
main_user_instance,
|
||||
main_user_env,
|
||||
self.size.rows,
|
||||
self.size.cols,
|
||||
))),
|
||||
subscriptions.clone(),
|
||||
workers,
|
||||
);
|
||||
|
||||
let protobuf_plugin_configuration: ProtobufPluginConfiguration = self
|
||||
.plugin
|
||||
.userspace_configuration
|
||||
|
|
@ -640,18 +654,6 @@ impl<'a> PluginLoader<'a> {
|
|||
self.senders,
|
||||
self.plugin_id
|
||||
);
|
||||
plugin_map.lock().unwrap().insert(
|
||||
self.plugin_id,
|
||||
self.client_id,
|
||||
Arc::new(Mutex::new(RunningPlugin::new(
|
||||
main_user_instance,
|
||||
main_user_env,
|
||||
self.size.rows,
|
||||
self.size.cols,
|
||||
))),
|
||||
subscriptions.clone(),
|
||||
workers,
|
||||
);
|
||||
display_loading_stage!(
|
||||
indicate_writing_plugin_to_cache_success,
|
||||
self.loading_indication,
|
||||
|
|
@ -764,13 +766,13 @@ impl<'a> PluginLoader<'a> {
|
|||
})
|
||||
.with_context(err_context)?;
|
||||
let wasi = wasi_env.import_object(&module).with_context(err_context)?;
|
||||
|
||||
let mut mut_plugin = self.plugin.clone();
|
||||
mut_plugin.set_tab_index(self.tab_index);
|
||||
let plugin_env = PluginEnv {
|
||||
plugin_id: self.plugin_id,
|
||||
client_id: self.client_id,
|
||||
plugin: mut_plugin,
|
||||
permissions: Arc::new(Mutex::new(None)),
|
||||
senders: self.senders.clone(),
|
||||
wasi_env,
|
||||
plugin_own_data_dir: self.plugin_own_data_dir.clone(),
|
||||
|
|
|
|||
|
|
@ -11,7 +11,6 @@ use wasmer_wasi::WasiEnv;
|
|||
use crate::{thread_bus::ThreadSenders, ClientId};
|
||||
|
||||
use zellij_utils::async_channel::Sender;
|
||||
use zellij_utils::errors::prelude::*;
|
||||
use zellij_utils::{
|
||||
data::EventType,
|
||||
data::PluginCapabilities,
|
||||
|
|
@ -20,6 +19,7 @@ use zellij_utils::{
|
|||
input::plugins::PluginConfig,
|
||||
ipc::ClientAttributes,
|
||||
};
|
||||
use zellij_utils::{data::PermissionType, errors::prelude::*};
|
||||
|
||||
// the idea here is to provide atomicity when adding/removing plugins from the map (eg. when a new
|
||||
// client connects) but to also allow updates/renders not to block each other
|
||||
|
|
@ -193,6 +193,7 @@ pub type Subscriptions = HashSet<EventType>;
|
|||
pub struct PluginEnv {
|
||||
pub plugin_id: PluginId,
|
||||
pub plugin: PluginConfig,
|
||||
pub permissions: Arc<Mutex<Option<HashSet<PermissionType>>>>,
|
||||
pub senders: ThreadSenders,
|
||||
pub wasi_env: WasiEnv,
|
||||
pub tab_index: usize,
|
||||
|
|
@ -215,6 +216,10 @@ impl PluginEnv {
|
|||
self.plugin_id
|
||||
)
|
||||
}
|
||||
|
||||
pub fn set_permissions(&mut self, permissions: HashSet<PermissionType>) {
|
||||
self.permissions.lock().unwrap().replace(permissions);
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Eq, PartialEq, Hash)]
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -0,0 +1,7 @@
|
|||
---
|
||||
source: zellij-server/src/plugins/./unit/plugin_tests.rs
|
||||
expression: "format!(\"{:#?}\", permissions)"
|
||||
---
|
||||
Some(
|
||||
[],
|
||||
)
|
||||
|
|
@ -0,0 +1,15 @@
|
|||
---
|
||||
source: zellij-server/src/plugins/./unit/plugin_tests.rs
|
||||
assertion_line: 4864
|
||||
expression: "format!(\"{:#?}\", permissions)"
|
||||
---
|
||||
Some(
|
||||
[
|
||||
ReadApplicationState,
|
||||
ChangeApplicationState,
|
||||
OpenFiles,
|
||||
RunCommands,
|
||||
OpenTerminalsOrPlugins,
|
||||
WriteToStdin,
|
||||
],
|
||||
)
|
||||
|
|
@ -0,0 +1,17 @@
|
|||
---
|
||||
source: zellij-server/src/plugins/./unit/plugin_tests.rs
|
||||
assertion_line: 4767
|
||||
expression: "format!(\"{:#?}\", new_tab_event)"
|
||||
---
|
||||
Some(
|
||||
[
|
||||
ChangeApplicationState,
|
||||
ReadApplicationState,
|
||||
ReadApplicationState,
|
||||
ChangeApplicationState,
|
||||
OpenFiles,
|
||||
RunCommands,
|
||||
OpenTerminalsOrPlugins,
|
||||
WriteToStdin,
|
||||
],
|
||||
)
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
---
|
||||
source: zellij-server/src/plugins/./unit/plugin_tests.rs
|
||||
assertion_line: 903
|
||||
expression: "format!(\"{:#?}\", switch_to_mode_event)"
|
||||
---
|
||||
None
|
||||
|
|
@ -13,6 +13,8 @@ use std::{
|
|||
};
|
||||
use wasmer::{Instance, Module, Store, Value};
|
||||
use zellij_utils::async_std::task::{self, JoinHandle};
|
||||
use zellij_utils::data::{PermissionStatus, PermissionType};
|
||||
use zellij_utils::input::permission::PermissionCache;
|
||||
use zellij_utils::notify_debouncer_full::{notify::RecommendedWatcher, Debouncer, FileIdMap};
|
||||
use zellij_utils::plugin_api::event::ProtobufEvent;
|
||||
|
||||
|
|
@ -706,6 +708,44 @@ impl WasmBridge {
|
|||
};
|
||||
}
|
||||
}
|
||||
pub fn cache_plugin_permissions(
|
||||
&mut self,
|
||||
plugin_id: PluginId,
|
||||
client_id: Option<ClientId>,
|
||||
permissions: Vec<PermissionType>,
|
||||
status: PermissionStatus,
|
||||
cache_path: Option<PathBuf>,
|
||||
) -> Result<()> {
|
||||
if let Some(running_plugin) = self
|
||||
.plugin_map
|
||||
.lock()
|
||||
.unwrap()
|
||||
.get_running_plugin(plugin_id, client_id)
|
||||
{
|
||||
let err_context = || format!("Failed to write plugin permission {plugin_id}");
|
||||
|
||||
let mut running_plugin = running_plugin.lock().unwrap();
|
||||
let permissions = if status == PermissionStatus::Granted {
|
||||
permissions
|
||||
} else {
|
||||
vec![]
|
||||
};
|
||||
|
||||
running_plugin
|
||||
.plugin_env
|
||||
.set_permissions(HashSet::from_iter(permissions.clone()));
|
||||
|
||||
let mut permission_cache = PermissionCache::from_path_or_default(cache_path);
|
||||
permission_cache.cache(
|
||||
running_plugin.plugin_env.plugin.location.to_string(),
|
||||
permissions,
|
||||
);
|
||||
|
||||
permission_cache.write_to_file().with_context(err_context)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_plugin_successful_loading(senders: &ThreadSenders, plugin_id: PluginId) {
|
||||
|
|
@ -728,6 +768,35 @@ fn handle_plugin_loading_failure(
|
|||
));
|
||||
}
|
||||
|
||||
// TODO: move to permissions?
|
||||
fn check_event_permission(
|
||||
plugin_env: &PluginEnv,
|
||||
event: &Event,
|
||||
) -> (PermissionStatus, Option<PermissionType>) {
|
||||
if plugin_env.plugin.is_builtin() {
|
||||
// built-in plugins can do all the things because they're part of the application and
|
||||
// there's no use to deny them anything
|
||||
return (PermissionStatus::Granted, None);
|
||||
}
|
||||
let permission = match event {
|
||||
Event::ModeUpdate(..)
|
||||
| Event::TabUpdate(..)
|
||||
| Event::PaneUpdate(..)
|
||||
| Event::CopyToClipboard(..)
|
||||
| Event::SystemClipboardFailure
|
||||
| Event::InputReceived => PermissionType::ReadApplicationState,
|
||||
_ => return (PermissionStatus::Granted, None),
|
||||
};
|
||||
|
||||
if let Some(permissions) = plugin_env.permissions.lock().unwrap().as_ref() {
|
||||
if permissions.contains(&permission) {
|
||||
return (PermissionStatus::Granted, None);
|
||||
}
|
||||
}
|
||||
|
||||
(PermissionStatus::Denied, Some(permission))
|
||||
}
|
||||
|
||||
pub fn apply_event_to_plugin(
|
||||
plugin_id: PluginId,
|
||||
client_id: ClientId,
|
||||
|
|
@ -739,35 +808,48 @@ pub fn apply_event_to_plugin(
|
|||
plugin_bytes: &mut Vec<(PluginId, ClientId, Vec<u8>)>,
|
||||
) -> Result<()> {
|
||||
let err_context = || format!("Failed to apply event to plugin {plugin_id}");
|
||||
let protobuf_event: ProtobufEvent = event
|
||||
.clone()
|
||||
.try_into()
|
||||
.map_err(|e| anyhow!("Failed to convert to protobuf: {:?}", e))?;
|
||||
let update = instance
|
||||
.exports
|
||||
.get_function("update")
|
||||
.with_context(err_context)?;
|
||||
wasi_write_object(&plugin_env.wasi_env, &protobuf_event.encode_to_vec())
|
||||
.with_context(err_context)?;
|
||||
let update_return = update.call(&[]).with_context(err_context)?;
|
||||
let should_render = match update_return.get(0) {
|
||||
Some(Value::I32(n)) => *n == 1,
|
||||
_ => false,
|
||||
};
|
||||
|
||||
if rows > 0 && columns > 0 && should_render {
|
||||
let rendered_bytes = instance
|
||||
.exports
|
||||
.get_function("render")
|
||||
.map_err(anyError::new)
|
||||
.and_then(|render| {
|
||||
render
|
||||
.call(&[Value::I32(rows as i32), Value::I32(columns as i32)])
|
||||
match check_event_permission(plugin_env, event) {
|
||||
(PermissionStatus::Granted, _) => {
|
||||
let protobuf_event: ProtobufEvent = event
|
||||
.clone()
|
||||
.try_into()
|
||||
.map_err(|e| anyhow!("Failed to convert to protobuf: {:?}", e))?;
|
||||
let update = instance
|
||||
.exports
|
||||
.get_function("update")
|
||||
.with_context(err_context)?;
|
||||
wasi_write_object(&plugin_env.wasi_env, &protobuf_event.encode_to_vec())
|
||||
.with_context(err_context)?;
|
||||
let update_return = update.call(&[]).with_context(err_context)?;
|
||||
let should_render = match update_return.get(0) {
|
||||
Some(Value::I32(n)) => *n == 1,
|
||||
_ => false,
|
||||
};
|
||||
if rows > 0 && columns > 0 && should_render {
|
||||
let rendered_bytes = instance
|
||||
.exports
|
||||
.get_function("render")
|
||||
.map_err(anyError::new)
|
||||
})
|
||||
.and_then(|_| wasi_read_string(&plugin_env.wasi_env))
|
||||
.with_context(err_context)?;
|
||||
plugin_bytes.push((plugin_id, client_id, rendered_bytes.as_bytes().to_vec()));
|
||||
.and_then(|render| {
|
||||
render
|
||||
.call(&[Value::I32(rows as i32), Value::I32(columns as i32)])
|
||||
.map_err(anyError::new)
|
||||
})
|
||||
.and_then(|_| wasi_read_string(&plugin_env.wasi_env))
|
||||
.with_context(err_context)?;
|
||||
plugin_bytes.push((plugin_id, client_id, rendered_bytes.as_bytes().to_vec()));
|
||||
}
|
||||
},
|
||||
(PermissionStatus::Denied, permission) => {
|
||||
log::error!(
|
||||
"PluginId '{}' permission '{}' is not allowed - Event '{:?}' denied",
|
||||
plugin_id,
|
||||
permission
|
||||
.map(|p| p.to_string())
|
||||
.unwrap_or("UNKNOWN".to_owned()),
|
||||
EventType::from_str(&event.to_string()).with_context(err_context)?
|
||||
);
|
||||
},
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,12 +8,15 @@ use std::{
|
|||
collections::{BTreeMap, HashSet},
|
||||
path::PathBuf,
|
||||
process,
|
||||
str::FromStr,
|
||||
sync::{Arc, Mutex},
|
||||
thread,
|
||||
time::{Duration, Instant},
|
||||
};
|
||||
use wasmer::{imports, Function, ImportObject, Store, WasmerEnv};
|
||||
use wasmer_wasi::WasiEnv;
|
||||
use zellij_utils::data::{CommandType, PermissionStatus, PermissionType, PluginPermission};
|
||||
use zellij_utils::input::permission::PermissionCache;
|
||||
|
||||
use url::Url;
|
||||
|
||||
|
|
@ -86,118 +89,134 @@ impl ForeignFunctionEnv {
|
|||
}
|
||||
|
||||
fn host_run_plugin_command(env: &ForeignFunctionEnv) {
|
||||
let err_context = || format!("failed to run plugin command {}", env.plugin_env.name());
|
||||
wasi_read_bytes(&env.plugin_env.wasi_env)
|
||||
.and_then(|bytes| {
|
||||
let command: ProtobufPluginCommand = ProtobufPluginCommand::decode(bytes.as_slice())?;
|
||||
let command: PluginCommand = command
|
||||
.try_into()
|
||||
.map_err(|e| anyhow!("failed to convert serialized command: {}", e))?;
|
||||
match command {
|
||||
PluginCommand::Subscribe(event_list) => subscribe(env, event_list)?,
|
||||
PluginCommand::Unsubscribe(event_list) => unsubscribe(env, event_list)?,
|
||||
PluginCommand::SetSelectable(selectable) => set_selectable(env, selectable),
|
||||
PluginCommand::GetPluginIds => get_plugin_ids(env),
|
||||
PluginCommand::GetZellijVersion => get_zellij_version(env),
|
||||
PluginCommand::OpenFile(file_to_open) => open_file(env, file_to_open),
|
||||
PluginCommand::OpenFileFloating(file_to_open) => {
|
||||
open_file_floating(env, file_to_open)
|
||||
match check_command_permission(&env.plugin_env, &command) {
|
||||
(PermissionStatus::Granted, _) => match command {
|
||||
PluginCommand::Subscribe(event_list) => subscribe(env, event_list)?,
|
||||
PluginCommand::Unsubscribe(event_list) => unsubscribe(env, event_list)?,
|
||||
PluginCommand::SetSelectable(selectable) => set_selectable(env, selectable),
|
||||
PluginCommand::GetPluginIds => get_plugin_ids(env),
|
||||
PluginCommand::GetZellijVersion => get_zellij_version(env),
|
||||
PluginCommand::OpenFile(file_to_open) => open_file(env, file_to_open),
|
||||
PluginCommand::OpenFileFloating(file_to_open) => {
|
||||
open_file_floating(env, file_to_open)
|
||||
},
|
||||
PluginCommand::OpenTerminal(cwd) => open_terminal(env, cwd.path.try_into()?),
|
||||
PluginCommand::OpenTerminalFloating(cwd) => {
|
||||
open_terminal_floating(env, cwd.path.try_into()?)
|
||||
},
|
||||
PluginCommand::OpenCommandPane(command_to_run) => {
|
||||
open_command_pane(env, command_to_run)
|
||||
},
|
||||
PluginCommand::OpenCommandPaneFloating(command_to_run) => {
|
||||
open_command_pane_floating(env, command_to_run)
|
||||
},
|
||||
PluginCommand::SwitchTabTo(tab_index) => switch_tab_to(env, tab_index),
|
||||
PluginCommand::SetTimeout(seconds) => set_timeout(env, seconds),
|
||||
PluginCommand::ExecCmd(command_line) => exec_cmd(env, command_line),
|
||||
PluginCommand::PostMessageTo(plugin_message) => {
|
||||
post_message_to(env, plugin_message)?
|
||||
},
|
||||
PluginCommand::PostMessageToPlugin(plugin_message) => {
|
||||
post_message_to_plugin(env, plugin_message)?
|
||||
},
|
||||
PluginCommand::HideSelf => hide_self(env)?,
|
||||
PluginCommand::ShowSelf(should_float_if_hidden) => {
|
||||
show_self(env, should_float_if_hidden)
|
||||
},
|
||||
PluginCommand::SwitchToMode(input_mode) => {
|
||||
switch_to_mode(env, input_mode.try_into()?)
|
||||
},
|
||||
PluginCommand::NewTabsWithLayout(raw_layout) => {
|
||||
new_tabs_with_layout(env, &raw_layout)?
|
||||
},
|
||||
PluginCommand::NewTab => new_tab(env),
|
||||
PluginCommand::GoToNextTab => go_to_next_tab(env),
|
||||
PluginCommand::GoToPreviousTab => go_to_previous_tab(env),
|
||||
PluginCommand::Resize(resize_payload) => resize(env, resize_payload),
|
||||
PluginCommand::ResizeWithDirection(resize_strategy) => {
|
||||
resize_with_direction(env, resize_strategy)
|
||||
},
|
||||
PluginCommand::FocusNextPane => focus_next_pane(env),
|
||||
PluginCommand::FocusPreviousPane => focus_previous_pane(env),
|
||||
PluginCommand::MoveFocus(direction) => move_focus(env, direction),
|
||||
PluginCommand::MoveFocusOrTab(direction) => move_focus_or_tab(env, direction),
|
||||
PluginCommand::Detach => detach(env),
|
||||
PluginCommand::EditScrollback => edit_scrollback(env),
|
||||
PluginCommand::Write(bytes) => write(env, bytes),
|
||||
PluginCommand::WriteChars(chars) => write_chars(env, chars),
|
||||
PluginCommand::ToggleTab => toggle_tab(env),
|
||||
PluginCommand::MovePane => move_pane(env),
|
||||
PluginCommand::MovePaneWithDirection(direction) => {
|
||||
move_pane_with_direction(env, direction)
|
||||
},
|
||||
PluginCommand::ClearScreen => clear_screen(env),
|
||||
PluginCommand::ScrollUp => scroll_up(env),
|
||||
PluginCommand::ScrollDown => scroll_down(env),
|
||||
PluginCommand::ScrollToTop => scroll_to_top(env),
|
||||
PluginCommand::ScrollToBottom => scroll_to_bottom(env),
|
||||
PluginCommand::PageScrollUp => page_scroll_up(env),
|
||||
PluginCommand::PageScrollDown => page_scroll_down(env),
|
||||
PluginCommand::ToggleFocusFullscreen => toggle_focus_fullscreen(env),
|
||||
PluginCommand::TogglePaneFrames => toggle_pane_frames(env),
|
||||
PluginCommand::TogglePaneEmbedOrEject => toggle_pane_embed_or_eject(env),
|
||||
PluginCommand::UndoRenamePane => undo_rename_pane(env),
|
||||
PluginCommand::CloseFocus => close_focus(env),
|
||||
PluginCommand::ToggleActiveTabSync => toggle_active_tab_sync(env),
|
||||
PluginCommand::CloseFocusedTab => close_focused_tab(env),
|
||||
PluginCommand::UndoRenameTab => undo_rename_tab(env),
|
||||
PluginCommand::QuitZellij => quit_zellij(env),
|
||||
PluginCommand::PreviousSwapLayout => previous_swap_layout(env),
|
||||
PluginCommand::NextSwapLayout => next_swap_layout(env),
|
||||
PluginCommand::GoToTabName(tab_name) => go_to_tab_name(env, tab_name),
|
||||
PluginCommand::FocusOrCreateTab(tab_name) => focus_or_create_tab(env, tab_name),
|
||||
PluginCommand::GoToTab(tab_index) => go_to_tab(env, tab_index),
|
||||
PluginCommand::StartOrReloadPlugin(plugin_url) => {
|
||||
start_or_reload_plugin(env, &plugin_url)?
|
||||
},
|
||||
PluginCommand::CloseTerminalPane(terminal_pane_id) => {
|
||||
close_terminal_pane(env, terminal_pane_id)
|
||||
},
|
||||
PluginCommand::ClosePluginPane(plugin_pane_id) => {
|
||||
close_plugin_pane(env, plugin_pane_id)
|
||||
},
|
||||
PluginCommand::FocusTerminalPane(terminal_pane_id, should_float_if_hidden) => {
|
||||
focus_terminal_pane(env, terminal_pane_id, should_float_if_hidden)
|
||||
},
|
||||
PluginCommand::FocusPluginPane(plugin_pane_id, should_float_if_hidden) => {
|
||||
focus_plugin_pane(env, plugin_pane_id, should_float_if_hidden)
|
||||
},
|
||||
PluginCommand::RenameTerminalPane(terminal_pane_id, new_name) => {
|
||||
rename_terminal_pane(env, terminal_pane_id, &new_name)
|
||||
},
|
||||
PluginCommand::RenamePluginPane(plugin_pane_id, new_name) => {
|
||||
rename_plugin_pane(env, plugin_pane_id, &new_name)
|
||||
},
|
||||
PluginCommand::RenameTab(tab_index, new_name) => {
|
||||
rename_tab(env, tab_index, &new_name)
|
||||
},
|
||||
PluginCommand::ReportPanic(crash_payload) => report_panic(env, &crash_payload),
|
||||
PluginCommand::RequestPluginPermissions(permissions) => {
|
||||
request_permission(env, permissions)?
|
||||
},
|
||||
},
|
||||
PluginCommand::OpenTerminal(cwd) => open_terminal(env, cwd.path.try_into()?),
|
||||
PluginCommand::OpenTerminalFloating(cwd) => {
|
||||
open_terminal_floating(env, cwd.path.try_into()?)
|
||||
(PermissionStatus::Denied, permission) => {
|
||||
log::error!(
|
||||
"Plugin '{}' permission '{}' denied - Command '{:?}' denied",
|
||||
env.plugin_env.name(),
|
||||
permission
|
||||
.map(|p| p.to_string())
|
||||
.unwrap_or("UNKNOWN".to_owned()),
|
||||
CommandType::from_str(&command.to_string()).with_context(err_context)?
|
||||
);
|
||||
},
|
||||
PluginCommand::OpenCommandPane(command_to_run) => {
|
||||
open_command_pane(env, command_to_run)
|
||||
},
|
||||
PluginCommand::OpenCommandPaneFloating(command_to_run) => {
|
||||
open_command_pane_floating(env, command_to_run)
|
||||
},
|
||||
PluginCommand::SwitchTabTo(tab_index) => switch_tab_to(env, tab_index),
|
||||
PluginCommand::SetTimeout(seconds) => set_timeout(env, seconds),
|
||||
PluginCommand::ExecCmd(command_line) => exec_cmd(env, command_line),
|
||||
PluginCommand::PostMessageTo(plugin_message) => {
|
||||
post_message_to(env, plugin_message)?
|
||||
},
|
||||
PluginCommand::PostMessageToPlugin(plugin_message) => {
|
||||
post_message_to_plugin(env, plugin_message)?
|
||||
},
|
||||
PluginCommand::HideSelf => hide_self(env)?,
|
||||
PluginCommand::ShowSelf(should_float_if_hidden) => {
|
||||
show_self(env, should_float_if_hidden)
|
||||
},
|
||||
PluginCommand::SwitchToMode(input_mode) => {
|
||||
switch_to_mode(env, input_mode.try_into()?)
|
||||
},
|
||||
PluginCommand::NewTabsWithLayout(raw_layout) => {
|
||||
new_tabs_with_layout(env, &raw_layout)?
|
||||
},
|
||||
PluginCommand::NewTab => new_tab(env),
|
||||
PluginCommand::GoToNextTab => go_to_next_tab(env),
|
||||
PluginCommand::GoToPreviousTab => go_to_previous_tab(env),
|
||||
PluginCommand::Resize(resize_payload) => resize(env, resize_payload),
|
||||
PluginCommand::ResizeWithDirection(resize_strategy) => {
|
||||
resize_with_direction(env, resize_strategy)
|
||||
},
|
||||
PluginCommand::FocusNextPane => focus_next_pane(env),
|
||||
PluginCommand::FocusPreviousPane => focus_previous_pane(env),
|
||||
PluginCommand::MoveFocus(direction) => move_focus(env, direction),
|
||||
PluginCommand::MoveFocusOrTab(direction) => move_focus_or_tab(env, direction),
|
||||
PluginCommand::Detach => detach(env),
|
||||
PluginCommand::EditScrollback => edit_scrollback(env),
|
||||
PluginCommand::Write(bytes) => write(env, bytes),
|
||||
PluginCommand::WriteChars(chars) => write_chars(env, chars),
|
||||
PluginCommand::ToggleTab => toggle_tab(env),
|
||||
PluginCommand::MovePane => move_pane(env),
|
||||
PluginCommand::MovePaneWithDirection(direction) => {
|
||||
move_pane_with_direction(env, direction)
|
||||
},
|
||||
PluginCommand::ClearScreen => clear_screen(env),
|
||||
PluginCommand::ScrollUp => scroll_up(env),
|
||||
PluginCommand::ScrollDown => scroll_down(env),
|
||||
PluginCommand::ScrollToTop => scroll_to_top(env),
|
||||
PluginCommand::ScrollToBottom => scroll_to_bottom(env),
|
||||
PluginCommand::PageScrollUp => page_scroll_up(env),
|
||||
PluginCommand::PageScrollDown => page_scroll_down(env),
|
||||
PluginCommand::ToggleFocusFullscreen => toggle_focus_fullscreen(env),
|
||||
PluginCommand::TogglePaneFrames => toggle_pane_frames(env),
|
||||
PluginCommand::TogglePaneEmbedOrEject => toggle_pane_embed_or_eject(env),
|
||||
PluginCommand::UndoRenamePane => undo_rename_pane(env),
|
||||
PluginCommand::CloseFocus => close_focus(env),
|
||||
PluginCommand::ToggleActiveTabSync => toggle_active_tab_sync(env),
|
||||
PluginCommand::CloseFocusedTab => close_focused_tab(env),
|
||||
PluginCommand::UndoRenameTab => undo_rename_tab(env),
|
||||
PluginCommand::QuitZellij => quit_zellij(env),
|
||||
PluginCommand::PreviousSwapLayout => previous_swap_layout(env),
|
||||
PluginCommand::NextSwapLayout => next_swap_layout(env),
|
||||
PluginCommand::GoToTabName(tab_name) => go_to_tab_name(env, tab_name),
|
||||
PluginCommand::FocusOrCreateTab(tab_name) => focus_or_create_tab(env, tab_name),
|
||||
PluginCommand::GoToTab(tab_index) => go_to_tab(env, tab_index),
|
||||
PluginCommand::StartOrReloadPlugin(plugin_url) => {
|
||||
start_or_reload_plugin(env, &plugin_url)?
|
||||
},
|
||||
PluginCommand::CloseTerminalPane(terminal_pane_id) => {
|
||||
close_terminal_pane(env, terminal_pane_id)
|
||||
},
|
||||
PluginCommand::ClosePluginPane(plugin_pane_id) => {
|
||||
close_plugin_pane(env, plugin_pane_id)
|
||||
},
|
||||
PluginCommand::FocusTerminalPane(terminal_pane_id, should_float_if_hidden) => {
|
||||
focus_terminal_pane(env, terminal_pane_id, should_float_if_hidden)
|
||||
},
|
||||
PluginCommand::FocusPluginPane(plugin_pane_id, should_float_if_hidden) => {
|
||||
focus_plugin_pane(env, plugin_pane_id, should_float_if_hidden)
|
||||
},
|
||||
PluginCommand::RenameTerminalPane(terminal_pane_id, new_name) => {
|
||||
rename_terminal_pane(env, terminal_pane_id, &new_name)
|
||||
},
|
||||
PluginCommand::RenamePluginPane(plugin_pane_id, new_name) => {
|
||||
rename_plugin_pane(env, plugin_pane_id, &new_name)
|
||||
},
|
||||
PluginCommand::RenameTab(tab_index, new_name) => {
|
||||
rename_tab(env, tab_index, &new_name)
|
||||
},
|
||||
PluginCommand::ReportPanic(crash_payload) => report_panic(env, &crash_payload),
|
||||
}
|
||||
};
|
||||
Ok(())
|
||||
})
|
||||
.with_context(|| format!("failed to run plugin command {}", env.plugin_env.name()))
|
||||
|
|
@ -255,6 +274,30 @@ fn set_selectable(env: &ForeignFunctionEnv, selectable: bool) {
|
|||
}
|
||||
}
|
||||
|
||||
fn request_permission(env: &ForeignFunctionEnv, permissions: Vec<PermissionType>) -> Result<()> {
|
||||
if PermissionCache::from_path_or_default(None)
|
||||
.check_permissions(env.plugin_env.plugin.location.to_string(), &permissions)
|
||||
{
|
||||
return env
|
||||
.plugin_env
|
||||
.senders
|
||||
.send_to_plugin(PluginInstruction::PermissionRequestResult(
|
||||
env.plugin_env.plugin_id,
|
||||
Some(env.plugin_env.client_id),
|
||||
permissions.to_vec(),
|
||||
PermissionStatus::Granted,
|
||||
None,
|
||||
));
|
||||
}
|
||||
|
||||
env.plugin_env
|
||||
.senders
|
||||
.send_to_screen(ScreenInstruction::RequestPluginPermissions(
|
||||
env.plugin_env.plugin_id,
|
||||
PluginPermission::new(env.plugin_env.plugin.location.to_string(), permissions),
|
||||
))
|
||||
}
|
||||
|
||||
fn get_plugin_ids(env: &ForeignFunctionEnv) {
|
||||
let ids = PluginIds {
|
||||
plugin_id: env.plugin_env.plugin_id,
|
||||
|
|
@ -1001,3 +1044,81 @@ pub fn wasi_read_bytes(wasi_env: &WasiEnv) -> Result<Vec<u8>> {
|
|||
.and_then(|string| serde_json::from_str(&string).map_err(anyError::new))
|
||||
.with_context(|| format!("failed to deserialize object from WASI env '{wasi_env:?}'"))
|
||||
}
|
||||
|
||||
// TODO: move to permissions?
|
||||
fn check_command_permission(
|
||||
plugin_env: &PluginEnv,
|
||||
command: &PluginCommand,
|
||||
) -> (PermissionStatus, Option<PermissionType>) {
|
||||
if plugin_env.plugin.is_builtin() {
|
||||
// built-in plugins can do all the things because they're part of the application and
|
||||
// there's no use to deny them anything
|
||||
return (PermissionStatus::Granted, None);
|
||||
}
|
||||
let permission = match command {
|
||||
PluginCommand::OpenFile(..) | PluginCommand::OpenFileFloating(..) => {
|
||||
PermissionType::OpenFiles
|
||||
},
|
||||
PluginCommand::OpenTerminal(..)
|
||||
| PluginCommand::StartOrReloadPlugin(..)
|
||||
| PluginCommand::OpenTerminalFloating(..) => PermissionType::OpenTerminalsOrPlugins,
|
||||
PluginCommand::OpenCommandPane(..)
|
||||
| PluginCommand::OpenCommandPaneFloating(..)
|
||||
| PluginCommand::ExecCmd(..) => PermissionType::RunCommands,
|
||||
PluginCommand::Write(..) | PluginCommand::WriteChars(..) => PermissionType::WriteToStdin,
|
||||
PluginCommand::SwitchTabTo(..)
|
||||
| PluginCommand::SwitchToMode(..)
|
||||
| PluginCommand::NewTabsWithLayout(..)
|
||||
| PluginCommand::NewTab
|
||||
| PluginCommand::GoToNextTab
|
||||
| PluginCommand::GoToPreviousTab
|
||||
| PluginCommand::Resize(..)
|
||||
| PluginCommand::ResizeWithDirection(..)
|
||||
| PluginCommand::FocusNextPane
|
||||
| PluginCommand::MoveFocus(..)
|
||||
| PluginCommand::MoveFocusOrTab(..)
|
||||
| PluginCommand::Detach
|
||||
| PluginCommand::EditScrollback
|
||||
| PluginCommand::ToggleTab
|
||||
| PluginCommand::MovePane
|
||||
| PluginCommand::MovePaneWithDirection(..)
|
||||
| PluginCommand::ClearScreen
|
||||
| PluginCommand::ScrollUp
|
||||
| PluginCommand::ScrollDown
|
||||
| PluginCommand::ScrollToTop
|
||||
| PluginCommand::ScrollToBottom
|
||||
| PluginCommand::PageScrollUp
|
||||
| PluginCommand::PageScrollDown
|
||||
| PluginCommand::ToggleFocusFullscreen
|
||||
| PluginCommand::TogglePaneFrames
|
||||
| PluginCommand::TogglePaneEmbedOrEject
|
||||
| PluginCommand::UndoRenamePane
|
||||
| PluginCommand::CloseFocus
|
||||
| PluginCommand::ToggleActiveTabSync
|
||||
| PluginCommand::CloseFocusedTab
|
||||
| PluginCommand::UndoRenameTab
|
||||
| PluginCommand::QuitZellij
|
||||
| PluginCommand::PreviousSwapLayout
|
||||
| PluginCommand::NextSwapLayout
|
||||
| PluginCommand::GoToTabName(..)
|
||||
| PluginCommand::FocusOrCreateTab(..)
|
||||
| PluginCommand::GoToTab(..)
|
||||
| PluginCommand::CloseTerminalPane(..)
|
||||
| PluginCommand::ClosePluginPane(..)
|
||||
| PluginCommand::FocusTerminalPane(..)
|
||||
| PluginCommand::FocusPluginPane(..)
|
||||
| PluginCommand::RenameTerminalPane(..)
|
||||
| PluginCommand::RenamePluginPane(..)
|
||||
| PluginCommand::RenameTab(..) => PermissionType::ChangeApplicationState,
|
||||
_ => return (PermissionStatus::Granted, None),
|
||||
};
|
||||
|
||||
log::info!("plugin permissions: {:?}", plugin_env.permissions);
|
||||
if let Some(permissions) = plugin_env.permissions.lock().unwrap().as_ref() {
|
||||
if permissions.contains(&permission) {
|
||||
return (PermissionStatus::Granted, None);
|
||||
}
|
||||
}
|
||||
|
||||
(PermissionStatus::Denied, Some(permission))
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ use std::path::PathBuf;
|
|||
use std::rc::Rc;
|
||||
use std::str;
|
||||
|
||||
use zellij_utils::data::{Direction, PaneManifest, Resize, ResizeStrategy};
|
||||
use zellij_utils::data::{Direction, PaneManifest, PluginPermission, Resize, ResizeStrategy};
|
||||
use zellij_utils::errors::prelude::*;
|
||||
use zellij_utils::input::command::RunCommand;
|
||||
use zellij_utils::input::options::Clipboard;
|
||||
|
|
@ -280,6 +280,10 @@ pub enum ScreenInstruction {
|
|||
FocusPaneWithId(PaneId, bool, ClientId), // bool is should_float
|
||||
RenamePane(PaneId, Vec<u8>),
|
||||
RenameTab(usize, Vec<u8>),
|
||||
RequestPluginPermissions(
|
||||
u32, // u32 - plugin_id
|
||||
PluginPermission,
|
||||
),
|
||||
BreakPane(Box<Layout>, Option<TerminalAction>, ClientId),
|
||||
BreakPaneRight(ClientId),
|
||||
BreakPaneLeft(ClientId),
|
||||
|
|
@ -450,6 +454,9 @@ impl From<&ScreenInstruction> for ScreenContext {
|
|||
ScreenInstruction::FocusPaneWithId(..) => ScreenContext::FocusPaneWithId,
|
||||
ScreenInstruction::RenamePane(..) => ScreenContext::RenamePane,
|
||||
ScreenInstruction::RenameTab(..) => ScreenContext::RenameTab,
|
||||
ScreenInstruction::RequestPluginPermissions(..) => {
|
||||
ScreenContext::RequestPluginPermissions
|
||||
},
|
||||
ScreenInstruction::BreakPane(..) => ScreenContext::BreakPane,
|
||||
ScreenInstruction::BreakPaneRight(..) => ScreenContext::BreakPaneRight,
|
||||
ScreenInstruction::BreakPaneLeft(..) => ScreenContext::BreakPaneLeft,
|
||||
|
|
@ -2958,6 +2965,24 @@ pub(crate) fn screen_thread_main(
|
|||
}
|
||||
screen.report_tab_state()?;
|
||||
},
|
||||
ScreenInstruction::RequestPluginPermissions(plugin_id, plugin_permission) => {
|
||||
let all_tabs = screen.get_tabs_mut();
|
||||
let found = all_tabs.values_mut().any(|tab| {
|
||||
if tab.has_plugin(plugin_id) {
|
||||
tab.request_plugin_permissions(plugin_id, Some(plugin_permission.clone()));
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
});
|
||||
|
||||
if !found {
|
||||
log::error!(
|
||||
"PluginId '{}' not found - cannot request permissions",
|
||||
plugin_id
|
||||
);
|
||||
}
|
||||
},
|
||||
ScreenInstruction::BreakPane(default_layout, default_shell, client_id) => {
|
||||
screen.break_pane(default_shell, default_layout, client_id)?;
|
||||
},
|
||||
|
|
|
|||
|
|
@ -9,7 +9,9 @@ mod swap_layouts;
|
|||
use copy_command::CopyCommand;
|
||||
use std::env::temp_dir;
|
||||
use uuid::Uuid;
|
||||
use zellij_utils::data::{Direction, PaneInfo, ResizeStrategy};
|
||||
use zellij_utils::data::{
|
||||
Direction, PaneInfo, PermissionStatus, PermissionType, PluginPermission, ResizeStrategy,
|
||||
};
|
||||
use zellij_utils::errors::prelude::*;
|
||||
use zellij_utils::input::command::RunCommand;
|
||||
use zellij_utils::position::{Column, Line};
|
||||
|
|
@ -220,6 +222,7 @@ pub trait Pane {
|
|||
fn set_should_render_boundaries(&mut self, _should_render: bool) {}
|
||||
fn selectable(&self) -> bool;
|
||||
fn set_selectable(&mut self, selectable: bool);
|
||||
fn request_permissions_from_user(&mut self, _permissions: Option<PluginPermission>) {}
|
||||
fn render(
|
||||
&mut self,
|
||||
client_id: Option<ClientId>,
|
||||
|
|
@ -473,6 +476,7 @@ pub trait Pane {
|
|||
pub enum AdjustedInput {
|
||||
WriteBytesToTerminal(Vec<u8>),
|
||||
ReRunCommandInThisPane(RunCommand),
|
||||
PermissionRequestResult(Vec<PermissionType>, PermissionStatus),
|
||||
CloseThisPane,
|
||||
}
|
||||
pub fn get_next_terminal_position(
|
||||
|
|
@ -1560,17 +1564,35 @@ impl Tab {
|
|||
self.close_pane(PaneId::Terminal(active_terminal_id), false, None);
|
||||
should_update_ui = true;
|
||||
},
|
||||
Some(_) => {},
|
||||
None => {},
|
||||
}
|
||||
},
|
||||
PaneId::Plugin(pid) => {
|
||||
let mut plugin_updates = vec![];
|
||||
for key in parse_keys(&input_bytes) {
|
||||
plugin_updates.push((Some(pid), client_id, Event::Key(key)));
|
||||
}
|
||||
self.senders
|
||||
.send_to_plugin(PluginInstruction::Update(plugin_updates))
|
||||
.with_context(err_context)?;
|
||||
PaneId::Plugin(pid) => match active_terminal.adjust_input_to_terminal(input_bytes) {
|
||||
Some(AdjustedInput::WriteBytesToTerminal(adjusted_input)) => {
|
||||
let mut plugin_updates = vec![];
|
||||
for key in parse_keys(&adjusted_input) {
|
||||
plugin_updates.push((Some(pid), client_id, Event::Key(key)));
|
||||
}
|
||||
self.senders
|
||||
.send_to_plugin(PluginInstruction::Update(plugin_updates))
|
||||
.with_context(err_context)?;
|
||||
},
|
||||
Some(AdjustedInput::PermissionRequestResult(permissions, status)) => {
|
||||
self.request_plugin_permissions(pid, None);
|
||||
self.senders
|
||||
.send_to_plugin(PluginInstruction::PermissionRequestResult(
|
||||
pid,
|
||||
client_id,
|
||||
permissions,
|
||||
status,
|
||||
None,
|
||||
))
|
||||
.with_context(err_context)?;
|
||||
should_update_ui = true;
|
||||
},
|
||||
Some(_) => {},
|
||||
None => {},
|
||||
},
|
||||
}
|
||||
Ok(should_update_ui)
|
||||
|
|
@ -3437,6 +3459,20 @@ impl Tab {
|
|||
}
|
||||
Ok(())
|
||||
}
|
||||
pub fn request_plugin_permissions(&mut self, pid: u32, permissions: Option<PluginPermission>) {
|
||||
if let Some(plugin_pane) = self
|
||||
.tiled_panes
|
||||
.get_pane_mut(PaneId::Plugin(pid))
|
||||
.or_else(|| self.floating_panes.get_pane_mut(PaneId::Plugin(pid)))
|
||||
.or_else(|| {
|
||||
self.suppressed_panes
|
||||
.values_mut()
|
||||
.find(|s_p| s_p.pid() == PaneId::Plugin(pid))
|
||||
})
|
||||
{
|
||||
plugin_pane.request_permissions_from_user(permissions);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn pane_info_for_pane(pane_id: &PaneId, pane: &Box<dyn Pane>) -> PaneInfo {
|
||||
|
|
|
|||
|
|
@ -107,6 +107,7 @@ macro_rules! register_plugin {
|
|||
fn load() {
|
||||
STATE.with(|state| {
|
||||
use std::collections::BTreeMap;
|
||||
use std::convert::TryFrom;
|
||||
use std::convert::TryInto;
|
||||
use zellij_tile::shim::plugin_api::action::ProtobufPluginConfiguration;
|
||||
use zellij_tile::shim::prost::Message;
|
||||
|
|
|
|||
|
|
@ -39,6 +39,13 @@ pub fn set_selectable(selectable: bool) {
|
|||
unsafe { host_run_plugin_command() };
|
||||
}
|
||||
|
||||
pub fn request_permission(permissions: &[PermissionType]) {
|
||||
let plugin_command = PluginCommand::RequestPluginPermissions(permissions.into());
|
||||
let protobuf_plugin_command: ProtobufPluginCommand = plugin_command.try_into().unwrap();
|
||||
object_to_stdout(&protobuf_plugin_command.encode_to_vec());
|
||||
unsafe { host_run_plugin_command() };
|
||||
}
|
||||
|
||||
// Query Functions
|
||||
/// Returns the unique Zellij pane ID for the plugin as well as the Zellij process id.
|
||||
pub fn get_plugin_ids() -> PluginIds {
|
||||
|
|
|
|||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
|
@ -36,6 +36,8 @@ lazy_static! {
|
|||
.cache_dir()
|
||||
.to_path_buf()
|
||||
.join(format!("{}", Uuid::new_v4()));
|
||||
pub static ref ZELLIJ_PLUGIN_PERMISSIONS_CACHE: PathBuf =
|
||||
ZELLIJ_CACHE_DIR.join("permissions.kdl");
|
||||
}
|
||||
|
||||
pub const FEATURES: &[&str] = &[
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ use std::collections::{HashMap, HashSet};
|
|||
use std::fmt;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::str::FromStr;
|
||||
use strum_macros::{EnumDiscriminants, EnumIter, EnumString, ToString};
|
||||
use strum_macros::{Display, EnumDiscriminants, EnumIter, EnumString, ToString};
|
||||
|
||||
pub type ClientId = u16; // TODO: merge with crate type?
|
||||
|
||||
|
|
@ -493,6 +493,63 @@ pub enum Event {
|
|||
FileSystemUpdate(Vec<PathBuf>),
|
||||
/// A file was deleted somewhere in the Zellij CWD folder
|
||||
FileSystemDelete(Vec<PathBuf>),
|
||||
/// A Result of plugin permission request
|
||||
PermissionRequestResult(PermissionStatus),
|
||||
}
|
||||
|
||||
#[derive(
|
||||
Debug,
|
||||
PartialEq,
|
||||
Eq,
|
||||
Hash,
|
||||
Copy,
|
||||
Clone,
|
||||
EnumDiscriminants,
|
||||
ToString,
|
||||
Serialize,
|
||||
Deserialize,
|
||||
PartialOrd,
|
||||
Ord,
|
||||
)]
|
||||
#[strum_discriminants(derive(EnumString, Hash, Serialize, Deserialize, Display, PartialOrd, Ord))]
|
||||
#[strum_discriminants(name(PermissionType))]
|
||||
#[non_exhaustive]
|
||||
pub enum Permission {
|
||||
ReadApplicationState,
|
||||
ChangeApplicationState,
|
||||
OpenFiles,
|
||||
RunCommands,
|
||||
OpenTerminalsOrPlugins,
|
||||
WriteToStdin,
|
||||
}
|
||||
|
||||
impl PermissionType {
|
||||
pub fn display_name(&self) -> String {
|
||||
match self {
|
||||
PermissionType::ReadApplicationState => {
|
||||
"Access Zellij state (Panes, Tabs and UI)".to_owned()
|
||||
},
|
||||
PermissionType::ChangeApplicationState => {
|
||||
"Change Zellij state (Panes, Tabs and UI)".to_owned()
|
||||
},
|
||||
PermissionType::OpenFiles => "Open files (eg. for editing)".to_owned(),
|
||||
PermissionType::RunCommands => "Run commands".to_owned(),
|
||||
PermissionType::OpenTerminalsOrPlugins => "Start new terminals and plugins".to_owned(),
|
||||
PermissionType::WriteToStdin => "Write to standard input (STDIN)".to_owned(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct PluginPermission {
|
||||
pub name: String,
|
||||
pub permissions: Vec<PermissionType>,
|
||||
}
|
||||
|
||||
impl PluginPermission {
|
||||
pub fn new(name: String, permissions: Vec<PermissionType>) -> Self {
|
||||
PluginPermission { name, permissions }
|
||||
}
|
||||
}
|
||||
|
||||
/// Describes the different input modes, which change the way that keystrokes will be interpreted.
|
||||
|
|
@ -811,6 +868,12 @@ pub enum CopyDestination {
|
|||
System,
|
||||
}
|
||||
|
||||
#[derive(Debug, Copy, Clone, PartialEq, Serialize, Deserialize)]
|
||||
pub enum PermissionStatus {
|
||||
Granted,
|
||||
Denied,
|
||||
}
|
||||
|
||||
#[derive(Debug, Default, Clone)]
|
||||
pub struct FileToOpen {
|
||||
pub path: PathBuf,
|
||||
|
|
@ -882,7 +945,9 @@ impl PluginMessage {
|
|||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
#[derive(Debug, Clone, EnumDiscriminants, ToString)]
|
||||
#[strum_discriminants(derive(EnumString, Hash, Serialize, Deserialize))]
|
||||
#[strum_discriminants(name(CommandType))]
|
||||
pub enum PluginCommand {
|
||||
Subscribe(HashSet<EventType>),
|
||||
Unsubscribe(HashSet<EventType>),
|
||||
|
|
@ -950,4 +1015,5 @@ pub enum PluginCommand {
|
|||
RenamePluginPane(u32, String), // plugin pane id, new name
|
||||
RenameTab(u32, String), // tab index, new name
|
||||
ReportPanic(String), // stringified panic
|
||||
RequestPluginPermissions(Vec<PermissionType>),
|
||||
}
|
||||
|
|
|
|||
|
|
@ -338,6 +338,7 @@ pub enum ScreenContext {
|
|||
FocusPaneWithId,
|
||||
RenamePane,
|
||||
RenameTab,
|
||||
RequestPluginPermissions,
|
||||
BreakPane,
|
||||
BreakPaneRight,
|
||||
BreakPaneLeft,
|
||||
|
|
@ -377,6 +378,7 @@ pub enum PluginContext {
|
|||
PostMessageToPluginWorker,
|
||||
PostMessageToPlugin,
|
||||
PluginSubscribedToEvents,
|
||||
PermissionRequestResult,
|
||||
}
|
||||
|
||||
/// Stack call representations corresponding to the different types of [`ClientInstruction`]s.
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ pub mod config;
|
|||
pub mod keybinds;
|
||||
pub mod layout;
|
||||
pub mod options;
|
||||
pub mod permission;
|
||||
pub mod plugins;
|
||||
pub mod theme;
|
||||
|
||||
|
|
|
|||
60
zellij-utils/src/input/permission.rs
Normal file
60
zellij-utils/src/input/permission.rs
Normal file
|
|
@ -0,0 +1,60 @@
|
|||
use std::{
|
||||
collections::HashMap,
|
||||
fs::{self, File},
|
||||
io::Write,
|
||||
path::PathBuf,
|
||||
};
|
||||
|
||||
use crate::{consts::ZELLIJ_PLUGIN_PERMISSIONS_CACHE, data::PermissionType};
|
||||
|
||||
pub type GrantedPermission = HashMap<String, Vec<PermissionType>>;
|
||||
|
||||
#[derive(Default, Debug)]
|
||||
pub struct PermissionCache {
|
||||
path: PathBuf,
|
||||
granted: GrantedPermission,
|
||||
}
|
||||
|
||||
impl PermissionCache {
|
||||
pub fn cache(&mut self, plugin_name: String, permissions: Vec<PermissionType>) {
|
||||
self.granted.insert(plugin_name, permissions);
|
||||
}
|
||||
|
||||
pub fn get_permissions(&self, plugin_name: String) -> Option<&Vec<PermissionType>> {
|
||||
self.granted.get(&plugin_name)
|
||||
}
|
||||
|
||||
pub fn check_permissions(
|
||||
&self,
|
||||
plugin_name: String,
|
||||
permissions: &Vec<PermissionType>,
|
||||
) -> bool {
|
||||
if let Some(target) = self.granted.get(&plugin_name) {
|
||||
if target == permissions {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
false
|
||||
}
|
||||
|
||||
pub fn from_path_or_default(cache_path: Option<PathBuf>) -> Self {
|
||||
let cache_path = cache_path.unwrap_or(ZELLIJ_PLUGIN_PERMISSIONS_CACHE.to_path_buf());
|
||||
|
||||
let granted = match fs::read_to_string(cache_path.clone()) {
|
||||
Ok(raw_string) => PermissionCache::from_string(raw_string).unwrap_or_default(),
|
||||
Err(_) => GrantedPermission::default(),
|
||||
};
|
||||
|
||||
PermissionCache {
|
||||
path: cache_path,
|
||||
granted,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn write_to_file(&self) -> std::io::Result<()> {
|
||||
let mut f = File::create(&self.path)?;
|
||||
write!(f, "{}", PermissionCache::to_string(&self.granted))?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
|
@ -1,15 +1,16 @@
|
|||
mod kdl_layout_parser;
|
||||
use crate::data::{Direction, InputMode, Key, Palette, PaletteColor, Resize};
|
||||
use crate::data::{Direction, InputMode, Key, Palette, PaletteColor, PermissionType, Resize};
|
||||
use crate::envs::EnvironmentVariables;
|
||||
use crate::input::config::{Config, ConfigError, KdlError};
|
||||
use crate::input::keybinds::Keybinds;
|
||||
use crate::input::layout::{Layout, PluginUserConfiguration, RunPlugin, RunPluginLocation};
|
||||
use crate::input::options::{Clipboard, OnForceClose, Options};
|
||||
use crate::input::permission::{GrantedPermission, PermissionCache};
|
||||
use crate::input::plugins::{PluginConfig, PluginTag, PluginType, PluginsConfig};
|
||||
use crate::input::theme::{FrameConfig, Theme, Themes, UiConfig};
|
||||
use crate::setup::{find_default_config_dir, get_layout_dir};
|
||||
use kdl_layout_parser::KdlLayoutParser;
|
||||
use std::collections::{BTreeMap, HashMap};
|
||||
use std::collections::{BTreeMap, HashMap, HashSet};
|
||||
use strum::IntoEnumIterator;
|
||||
|
||||
use miette::NamedSource;
|
||||
|
|
@ -1793,6 +1794,53 @@ impl Themes {
|
|||
}
|
||||
}
|
||||
|
||||
impl PermissionCache {
|
||||
pub fn from_string(raw_string: String) -> Result<GrantedPermission, ConfigError> {
|
||||
let kdl_document: KdlDocument = raw_string.parse()?;
|
||||
|
||||
let mut granted_permission = GrantedPermission::default();
|
||||
|
||||
for node in kdl_document.nodes() {
|
||||
if let Some(children) = node.children() {
|
||||
let key = kdl_name!(node);
|
||||
let permissions: Vec<PermissionType> = children
|
||||
.nodes()
|
||||
.iter()
|
||||
.filter_map(|p| {
|
||||
let v = kdl_name!(p);
|
||||
PermissionType::from_str(v).ok()
|
||||
})
|
||||
.collect();
|
||||
|
||||
granted_permission.insert(key.into(), permissions);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(granted_permission)
|
||||
}
|
||||
|
||||
pub fn to_string(granted: &GrantedPermission) -> String {
|
||||
let mut kdl_doucment = KdlDocument::new();
|
||||
|
||||
granted.iter().for_each(|(k, v)| {
|
||||
let mut node = KdlNode::new(k.as_str());
|
||||
let mut children = KdlDocument::new();
|
||||
|
||||
let permissions: HashSet<PermissionType> = v.clone().into_iter().collect();
|
||||
permissions.iter().for_each(|f| {
|
||||
let n = KdlNode::new(f.to_string().as_str());
|
||||
children.nodes_mut().push(n);
|
||||
});
|
||||
|
||||
node.set_children(children);
|
||||
kdl_doucment.nodes_mut().push(node);
|
||||
});
|
||||
|
||||
kdl_doucment.fmt();
|
||||
kdl_doucment.to_string()
|
||||
}
|
||||
}
|
||||
|
||||
pub fn parse_plugin_user_configuration(
|
||||
plugin_block: &KdlNode,
|
||||
) -> Result<BTreeMap<String, String>, ConfigError> {
|
||||
|
|
|
|||
|
|
@ -38,6 +38,7 @@ enum EventType {
|
|||
FileSystemUpdate = 13;
|
||||
/// A file was deleted somewhere in the Zellij CWD folder
|
||||
FileSystemDelete = 14;
|
||||
PermissionRequestResult = 15;
|
||||
}
|
||||
|
||||
message EventNameList {
|
||||
|
|
@ -57,9 +58,14 @@ message Event {
|
|||
bool visible_payload = 9;
|
||||
CustomMessagePayload custom_message_payload = 10;
|
||||
FileListPayload file_list_payload = 11;
|
||||
PermissionRequestResultPayload permission_request_result_payload = 12;
|
||||
}
|
||||
}
|
||||
|
||||
message PermissionRequestResultPayload {
|
||||
bool granted = 1;
|
||||
}
|
||||
|
||||
message FileListPayload {
|
||||
repeated string paths = 1;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -13,9 +13,10 @@ pub use super::generated_api::api::{
|
|||
style::Style as ProtobufStyle,
|
||||
};
|
||||
use crate::data::{
|
||||
CopyDestination, Direction, Event, EventType, InputMode, Key, ModeInfo, Mouse, Palette,
|
||||
PaletteColor, PaneInfo, PaneManifest, PluginCapabilities, Style, TabInfo, ThemeHue,
|
||||
CopyDestination, Event, EventType, InputMode, Key, ModeInfo, Mouse, PaneInfo, PaneManifest,
|
||||
PermissionStatus, PluginCapabilities, Style, TabInfo,
|
||||
};
|
||||
|
||||
use crate::errors::prelude::*;
|
||||
use crate::input::actions::Action;
|
||||
|
||||
|
|
@ -160,6 +161,16 @@ impl TryFrom<ProtobufEvent> for Event {
|
|||
},
|
||||
_ => Err("Malformed payload for the file system delete Event"),
|
||||
},
|
||||
Some(ProtobufEventType::PermissionRequestResult) => match protobuf_event.payload {
|
||||
Some(ProtobufEventPayload::PermissionRequestResultPayload(payload)) => {
|
||||
if payload.granted {
|
||||
Ok(Event::PermissionRequestResult(PermissionStatus::Granted))
|
||||
} else {
|
||||
Ok(Event::PermissionRequestResult(PermissionStatus::Denied))
|
||||
}
|
||||
},
|
||||
_ => Err("Malformed payload for the file system delete Event"),
|
||||
},
|
||||
None => Err("Unknown Protobuf Event"),
|
||||
}
|
||||
}
|
||||
|
|
@ -290,6 +301,18 @@ impl TryFrom<Event> for ProtobufEvent {
|
|||
payload: Some(event::Payload::FileListPayload(file_list_payload)),
|
||||
})
|
||||
},
|
||||
Event::PermissionRequestResult(permission_status) => {
|
||||
let granted = match permission_status {
|
||||
PermissionStatus::Granted => true,
|
||||
PermissionStatus::Denied => false,
|
||||
};
|
||||
Ok(ProtobufEvent {
|
||||
name: ProtobufEventType::PermissionRequestResult as i32,
|
||||
payload: Some(event::Payload::PermissionRequestResultPayload(
|
||||
PermissionRequestResultPayload { granted },
|
||||
)),
|
||||
})
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -673,6 +696,7 @@ impl TryFrom<ProtobufEventType> for EventType {
|
|||
ProtobufEventType::FileSystemRead => EventType::FileSystemRead,
|
||||
ProtobufEventType::FileSystemUpdate => EventType::FileSystemUpdate,
|
||||
ProtobufEventType::FileSystemDelete => EventType::FileSystemDelete,
|
||||
ProtobufEventType::PermissionRequestResult => EventType::PermissionRequestResult,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
@ -696,6 +720,7 @@ impl TryFrom<EventType> for ProtobufEventType {
|
|||
EventType::FileSystemRead => ProtobufEventType::FileSystemRead,
|
||||
EventType::FileSystemUpdate => ProtobufEventType::FileSystemUpdate,
|
||||
EventType::FileSystemDelete => ProtobufEventType::FileSystemDelete,
|
||||
EventType::PermissionRequestResult => ProtobufEventType::PermissionRequestResult,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
@ -717,6 +742,7 @@ fn serialize_mode_update_event() {
|
|||
|
||||
#[test]
|
||||
fn serialize_mode_update_event_with_non_default_values() {
|
||||
use crate::data::{Direction, Palette, PaletteColor, ThemeHue};
|
||||
use prost::Message;
|
||||
let mode_update_event = Event::ModeUpdate(ModeInfo {
|
||||
mode: InputMode::Locked,
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ pub mod key;
|
|||
pub mod message;
|
||||
pub mod plugin_command;
|
||||
pub mod plugin_ids;
|
||||
pub mod plugin_permission;
|
||||
pub mod resize;
|
||||
pub mod style;
|
||||
pub mod generated_api {
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ import "command.proto";
|
|||
import "message.proto";
|
||||
import "input_mode.proto";
|
||||
import "resize.proto";
|
||||
import "plugin_permission.proto";
|
||||
|
||||
package api.plugin_command;
|
||||
|
||||
|
|
@ -76,6 +77,7 @@ enum CommandName {
|
|||
RenamePluginPane = 63;
|
||||
RenameTab = 64;
|
||||
ReportCrash = 65;
|
||||
RequestPluginPermissions = 66;
|
||||
}
|
||||
|
||||
message PluginCommand {
|
||||
|
|
@ -117,9 +119,14 @@ message PluginCommand {
|
|||
IdAndNewName rename_plugin_pane_payload = 35;
|
||||
IdAndNewName rename_tab_payload = 36;
|
||||
string report_crash_payload = 37;
|
||||
RequestPluginPermissionPayload request_plugin_permission_payload = 38;
|
||||
}
|
||||
}
|
||||
|
||||
message RequestPluginPermissionPayload {
|
||||
repeated plugin_permission.PermissionType permissions = 1;
|
||||
}
|
||||
|
||||
message SubscribePayload {
|
||||
event.EventNameList subscriptions = 1;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,14 +3,15 @@ pub use super::generated_api::api::{
|
|||
plugin_command::{
|
||||
plugin_command::Payload, CommandName, ExecCmdPayload, IdAndNewName, MovePayload,
|
||||
OpenCommandPanePayload, OpenFilePayload, PaneIdAndShouldFloat,
|
||||
PluginCommand as ProtobufPluginCommand, PluginMessagePayload, ResizePayload,
|
||||
SetTimeoutPayload, SubscribePayload, SwitchTabToPayload, SwitchToModePayload,
|
||||
UnsubscribePayload,
|
||||
PluginCommand as ProtobufPluginCommand, PluginMessagePayload,
|
||||
RequestPluginPermissionPayload, ResizePayload, SetTimeoutPayload, SubscribePayload,
|
||||
SwitchTabToPayload, SwitchToModePayload, UnsubscribePayload,
|
||||
},
|
||||
plugin_permission::PermissionType as ProtobufPermissionType,
|
||||
resize::ResizeAction as ProtobufResizeAction,
|
||||
};
|
||||
|
||||
use crate::data::PluginCommand;
|
||||
use crate::data::{PermissionType, PluginCommand};
|
||||
|
||||
use std::convert::TryFrom;
|
||||
|
||||
|
|
@ -486,6 +487,19 @@ impl TryFrom<ProtobufPluginCommand> for PluginCommand {
|
|||
},
|
||||
_ => Err("Mismatched payload for ReportCrash"),
|
||||
},
|
||||
Some(CommandName::RequestPluginPermissions) => match protobuf_plugin_command.payload {
|
||||
Some(Payload::RequestPluginPermissionPayload(payload)) => {
|
||||
Ok(PluginCommand::RequestPluginPermissions(
|
||||
payload
|
||||
.permissions
|
||||
.iter()
|
||||
.filter_map(|p| ProtobufPermissionType::from_i32(*p))
|
||||
.filter_map(|p| PermissionType::try_from(p).ok())
|
||||
.collect(),
|
||||
))
|
||||
},
|
||||
_ => Err("Mismatched payload for RequestPluginPermission"),
|
||||
},
|
||||
None => Err("Unrecognized plugin command"),
|
||||
}
|
||||
}
|
||||
|
|
@ -820,6 +834,18 @@ impl TryFrom<PluginCommand> for ProtobufPluginCommand {
|
|||
name: CommandName::ReportCrash as i32,
|
||||
payload: Some(Payload::ReportCrashPayload(payload)),
|
||||
}),
|
||||
PluginCommand::RequestPluginPermissions(permissions) => Ok(ProtobufPluginCommand {
|
||||
name: CommandName::RequestPluginPermissions as i32,
|
||||
payload: Some(Payload::RequestPluginPermissionPayload(
|
||||
RequestPluginPermissionPayload {
|
||||
permissions: permissions
|
||||
.iter()
|
||||
.filter_map(|p| ProtobufPermissionType::try_from(*p).ok())
|
||||
.map(|p| p as i32)
|
||||
.collect(),
|
||||
},
|
||||
)),
|
||||
}),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
12
zellij-utils/src/plugin_api/plugin_permission.proto
Normal file
12
zellij-utils/src/plugin_api/plugin_permission.proto
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
syntax = "proto3";
|
||||
|
||||
package api.plugin_permission;
|
||||
|
||||
enum PermissionType {
|
||||
ReadApplicationState = 0;
|
||||
ChangeApplicationState = 1;
|
||||
OpenFiles = 2;
|
||||
RunCommands = 3;
|
||||
OpenTerminalsOrPlugins = 4;
|
||||
WriteToStdin = 5;
|
||||
}
|
||||
44
zellij-utils/src/plugin_api/plugin_permission.rs
Normal file
44
zellij-utils/src/plugin_api/plugin_permission.rs
Normal file
|
|
@ -0,0 +1,44 @@
|
|||
pub use super::generated_api::api::plugin_permission::PermissionType as ProtobufPermissionType;
|
||||
use crate::data::PermissionType;
|
||||
|
||||
use std::convert::TryFrom;
|
||||
|
||||
impl TryFrom<ProtobufPermissionType> for PermissionType {
|
||||
type Error = &'static str;
|
||||
fn try_from(protobuf_permission: ProtobufPermissionType) -> Result<Self, &'static str> {
|
||||
match protobuf_permission {
|
||||
ProtobufPermissionType::ReadApplicationState => {
|
||||
Ok(PermissionType::ReadApplicationState)
|
||||
},
|
||||
ProtobufPermissionType::ChangeApplicationState => {
|
||||
Ok(PermissionType::ChangeApplicationState)
|
||||
},
|
||||
ProtobufPermissionType::OpenFiles => Ok(PermissionType::OpenFiles),
|
||||
ProtobufPermissionType::RunCommands => Ok(PermissionType::RunCommands),
|
||||
ProtobufPermissionType::OpenTerminalsOrPlugins => {
|
||||
Ok(PermissionType::OpenTerminalsOrPlugins)
|
||||
},
|
||||
ProtobufPermissionType::WriteToStdin => Ok(PermissionType::WriteToStdin),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<PermissionType> for ProtobufPermissionType {
|
||||
type Error = &'static str;
|
||||
fn try_from(permission: PermissionType) -> Result<Self, &'static str> {
|
||||
match permission {
|
||||
PermissionType::ReadApplicationState => {
|
||||
Ok(ProtobufPermissionType::ReadApplicationState)
|
||||
},
|
||||
PermissionType::ChangeApplicationState => {
|
||||
Ok(ProtobufPermissionType::ChangeApplicationState)
|
||||
},
|
||||
PermissionType::OpenFiles => Ok(ProtobufPermissionType::OpenFiles),
|
||||
PermissionType::RunCommands => Ok(ProtobufPermissionType::RunCommands),
|
||||
PermissionType::OpenTerminalsOrPlugins => {
|
||||
Ok(ProtobufPermissionType::OpenTerminalsOrPlugins)
|
||||
},
|
||||
PermissionType::WriteToStdin => Ok(ProtobufPermissionType::WriteToStdin),
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue