use jsonrpsee::types::ErrorObjectOwned;
use jsonrpsee::RpcModule;
use wrightty_protocol::error;
use wrightty_protocol::methods::*;
use wrightty_protocol::types::*;
use crate::kitty;
fn proto_err(code: i32, msg: impl Into<String>) -> ErrorObjectOwned {
ErrorObjectOwned::owned(code, msg.into(), None::<()>)
}
fn not_supported(method: &str) -> ErrorObjectOwned {
proto_err(error::NOT_SUPPORTED, format!("{method} is not supported by the kitty bridge"))
}
fn parse_window_id(session_id: &str) -> Result<u64, ErrorObjectOwned> {
session_id
.parse::<u64>()
.map_err(|_| proto_err(error::SESSION_NOT_FOUND, format!("invalid session id: {session_id}")))
}
fn encode_key_to_kitty(key: &KeyInput) -> String {
match key {
KeyInput::Shorthand(s) => shorthand_to_kitty(s),
KeyInput::Structured(event) => key_event_to_kitty(event),
}
}
fn shorthand_to_kitty(s: &str) -> String {
if let Some((modifier, key)) = s.split_once('+') {
let mod_lower = modifier.to_lowercase();
return format!("{}+{}", mod_lower, key.to_lowercase());
}
match s {
"Enter" | "Return" => "enter".to_string(),
"Tab" => "tab".to_string(),
"Backspace" => "backspace".to_string(),
"Delete" => "delete".to_string(),
"Escape" | "Esc" => "escape".to_string(),
"ArrowUp" | "Up" => "up".to_string(),
"ArrowDown" | "Down" => "down".to_string(),
"ArrowRight" | "Right" => "right".to_string(),
"ArrowLeft" | "Left" => "left".to_string(),
"Home" => "home".to_string(),
"End" => "end".to_string(),
"PageUp" => "page_up".to_string(),
"PageDown" => "page_down".to_string(),
"Insert" => "insert".to_string(),
_ => s.to_lowercase(),
}
}
fn key_event_to_kitty(event: &KeyEvent) -> String {
let has_ctrl = event.modifiers.iter().any(|m| matches!(m, Modifier::Ctrl));
let has_alt = event.modifiers.iter().any(|m| matches!(m, Modifier::Alt));
let has_shift = event.modifiers.iter().any(|m| matches!(m, Modifier::Shift));
let base = match &event.key {
KeyType::Char => event.char.as_deref().unwrap_or("").to_lowercase(),
KeyType::Enter => "enter".to_string(),
KeyType::Tab => "tab".to_string(),
KeyType::Backspace => "backspace".to_string(),
KeyType::Delete => "delete".to_string(),
KeyType::Escape => "escape".to_string(),
KeyType::ArrowUp => "up".to_string(),
KeyType::ArrowDown => "down".to_string(),
KeyType::ArrowRight => "right".to_string(),
KeyType::ArrowLeft => "left".to_string(),
KeyType::Home => "home".to_string(),
KeyType::End => "end".to_string(),
KeyType::PageUp => "page_up".to_string(),
KeyType::PageDown => "page_down".to_string(),
KeyType::Insert => "insert".to_string(),
KeyType::F => format!("f{}", event.n.unwrap_or(1)),
};
let mut modifiers = Vec::new();
if has_ctrl { modifiers.push("ctrl"); }
if has_alt { modifiers.push("alt"); }
if has_shift { modifiers.push("shift"); }
if modifiers.is_empty() {
base
} else {
format!("{}+{base}", modifiers.join("+"))
}
}
pub fn build_rpc_module() -> anyhow::Result<RpcModule<()>> {
let mut module = RpcModule::new(());
module.register_async_method("Wrightty.getInfo", |_params, _state, _| async move {
serde_json::to_value(GetInfoResult {
info: ServerInfo {
version: "0.1.0".to_string(),
implementation: "wrightty-bridge-kitty".to_string(),
capabilities: Capabilities {
screenshot: vec![ScreenshotFormat::Text],
max_sessions: 256,
supports_resize: true,
supports_scrollback: true,
supports_mouse: false,
supports_session_create: true,
supports_color_palette: false,
supports_raw_output: false,
supports_shell_integration: false,
events: vec![],
},
},
})
.map_err(|e| proto_err(-32603, e.to_string()))
})?;
module.register_async_method("Session.create", |_params, _state, _| async move {
let window_id = kitty::launch_window()
.await
.map_err(|e| proto_err(error::SPAWN_FAILED, e.to_string()))?;
serde_json::to_value(SessionCreateResult {
session_id: window_id.to_string(),
})
.map_err(|e| proto_err(-32603, e.to_string()))
})?;
module.register_async_method("Session.destroy", |params, _state, _| async move {
let p: SessionDestroyParams = params.parse()?;
let window_id = parse_window_id(&p.session_id)?;
kitty::close_window(window_id)
.await
.map_err(|e| proto_err(error::SESSION_NOT_FOUND, e.to_string()))?;
serde_json::to_value(SessionDestroyResult { exit_code: None })
.map_err(|e| proto_err(-32603, e.to_string()))
})?;
module.register_async_method("Session.list", |_params, _state, _| async move {
let windows = kitty::list_windows()
.await
.map_err(|e| proto_err(-32603, e.to_string()))?;
let sessions: Vec<SessionInfo> = windows
.into_iter()
.map(|w| SessionInfo {
session_id: w.id.to_string(),
title: w.title,
cwd: w.cwd,
cols: w.columns,
rows: w.lines,
pid: w.pid,
running: true,
alternate_screen: false,
})
.collect();
serde_json::to_value(SessionListResult { sessions })
.map_err(|e| proto_err(-32603, e.to_string()))
})?;
module.register_async_method("Session.getInfo", |params, _state, _| async move {
let p: SessionGetInfoParams = params.parse()?;
let window_id = parse_window_id(&p.session_id)?;
let w = kitty::find_window(window_id)
.await
.map_err(|e| proto_err(error::SESSION_NOT_FOUND, e.to_string()))?;
let info = SessionInfo {
session_id: w.id.to_string(),
title: w.title,
cwd: w.cwd,
cols: w.columns,
rows: w.lines,
pid: w.pid,
running: true,
alternate_screen: false,
};
serde_json::to_value(info).map_err(|e| proto_err(-32603, e.to_string()))
})?;
module.register_async_method("Input.sendText", |params, _state, _| async move {
let p: InputSendTextParams = params.parse()?;
let window_id = parse_window_id(&p.session_id)?;
kitty::send_text(window_id, &p.text)
.await
.map_err(|e| proto_err(error::SESSION_NOT_FOUND, e.to_string()))?;
Ok::<_, ErrorObjectOwned>(serde_json::json!({}))
})?;
module.register_async_method("Input.sendKeys", |params, _state, _| async move {
let p: InputSendKeysParams = params.parse()?;
let window_id = parse_window_id(&p.session_id)?;
for key in &p.keys {
let is_literal_char = matches!(key, KeyInput::Shorthand(s) if s.len() == 1 && !s.contains('+'));
if is_literal_char {
let text = match key {
KeyInput::Shorthand(s) => s.clone(),
_ => unreachable!(),
};
kitty::send_text(window_id, &text)
.await
.map_err(|e| proto_err(error::SESSION_NOT_FOUND, e.to_string()))?;
} else {
let kitty_key = encode_key_to_kitty(key);
kitty::send_key(window_id, &kitty_key)
.await
.map_err(|e| proto_err(error::SESSION_NOT_FOUND, e.to_string()))?;
}
}
Ok::<_, ErrorObjectOwned>(serde_json::json!({}))
})?;
module.register_async_method("Screen.getText", |params, _state, _| async move {
let p: ScreenGetTextParams = params.parse()?;
let window_id = parse_window_id(&p.session_id)?;
let mut text = kitty::get_text(window_id)
.await
.map_err(|e| proto_err(error::SESSION_NOT_FOUND, e.to_string()))?;
if p.trim_trailing_whitespace {
text = text
.lines()
.map(|line| line.trim_end())
.collect::<Vec<_>>()
.join("\n");
}
serde_json::to_value(ScreenGetTextResult { text })
.map_err(|e| proto_err(-32603, e.to_string()))
})?;
module.register_async_method("Terminal.getSize", |params, _state, _| async move {
let p: TerminalGetSizeParams = params.parse()?;
let window_id = parse_window_id(&p.session_id)?;
let w = kitty::find_window(window_id)
.await
.map_err(|e| proto_err(error::SESSION_NOT_FOUND, e.to_string()))?;
serde_json::to_value(TerminalGetSizeResult {
cols: w.columns,
rows: w.lines,
})
.map_err(|e| proto_err(-32603, e.to_string()))
})?;
module.register_async_method("Terminal.resize", |params, _state, _| async move {
let p: TerminalResizeParams = params.parse()?;
let window_id = parse_window_id(&p.session_id)?;
kitty::resize_window(window_id, p.cols, p.rows)
.await
.map_err(|e| proto_err(error::SESSION_NOT_FOUND, e.to_string()))?;
Ok::<_, ErrorObjectOwned>(serde_json::json!({}))
})?;
module.register_async_method("Screen.getContents", |_params, _state, _| async move {
Err::<serde_json::Value, _>(not_supported("Screen.getContents"))
})?;
module.register_async_method("Screen.screenshot", |_params, _state, _| async move {
Err::<serde_json::Value, _>(not_supported("Screen.screenshot"))
})?;
module.register_async_method("Input.sendMouse", |_params, _state, _| async move {
Err::<serde_json::Value, _>(not_supported("Input.sendMouse"))
})?;
Ok(module)
}