aboutsummaryrefslogtreecommitdiff
path: root/src/server
diff options
context:
space:
mode:
authorOgulcan Celik <ogulcancelik@gmail.com>2026-06-14 02:43:45 +0300
committerOgulcan Celik <ogulcancelik@gmail.com>2026-06-14 03:47:07 +0300
commitfbd20ad6e488fb3b9efc1612fdffee6e672976c1 (patch)
treed21daf93241786f8aa6fde23b41cdb9e15182b79 /src/server
parentd35c642ef36ee5ac2afe7c1b18ad5a717bc807d8 (diff)
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.
Diffstat (limited to 'src/server')
-rw-r--r--src/server/headless.rs67
-rw-r--r--src/server/notifications.rs2
2 files changed, 65 insertions, 4 deletions
diff --git a/src/server/headless.rs b/src/server/headless.rs
index ed81b015..750e9250 100644
--- a/src/server/headless.rs
+++ b/src/server/headless.rs
@@ -100,7 +100,7 @@ fn non_empty_body(value: &str) -> Option<String> {
enum LoopEvent {
Timer,
Internal(AppEvent),
- Api(api::ApiRequestMessage),
+ Api(Box<api::ApiRequestMessage>),
ServerEvent(ServerEvent),
RenderRequested,
}
@@ -637,7 +637,7 @@ impl HeadlessServer {
let event = {
tokio::select! {
maybe_api = self.app.api_rx.recv() => match maybe_api {
- Some(msg) => LoopEvent::Api(msg),
+ Some(msg) => LoopEvent::Api(Box::new(msg)),
None => LoopEvent::Timer,
},
maybe_ev = self.app.event_rx.recv() => match maybe_ev {
@@ -662,7 +662,7 @@ impl HeadlessServer {
}
}
LoopEvent::Api(msg) => {
- if self.handle_api_request_with_shutdown_check(msg) {
+ if self.handle_api_request_with_shutdown_check(*msg) {
needs_render = true;
needs_full_render = true;
}
@@ -1603,6 +1603,39 @@ impl HeadlessServer {
.unwrap_or_else(|_| "{}".to_string())
}
+ fn handle_client_window_title_api(&mut self, id: String, title: Option<String>) -> String {
+ use api::schema::{ClientWindowTitleReason, ResponseResult};
+
+ let title = match title {
+ Some(title) => match sanitize_window_title_text(&title, 200) {
+ Some(title) => Some(title),
+ None => {
+ return serde_json::to_string(&api::schema::ErrorResponse {
+ id,
+ error: api::schema::ErrorBody {
+ code: "invalid_params".into(),
+ message: "window title is empty".into(),
+ },
+ })
+ .unwrap_or_else(|_| "{}".to_string());
+ }
+ },
+ None => None,
+ };
+ let set_title = title.is_some();
+ let changed = self.send_to_foreground_client(ServerMessage::WindowTitle { title });
+ let reason = match (changed, set_title) {
+ (true, true) => ClientWindowTitleReason::Set,
+ (true, false) => ClientWindowTitleReason::Cleared,
+ (false, _) => ClientWindowTitleReason::NoForegroundClient,
+ };
+ serde_json::to_string(&api::schema::SuccessResponse {
+ id,
+ result: ResponseResult::ClientWindowTitle { changed, reason },
+ })
+ .unwrap_or_else(|_| "{}".to_string())
+ }
+
fn forward_api_notification_sound(&mut self, sound: api::schema::NotificationShowSound) {
let Some(sound) = sound.to_sound() else {
return;
@@ -2511,6 +2544,23 @@ impl HeadlessServer {
return true;
}
+ match &msg.request.method {
+ api::schema::Method::ClientWindowTitleSet(params) => {
+ let response = self.handle_client_window_title_api(
+ msg.request.id.clone(),
+ Some(params.title.clone()),
+ );
+ let _ = msg.respond_to.send(response);
+ return true;
+ }
+ api::schema::Method::ClientWindowTitleClear(_) => {
+ let response = self.handle_client_window_title_api(msg.request.id.clone(), None);
+ let _ = msg.respond_to.send(response);
+ return true;
+ }
+ _ => {}
+ }
+
let mut changed = api::request_changes_ui(&msg.request);
let skip_default_workspace = matches!(
&msg.request.method,
@@ -3544,6 +3594,17 @@ fn sanitize_notification_text(value: &str, max_chars: usize) -> Option<String> {
(!sanitized.is_empty()).then_some(sanitized)
}
+fn sanitize_window_title_text(value: &str, max_chars: usize) -> Option<String> {
+ let sanitized = value
+ .chars()
+ .filter(|ch| !matches!(*ch, '\u{1b}' | '\u{7}' | '\u{9c}') && !ch.is_control())
+ .take(max_chars)
+ .collect::<String>()
+ .trim()
+ .to_string();
+ (!sanitized.is_empty()).then_some(sanitized)
+}
+
fn server_config_diagnostic_summaries(diagnostics: &[String]) -> (Option<String>, Option<String>) {
let without_keybindings = diagnostics
.iter()
diff --git a/src/server/notifications.rs b/src/server/notifications.rs
index 63bfe944..79ff63ca 100644
--- a/src/server/notifications.rs
+++ b/src/server/notifications.rs
@@ -121,10 +121,10 @@ mod tests {
0,
crate::terminal_theme::TerminalTheme::default(),
crate::pane::PaneShellConfig::new("/bin/sh", crate::config::ShellModeConfig::NonLogin),
+ &crate::pane::PaneLaunchEnv::default(),
events,
std::sync::Arc::new(tokio::sync::Notify::new()),
std::sync::Arc::new(std::sync::atomic::AtomicBool::new(false)),
- None,
)
.unwrap();
let deadline = std::time::Instant::now() + std::time::Duration::from_secs(2);