aboutsummaryrefslogtreecommitdiff
path: root/src/app/api/plugins/panes.rs
diff options
context:
space:
mode:
Diffstat (limited to 'src/app/api/plugins/panes.rs')
-rw-r--r--src/app/api/plugins/panes.rs284
1 files changed, 284 insertions, 0 deletions
diff --git a/src/app/api/plugins/panes.rs b/src/app/api/plugins/panes.rs
new file mode 100644
index 00000000..22173cbf
--- /dev/null
+++ b/src/app/api/plugins/panes.rs
@@ -0,0 +1,284 @@
+use ratatui::layout::Direction;
+
+use super::super::responses::{encode_error, encode_success};
+use crate::api::schema::{
+ InstalledPluginInfo, PluginInvocationContext, PluginManifestPane, PluginPaneInfo,
+ PluginPaneOpenParams, PluginPanePlacement, ResponseResult,
+};
+use crate::app::App;
+
+impl App {
+ pub(super) fn open_plugin_overlay_pane(
+ &mut self,
+ id: String,
+ params: PluginPaneOpenParams,
+ plugin: &InstalledPluginInfo,
+ pane: PluginManifestPane,
+ ) -> String {
+ let context = self.current_plugin_context("plugin-pane");
+ let extra_env =
+ match self.plugin_pane_launch_env(plugin, &pane.id, params.env.clone(), &context) {
+ Ok(env) => env,
+ Err((code, message)) => return encode_error(id, &code, message),
+ };
+ let cwd = Some(self.plugin_pane_cwd(plugin, params.cwd));
+ let (ws_idx, new_pane) =
+ match self.spawn_overlay_argv_command(&pane.command, cwd, extra_env, Vec::new()) {
+ Ok(result) => result,
+ Err(err) => return encode_error(id, "plugin_pane_open_failed", err.to_string()),
+ };
+ self.finish_plugin_pane_open(id, ws_idx, None, new_pane, plugin.plugin_id.clone(), pane)
+ }
+
+ pub(super) fn open_plugin_split_pane(
+ &mut self,
+ id: String,
+ params: PluginPaneOpenParams,
+ plugin: &InstalledPluginInfo,
+ pane: PluginManifestPane,
+ placement: PluginPanePlacement,
+ ) -> String {
+ let target_pane_id = params
+ .target_pane_id
+ .clone()
+ .or_else(|| self.current_public_pane_id());
+ let Some(target_pane_id) = target_pane_id else {
+ return encode_error(id, "no_active_pane", "no active pane");
+ };
+ let Some((ws_idx, target_pane)) = self.parse_pane_id(&target_pane_id) else {
+ return encode_error(
+ id,
+ "pane_not_found",
+ format!("pane {target_pane_id} not found"),
+ );
+ };
+ let context = self.plugin_context_for_pane(ws_idx, target_pane, "plugin-pane");
+ let extra_env =
+ match self.plugin_pane_launch_env(plugin, &pane.id, params.env.clone(), &context) {
+ Ok(env) => env,
+ Err((code, message)) => return encode_error(id, &code, message),
+ };
+ let direction = match params
+ .direction
+ .unwrap_or(crate::api::schema::SplitDirection::Right)
+ {
+ crate::api::schema::SplitDirection::Right => Direction::Horizontal,
+ crate::api::schema::SplitDirection::Down => Direction::Vertical,
+ };
+ let cwd = Some(self.plugin_pane_cwd(plugin, params.cwd));
+ let (rows, cols) = self.state.estimate_pane_size();
+ let previous_focus = self.state.current_pane_focus_target();
+ let Some(ws) = self.state.workspaces.get_mut(ws_idx) else {
+ return encode_error(id, "workspace_not_found", "workspace not found");
+ };
+ let result = ws.split_pane_argv_command(
+ target_pane,
+ direction,
+ rows.max(4),
+ cols.max(10),
+ cwd,
+ &pane.command,
+ extra_env,
+ self.state.pane_scrollback_limit_bytes,
+ self.state.host_terminal_theme,
+ params.focus || placement == PluginPanePlacement::Zoomed,
+ );
+ let (tab_idx, new_pane) = match result {
+ Some(Ok(result)) => result,
+ Some(Err(err)) => return encode_error(id, "plugin_pane_open_failed", err.to_string()),
+ None => {
+ return encode_error(
+ id,
+ "pane_not_found",
+ format!("pane {target_pane_id} not found"),
+ )
+ }
+ };
+ if params.focus || placement == PluginPanePlacement::Zoomed {
+ self.state.switch_workspace_tab(ws_idx, tab_idx);
+ self.state
+ .record_pane_focus_change(previous_focus, ws_idx, new_pane.pane_id);
+ self.state.mode = crate::app::Mode::Terminal;
+ }
+ if placement == PluginPanePlacement::Zoomed {
+ if let Some(tab) = self
+ .state
+ .workspaces
+ .get_mut(ws_idx)
+ .and_then(|ws| ws.tabs.get_mut(tab_idx))
+ {
+ tab.zoomed = true;
+ }
+ }
+ self.finish_plugin_pane_open(id, ws_idx, None, new_pane, plugin.plugin_id.clone(), pane)
+ }
+
+ pub(super) fn open_plugin_tab(
+ &mut self,
+ id: String,
+ params: PluginPaneOpenParams,
+ plugin: &InstalledPluginInfo,
+ pane: PluginManifestPane,
+ ) -> String {
+ let ws_idx = match params.workspace_id.as_deref() {
+ Some(workspace_id) => match self.parse_workspace_id(workspace_id) {
+ Some(ws_idx) => ws_idx,
+ None => return encode_error(id, "workspace_not_found", "workspace not found"),
+ },
+ None => match self.state.active {
+ Some(ws_idx) => ws_idx,
+ None => return encode_error(id, "no_active_workspace", "no active workspace"),
+ },
+ };
+ let cwd = self.plugin_pane_cwd(plugin, params.cwd);
+ let context = self.plugin_context_for_workspace(ws_idx, "plugin-pane");
+ let extra_env =
+ match self.plugin_pane_launch_env(plugin, &pane.id, params.env.clone(), &context) {
+ Ok(env) => env,
+ Err((code, message)) => return encode_error(id, &code, message),
+ };
+ let (rows, cols) = self.state.estimate_pane_size();
+ let Some(ws) = self.state.workspaces.get_mut(ws_idx) else {
+ return encode_error(id, "workspace_not_found", "workspace not found");
+ };
+ let (tab_idx, terminal, runtime) = match ws.create_tab_argv_command(
+ rows.max(4),
+ cols.max(10),
+ cwd,
+ &pane.command,
+ extra_env,
+ self.state.pane_scrollback_limit_bytes,
+ self.state.host_terminal_theme,
+ ) {
+ Ok(result) => result,
+ Err(err) => return encode_error(id, "plugin_pane_open_failed", err.to_string()),
+ };
+ let pane_id = ws.tabs[tab_idx].root_pane;
+ if params.focus {
+ self.state.switch_workspace_tab(ws_idx, tab_idx);
+ self.state.mode = crate::app::Mode::Terminal;
+ }
+ let new_pane = crate::workspace::NewPane {
+ pane_id,
+ terminal,
+ runtime,
+ };
+ self.finish_plugin_pane_open(
+ id,
+ ws_idx,
+ Some(tab_idx),
+ new_pane,
+ plugin.plugin_id.clone(),
+ pane,
+ )
+ }
+
+ fn plugin_pane_launch_env(
+ &self,
+ plugin: &InstalledPluginInfo,
+ entrypoint: &str,
+ env: std::collections::HashMap<String, String>,
+ context: &PluginInvocationContext,
+ ) -> Result<Vec<(String, String)>, (String, String)> {
+ let mut env = super::super::env::normalize_launch_env(env)?;
+ let context_json = serde_json::to_string(&context)
+ .map_err(|err| ("invalid_plugin_context".to_string(), err.to_string()))?;
+ env.retain(|(key, _)| !plugin_pane_protected_env_key(key));
+ env.extend(super::env::plugin_path_env(plugin));
+ env.push(("HERDR_PLUGIN_ID".to_string(), plugin.plugin_id.clone()));
+ env.push((
+ "HERDR_PLUGIN_ENTRYPOINT_ID".to_string(),
+ entrypoint.to_string(),
+ ));
+ env.push(("HERDR_PLUGIN_CONTEXT_JSON".to_string(), context_json));
+ if let Ok(current_exe) = std::env::current_exe() {
+ env.push((
+ "HERDR_BIN_PATH".to_string(),
+ current_exe.display().to_string(),
+ ));
+ }
+ Ok(env)
+ }
+
+ fn finish_plugin_pane_open(
+ &mut self,
+ id: String,
+ ws_idx: usize,
+ created_tab_idx: Option<usize>,
+ new_pane: crate::workspace::NewPane,
+ plugin_id: String,
+ pane_manifest: PluginManifestPane,
+ ) -> String {
+ let entrypoint = pane_manifest.id.clone();
+ let mut terminal = new_pane.terminal;
+ terminal.set_manual_label(pane_manifest.title.clone());
+ let terminal_id = terminal.id.clone();
+ self.terminal_runtimes
+ .insert(terminal_id.clone(), new_pane.runtime);
+ self.state
+ .remove_alias_shadowed_by_new_pane(new_pane.pane_id);
+ self.state.terminals.insert(terminal_id, terminal);
+ self.state.plugin_panes.insert(
+ new_pane.pane_id,
+ crate::app::state::PluginPaneRecord {
+ plugin_id: plugin_id.clone(),
+ entrypoint: entrypoint.clone(),
+ },
+ );
+ if let Some(tab_idx) = created_tab_idx {
+ if let Some(tab) = self.tab_info(ws_idx, tab_idx) {
+ self.emit_event(crate::api::schema::EventEnvelope {
+ event: crate::api::schema::EventKind::TabCreated,
+ data: crate::api::schema::EventData::TabCreated { tab },
+ });
+ }
+ }
+ self.schedule_session_save();
+ let Some(pane) = self.pane_info(ws_idx, new_pane.pane_id) else {
+ return encode_error(id, "plugin_pane_open_failed", "plugin pane disappeared");
+ };
+ self.emit_event(crate::api::schema::EventEnvelope {
+ event: crate::api::schema::EventKind::PaneCreated,
+ data: crate::api::schema::EventData::PaneCreated { pane: pane.clone() },
+ });
+ encode_success(
+ id,
+ ResponseResult::PluginPaneOpened {
+ plugin_pane: PluginPaneInfo {
+ plugin_id,
+ entrypoint,
+ pane,
+ },
+ },
+ )
+ }
+
+ fn plugin_pane_cwd(
+ &self,
+ plugin: &InstalledPluginInfo,
+ override_cwd: Option<String>,
+ ) -> std::path::PathBuf {
+ override_cwd
+ .map(std::path::PathBuf::from)
+ .unwrap_or_else(|| std::path::PathBuf::from(&plugin.plugin_root))
+ }
+
+ fn current_public_pane_id(&self) -> Option<String> {
+ let ws_idx = self.state.active?;
+ let pane_id = self.state.workspaces.get(ws_idx)?.focused_pane_id()?;
+ self.public_pane_id(ws_idx, pane_id)
+ }
+}
+
+fn plugin_pane_protected_env_key(key: &str) -> bool {
+ matches!(
+ key,
+ "HERDR_PLUGIN_ID"
+ | "HERDR_PLUGIN_ROOT"
+ | "HERDR_PLUGIN_CONFIG_DIR"
+ | "HERDR_PLUGIN_STATE_DIR"
+ | "HERDR_PLUGIN_ENTRYPOINT_ID"
+ | "HERDR_PLUGIN_CONTEXT_JSON"
+ | "HERDR_BIN_PATH"
+ )
+}