use anyhow::Result;
use std::sync::Arc;
use crate::hooks::registry::HookRegistry;
use crate::ipc::server::IpcServer;
use crate::pty::registry::PtyRegistry;
use crate::runtime::RuntimeAdapter;
use crate::state::SharedState;
use crate::utils::keys::tmux_key_to_bytes;
enum SendVariant {
Keys,
KeysLiteral,
TextAndEnter,
}
impl SendVariant {
fn to_pty_bytes(&self, payload: &str) -> Vec<u8> {
match self {
SendVariant::Keys => tmux_key_to_bytes(payload),
SendVariant::KeysLiteral => payload.as_bytes().to_vec(),
SendVariant::TextAndEnter => {
let mut data = payload.as_bytes().to_vec();
data.push(b'\r');
data
}
}
}
fn name(&self) -> &'static str {
match self {
SendVariant::Keys => "send_keys",
SendVariant::KeysLiteral => "send_keys_literal",
SendVariant::TextAndEnter => "send_text_and_enter",
}
}
}
pub struct CommandSender {
ipc_server: Option<Arc<IpcServer>>,
runtime: Arc<dyn RuntimeAdapter>,
app_state: SharedState,
hook_registry: Option<HookRegistry>,
pty_registry: Option<Arc<PtyRegistry>>,
}
impl CommandSender {
pub fn new(
ipc_server: Option<Arc<IpcServer>>,
runtime: Arc<dyn RuntimeAdapter>,
app_state: SharedState,
) -> Self {
Self {
ipc_server,
runtime,
app_state,
hook_registry: None,
pty_registry: None,
}
}
pub fn with_hook_registry(mut self, registry: HookRegistry) -> Self {
self.hook_registry = Some(registry);
self
}
pub fn with_pty_registry(mut self, registry: Arc<PtyRegistry>) -> Self {
self.pty_registry = Some(registry);
self
}
fn try_pty_session_write(&self, target: &str, data: &[u8]) -> bool {
if let Some(ref registry) = self.pty_registry {
if let Some(session) = registry.get(target) {
if session.is_running() {
return session.write_input(data).is_ok();
}
}
let session_id = {
let state = self.app_state.read();
state
.agents
.get(target)
.and_then(|a| a.pty_session_id.clone())
};
if let Some(sid) = session_id {
if let Some(session) = registry.get(&sid) {
if session.is_running() {
return session.write_input(data).is_ok();
}
}
}
}
false
}
pub fn send_keys(&self, target: &str, keys: &str) -> Result<()> {
self.try_send_via_tiers(target, keys, SendVariant::Keys)
}
pub fn send_keys_literal(&self, target: &str, keys: &str) -> Result<()> {
self.try_send_via_tiers(target, keys, SendVariant::KeysLiteral)
}
pub fn send_text_and_enter(&self, target: &str, text: &str) -> Result<()> {
self.try_send_via_tiers(target, text, SendVariant::TextAndEnter)
}
fn try_send_via_tiers(&self, target: &str, payload: &str, variant: SendVariant) -> Result<()> {
let pty_bytes = variant.to_pty_bytes(payload);
if self.try_pty_session_write(target, &pty_bytes) {
return Ok(());
}
if let Some(ref ipc) = self.ipc_server {
if let Some(pane_id) = self.get_pane_id_for_target(target) {
let ok = match variant {
SendVariant::Keys => ipc.try_send_keys(&pane_id, payload, false),
SendVariant::KeysLiteral => ipc.try_send_keys(&pane_id, payload, true),
SendVariant::TextAndEnter => ipc.try_send_keys_and_enter(&pane_id, payload),
};
if ok {
return Ok(());
}
}
}
let runtime_result = match variant {
SendVariant::Keys => self.runtime.send_keys(target, payload),
SendVariant::KeysLiteral => self.runtime.send_keys_literal(target, payload),
SendVariant::TextAndEnter => self.runtime.send_text_and_enter(target, payload),
};
if runtime_result.is_ok() {
return Ok(());
}
if let Some(pid) = self.resolve_pid_for_target(target) {
if pid > 0 {
return match variant {
SendVariant::Keys => crate::pty_inject::inject_text(pid, payload),
SendVariant::KeysLiteral => {
crate::pty_inject::inject_text_literal(pid, payload)
}
SendVariant::TextAndEnter => {
crate::pty_inject::inject_text_and_enter(pid, payload)
}
};
}
}
anyhow::bail!("All {} tiers failed for target {}", variant.name(), target)
}
pub fn runtime(&self) -> &Arc<dyn RuntimeAdapter> {
&self.runtime
}
pub fn ipc_server(&self) -> Option<&Arc<IpcServer>> {
self.ipc_server.as_ref()
}
fn get_pane_id_for_target(&self, target: &str) -> Option<String> {
let state = self.app_state.read();
state.target_to_pane_id.get(target).cloned()
}
fn resolve_pid_for_target(&self, target: &str) -> Option<u32> {
if let Some(ref registry) = self.hook_registry {
let pane_id = {
let state = self.app_state.read();
state.target_to_pane_id.get(target).cloned()
};
if let Some(pane_id) = pane_id {
let reg = registry.read();
if let Some(hook_state) = reg.get(&pane_id) {
if let Some(pid) = hook_state.pid {
return Some(pid);
}
}
}
}
let state = self.app_state.read();
if let Some(agent) = state.agents.get(target) {
if agent.pid > 0 {
return Some(agent.pid);
}
}
for agent in state.agents.values() {
if agent.target == target && agent.pid > 0 {
return Some(agent.pid);
}
}
None
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn send_variant_keys_converts_via_tmux_key_to_bytes() {
let bytes = SendVariant::Keys.to_pty_bytes("Enter");
assert_eq!(bytes, tmux_key_to_bytes("Enter"));
}
#[test]
fn send_variant_keys_literal_uses_raw_bytes() {
let bytes = SendVariant::KeysLiteral.to_pty_bytes("hello");
assert_eq!(bytes, b"hello");
}
#[test]
fn send_variant_text_and_enter_appends_cr() {
let bytes = SendVariant::TextAndEnter.to_pty_bytes("ls");
assert_eq!(bytes, b"ls\r");
}
#[test]
fn send_variant_names() {
assert_eq!(SendVariant::Keys.name(), "send_keys");
assert_eq!(SendVariant::KeysLiteral.name(), "send_keys_literal");
assert_eq!(SendVariant::TextAndEnter.name(), "send_text_and_enter");
}
#[test]
fn all_tiers_fail_returns_descriptive_error() {
use crate::runtime::StandaloneAdapter;
use crate::state::AppState;
use parking_lot::RwLock;
let state: SharedState = Arc::new(RwLock::new(AppState::default()));
let runtime: Arc<dyn RuntimeAdapter> = Arc::new(StandaloneAdapter::new());
let sender = CommandSender::new(None, runtime, state);
let err = sender.send_keys("no-such-target", "Enter").unwrap_err();
assert!(err.to_string().contains("send_keys"));
assert!(err.to_string().contains("no-such-target"));
let err = sender.send_keys_literal("no-such-target", "x").unwrap_err();
assert!(err.to_string().contains("send_keys_literal"));
let err = sender
.send_text_and_enter("no-such-target", "echo hi")
.unwrap_err();
assert!(err.to_string().contains("send_text_and_enter"));
}
}