From fbd20ad6e488fb3b9efc1612fdffee6e672976c1 Mon Sep 17 00:00:00 2001 From: Ogulcan Celik Date: Sun, 14 Jun 2026 02:43:45 +0300 Subject: feat: add plugin v1 system Add manifest-driven plugin actions, event hooks, pane entrypoints, link handlers, and host APIs. Support plugin link, GitHub install, build commands, installed plugin registry, pane movement, runtime context, and plugin docs. --- src/app/input/mod.rs | 12 ++++++- src/app/input/navigate.rs | 91 ++++++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 101 insertions(+), 2 deletions(-) (limited to 'src/app/input') diff --git a/src/app/input/mod.rs b/src/app/input/mod.rs index 1b5dfefe..339f7293 100644 --- a/src/app/input/mod.rs +++ b/src/app/input/mod.rs @@ -341,6 +341,13 @@ impl App { }; self.last_pane_click = None; + match self.invoke_plugin_link_handler_for_url(&url, info.id) { + Ok(true) => return true, + Ok(false) => {} + Err(err) => { + tracing::warn!(err = %err, url = %url, "failed to invoke plugin link handler"); + } + } if let Err(err) = crate::platform::open_url(&url) { tracing::warn!(err = %err, url = %url, "failed to open pane URL"); } @@ -514,6 +521,7 @@ impl AppState { self.pane_scrollback_limit_bytes, self.host_terminal_theme, crate::pane::PaneShellConfig::new(&self.default_shell, self.shell_mode), + Vec::new(), ) { let new_id = new_pane.pane_id; terminal_runtimes.insert(new_pane.terminal.id.clone(), new_pane.runtime); @@ -622,7 +630,9 @@ fn wait_for_file(path: &std::path::Path) -> String { let deadline = std::time::Instant::now() + std::time::Duration::from_secs(2); while std::time::Instant::now() < deadline { if let Ok(content) = std::fs::read_to_string(path) { - return content; + if !content.is_empty() { + return content; + } } std::thread::sleep(std::time::Duration::from_millis(20)); } diff --git a/src/app/input/navigate.rs b/src/app/input/navigate.rs index b27b5cb9..7072f659 100644 --- a/src/app/input/navigate.rs +++ b/src/app/input/navigate.rs @@ -140,6 +140,9 @@ impl App { crate::config::CustomCommandAction::Pane => { self.spawn_pane_command(&binding.command, Vec::new()) } + crate::config::CustomCommandAction::PluginAction => self + .invoke_plugin_action_from_keybind(binding.command.clone()) + .map_err(std::io::Error::other), }; match result { Ok(()) => finish_custom_command_context(&mut self.state, context, previous_mode), @@ -317,7 +320,7 @@ impl App { new_cols, cwd, command, - &env, + env, self.state.pane_scrollback_limit_bytes, self.state.host_terminal_theme, )?; @@ -355,6 +358,92 @@ impl App { self.state.mode = Mode::Terminal; Ok(()) } + + pub(crate) fn spawn_overlay_argv_command( + &mut self, + argv: &[String], + cwd: Option, + extra_env: Vec<(String, String)>, + temp_files: Vec, + ) -> std::io::Result<(usize, crate::workspace::NewPane)> { + let Some(ws_idx) = self.state.active else { + return Err(std::io::Error::other("no active workspace")); + }; + let previous_focus_target = self.state.current_pane_focus_target(); + let (rows, cols) = self.state.estimate_pane_size(); + let new_rows = rows.max(4); + let new_cols = cols.max(10); + + let ws = self + .state + .workspaces + .get(ws_idx) + .ok_or_else(|| std::io::Error::other("active workspace disappeared"))?; + let previous_focus = ws + .focused_pane_id() + .ok_or_else(|| std::io::Error::other("no focused pane"))?; + let cwd = cwd.or_else(|| { + ws.active_tab().and_then(|tab| { + tab.cwd_for_pane( + previous_focus, + &self.state.terminals, + &self.terminal_runtimes, + ) + }) + }); + + let (tab_idx, new_pane, workspace_id) = { + let ws = self + .state + .workspaces + .get_mut(ws_idx) + .ok_or_else(|| std::io::Error::other("active workspace disappeared"))?; + let previous_zoomed = ws.active_tab().map(|tab| tab.zoomed).unwrap_or(false); + let result = ws.split_pane_argv_command( + previous_focus, + Direction::Horizontal, + new_rows, + new_cols, + cwd, + argv, + extra_env, + self.state.pane_scrollback_limit_bytes, + self.state.host_terminal_theme, + true, + ); + let (tab_idx, new_pane) = match result { + Some(Ok(result)) => result, + Some(Err(err)) => return Err(err), + None => return Err(std::io::Error::other("focused pane disappeared")), + }; + ws.tabs + .get_mut(tab_idx) + .ok_or_else(|| std::io::Error::other("plugin overlay tab disappeared"))? + .zoomed = true; + self.overlay_panes.insert( + new_pane.pane_id, + super::super::OverlayPaneState { + ws_idx, + tab_idx, + previous_focus, + previous_zoomed, + temp_files, + }, + ); + (tab_idx, new_pane, ws.id.clone()) + }; + + let new_focus_target = crate::app::state::PaneFocusTarget { + workspace_id, + pane_id: new_pane.pane_id, + }; + if previous_focus_target.as_ref() != Some(&new_focus_target) { + self.state.previous_pane_focus = previous_focus_target; + } + self.state.switch_workspace_tab(ws_idx, tab_idx); + self.state.mode = Mode::Terminal; + Ok((ws_idx, new_pane)) + } } #[derive(Debug, Clone, Copy, PartialEq, Eq)] -- cgit v1.2.3