aboutsummaryrefslogtreecommitdiff
path: root/src/api
diff options
context:
space:
mode:
Diffstat (limited to 'src/api')
-rw-r--r--src/api/mod.rs101
-rw-r--r--src/api/schema.rs56
2 files changed, 108 insertions, 49 deletions
diff --git a/src/api/mod.rs b/src/api/mod.rs
index 1b81e09c..437a616e 100644
--- a/src/api/mod.rs
+++ b/src/api/mod.rs
@@ -2,7 +2,6 @@ pub mod schema;
use std::fs;
use std::io::{BufRead, BufReader, Read, Write};
-use std::os::unix::fs::PermissionsExt;
use std::os::unix::net::{UnixListener, UnixStream};
use std::path::{Path, PathBuf};
use std::sync::atomic::{AtomicBool, Ordering};
@@ -28,6 +27,25 @@ const APP_RESPONSE_TIMEOUT: Duration = Duration::from_secs(5);
const INITIAL_REQUEST_TIMEOUT: Duration = Duration::from_secs(5);
const STREAM_WRITE_TIMEOUT: Duration = Duration::from_secs(5);
+pub(crate) fn request_changes_ui(request: &Request) -> bool {
+ matches!(
+ &request.method,
+ Method::WorkspaceCreate(_)
+ | Method::WorkspaceFocus(_)
+ | Method::WorkspaceRename(_)
+ | Method::WorkspaceClose(_)
+ | Method::TabCreate(_)
+ | Method::TabFocus(_)
+ | Method::TabRename(_)
+ | Method::TabClose(_)
+ | Method::PaneSplit(_)
+ | Method::PaneReportAgent(_)
+ | Method::PaneClearAgentAuthority(_)
+ | Method::PaneReleaseAgent(_)
+ | Method::PaneClose(_)
+ )
+}
+
pub struct ApiRequestMessage {
pub request: Request,
pub respond_to: std::sync::mpsc::Sender<String>,
@@ -80,16 +98,6 @@ pub fn socket_path() -> PathBuf {
return PathBuf::from(path);
}
- let socket_name = if crate::config::app_dir_name() == "herdr" {
- "herdr.sock"
- } else {
- "herdr-dev.sock"
- };
-
- if let Ok(dir) = std::env::var("XDG_RUNTIME_DIR") {
- return PathBuf::from(dir).join(socket_name);
- }
-
crate::config::config_dir().join("herdr.sock")
}
@@ -156,47 +164,16 @@ pub fn start_server(
}
fn prepare_socket_path(path: &Path) -> std::io::Result<()> {
- if let Some(parent) = path.parent() {
- fs::create_dir_all(parent)?;
- }
-
- if !path.exists() {
- return Ok(());
- }
-
- match UnixStream::connect(path) {
- Ok(_) => {
- return Err(std::io::Error::new(
- std::io::ErrorKind::AddrInUse,
- format!(
- "herdr is already running (socket busy at {})",
- path.display()
- ),
- ));
- }
- Err(err)
- if matches!(
- err.kind(),
- std::io::ErrorKind::ConnectionRefused
- | std::io::ErrorKind::NotFound
- | std::io::ErrorKind::TimedOut
- ) => {}
- Err(err) => return Err(err),
- }
-
- if let Err(err) = fs::remove_file(path) {
- if err.kind() != std::io::ErrorKind::NotFound {
- return Err(err);
- }
- }
-
- Ok(())
+ crate::ipc::prepare_socket_path(path, |path| {
+ format!(
+ "herdr is already running (socket busy at {})",
+ path.display()
+ )
+ })
}
fn restrict_socket_permissions(path: &Path) -> std::io::Result<()> {
- let mut permissions = fs::metadata(path)?.permissions();
- permissions.set_mode(SOCKET_PERMISSION_MODE);
- fs::set_permissions(path, permissions)
+ crate::ipc::restrict_socket_permissions(path, SOCKET_PERMISSION_MODE)
}
fn handle_connection(
@@ -947,6 +924,13 @@ fn error_response_json(id: String, code: &str, message: String) -> String {
#[cfg(test)]
mod tests {
use super::*;
+ use std::os::unix::fs::PermissionsExt;
+ use std::sync::{Mutex, OnceLock};
+
+ fn env_lock() -> &'static Mutex<()> {
+ static LOCK: OnceLock<Mutex<()>> = OnceLock::new();
+ LOCK.get_or_init(|| Mutex::new(()))
+ }
fn unique_test_path(name: &str) -> PathBuf {
let nanos = std::time::SystemTime::now()
@@ -965,6 +949,7 @@ mod tests {
#[test]
fn socket_path_prefers_explicit_env_override() {
+ let _guard = env_lock().lock().unwrap();
let unique = format!("/tmp/herdr-test-{}.sock", std::process::id());
std::env::set_var(SOCKET_PATH_ENV_VAR, &unique);
assert_eq!(socket_path(), PathBuf::from(&unique));
@@ -972,6 +957,24 @@ mod tests {
}
#[test]
+ fn socket_path_defaults_to_config_dir_even_when_xdg_runtime_dir_is_set() {
+ let _guard = env_lock().lock().unwrap();
+ let config_home = unique_test_path("socket-default-config-home");
+ let runtime_dir = unique_test_path("socket-default-runtime");
+ std::env::remove_var(SOCKET_PATH_ENV_VAR);
+ std::env::set_var("XDG_CONFIG_HOME", &config_home);
+ std::env::set_var("XDG_RUNTIME_DIR", &runtime_dir);
+
+ let expected = config_home
+ .join(crate::config::app_dir_name())
+ .join("herdr.sock");
+ assert_eq!(socket_path(), expected);
+
+ std::env::remove_var("XDG_CONFIG_HOME");
+ std::env::remove_var("XDG_RUNTIME_DIR");
+ }
+
+ #[test]
fn restrict_socket_permissions_sets_user_only_mode() {
let dir = unique_test_path("socket-perms");
fs::create_dir_all(&dir).unwrap();
diff --git a/src/api/schema.rs b/src/api/schema.rs
index 38393141..6cb81c12 100644
--- a/src/api/schema.rs
+++ b/src/api/schema.rs
@@ -12,6 +12,8 @@ pub struct Request {
pub enum Method {
#[serde(rename = "ping")]
Ping(PingParams),
+ #[serde(rename = "server.stop")]
+ ServerStop(EmptyParams),
#[serde(rename = "workspace.create")]
WorkspaceCreate(WorkspaceCreateParams),
#[serde(rename = "workspace.list")]
@@ -64,6 +66,10 @@ pub enum Method {
EventsWait(EventsWaitParams),
#[serde(rename = "pane.wait_for_output")]
PaneWaitForOutput(PaneWaitForOutputParams),
+ #[serde(rename = "integration.install")]
+ IntegrationInstall(IntegrationInstallParams),
+ #[serde(rename = "integration.uninstall")]
+ IntegrationUninstall(IntegrationUninstallParams),
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
@@ -286,6 +292,25 @@ pub struct PaneWaitForOutputParams {
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
+pub struct IntegrationInstallParams {
+ pub target: IntegrationTarget,
+}
+
+#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
+pub struct IntegrationUninstallParams {
+ pub target: IntegrationTarget,
+}
+
+#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
+#[serde(rename_all = "snake_case")]
+pub enum IntegrationTarget {
+ Pi,
+ Claude,
+ Codex,
+ Opencode,
+}
+
+#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(tag = "type", rename_all = "snake_case")]
pub enum OutputMatch {
Substring { value: String },
@@ -442,6 +467,14 @@ pub enum ResponseResult {
matched_line: Option<String>,
read: PaneReadResult,
},
+ IntegrationInstall {
+ target: IntegrationTarget,
+ details: IntegrationInstallResult,
+ },
+ IntegrationUninstall {
+ target: IntegrationTarget,
+ details: IntegrationUninstallResult,
+ },
Ok {},
}
@@ -494,6 +527,16 @@ pub struct PaneReadResult {
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
+pub struct IntegrationInstallResult {
+ pub messages: Vec<String>,
+}
+
+#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
+pub struct IntegrationUninstallResult {
+ pub messages: Vec<String>,
+}
+
+#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct EventEnvelope {
pub event: EventKind,
pub data: EventData,
@@ -710,6 +753,19 @@ mod tests {
}
#[test]
+ fn request_round_trips_for_server_stop() {
+ let request = Request {
+ id: "req_stop".into(),
+ method: Method::ServerStop(EmptyParams::default()),
+ };
+
+ let json = serde_json::to_value(&request).unwrap();
+ assert_eq!(json["method"], "server.stop");
+ let restored: Request = serde_json::from_value(json).unwrap();
+ assert_eq!(restored, request);
+ }
+
+ #[test]
fn unknown_method_is_rejected() {
let json = r#"{"id":"req_1","method":"nope","params":{}}"#;
let err = serde_json::from_str::<Request>(json)