reovim_client_cli/lib.rs
1#![cfg_attr(coverage_nightly, allow(unused_features))]
2#![cfg_attr(coverage_nightly, feature(coverage_attribute))]
3//! Reovim CLI Client - gRPC v2 command-line interface.
4//!
5//! This crate provides a CLI client for interacting with reovim servers
6//! using the gRPC v2 protocol.
7//!
8//! # Example
9//!
10//! ```ignore
11//! use reovim_client_cli::{CliArgs, CliAction};
12//!
13//! #[tokio::main]
14//! async fn main() {
15//! let args = CliArgs::parse();
16//! let result = args.execute().await;
17//! match result {
18//! Ok(output) => println!("{}", output),
19//! Err(e) => eprintln!("Error: {}", e),
20//! }
21//! }
22//! ```
23//!
24//! # Commands
25//!
26//! - `keys <KEYS>` - Send keys in vim notation
27//! - `mode` - Get current editor mode
28//! - `cursor` - Get cursor position
29//! - `buffers` - List open buffers
30//! - `buffer [ID]` - Get buffer content
31//! - `ping` - Health check
32//! - `version` - Get server version
33//!
34//! # Protocol
35//!
36//! This CLI uses **gRPC v2** transport, not JSON-RPC v1.
37//! Connect to a server started with `--grpc <PORT>`.
38
39mod client;
40pub mod commands;
41
42use clap::{Parser, Subcommand};
43pub use client::{GrpcClient, GrpcClientError};
44
45/// CLI arguments for the gRPC v2 CLI client.
46#[derive(Debug, Parser)]
47#[command(name = "reovim-cli")]
48#[command(about = "Reovim CLI client (gRPC v2)", long_about = None)]
49pub struct CliArgs {
50 /// gRPC server address (host:port).
51 #[arg(long, default_value = "127.0.0.1:12540")]
52 pub grpc: String,
53
54 /// Output format.
55 #[arg(long, short, value_enum, default_value = "plain")]
56 pub format: OutputFormat,
57
58 /// Command to execute.
59 #[command(subcommand)]
60 pub command: CliCommand,
61}
62
63/// Output format for CLI results.
64#[derive(Debug, Clone, Copy, PartialEq, Eq, clap::ValueEnum)]
65pub enum OutputFormat {
66 /// Plain text output.
67 Plain,
68 /// JSON output.
69 Json,
70}
71
72/// CLI commands.
73#[derive(Debug, Subcommand)]
74pub enum CliCommand {
75 /// Send keys to a specific client.
76 Keys {
77 /// Keys in vim notation (e.g., `iHello<Esc>`).
78 keys: String,
79
80 /// Target client ID to send keys to (required).
81 #[arg(long, short)]
82 client: u64,
83 },
84
85 /// Get a specific client's editor mode.
86 Mode {
87 /// Target client ID to query mode from (required).
88 #[arg(long, short)]
89 client: u64,
90 },
91
92 /// Get a specific client's cursor position.
93 Cursor {
94 /// Target client ID to query cursor from (required).
95 #[arg(long, short)]
96 client: u64,
97 },
98
99 /// List open buffers.
100 Buffers,
101
102 /// Get buffer content.
103 Buffer {
104 /// Buffer ID (uses active buffer if not specified).
105 #[arg(long)]
106 id: Option<u64>,
107 },
108
109 /// Get register contents.
110 ///
111 /// Without arguments, lists all non-empty registers.
112 /// With a register name, shows that register's content.
113 Registers {
114 /// Register name (e.g., "a", "\"", "0").
115 name: Option<String>,
116 },
117
118 /// Capture screen content.
119 ///
120 /// For text formats (`plain_text`, `raw_ansi`, `cell_grid`): captures via gRPC relay
121 /// from a connected TUI client (requires `--client`).
122 ///
123 /// For visual formats (`png`, `html`): captures via Playwright headless browser
124 /// running the real web client (requires `--web-url`).
125 Capture {
126 /// Target client ID (required for text capture, ignored for web capture).
127 #[arg(long, short)]
128 client: Option<u64>,
129
130 /// Capture format: `raw_ansi`, `plain_text`, `cell_grid`, `png`, `html`.
131 #[arg(long, short = 'f', default_value = "raw_ansi")]
132 capture_format: String,
133
134 /// Web client URL for visual capture (required for png/html formats).
135 #[arg(long)]
136 web_url: Option<String>,
137
138 /// Viewport width in pixels (web capture only).
139 #[arg(long, default_value = "1920")]
140 width: u32,
141
142 /// Viewport height in pixels (web capture only).
143 #[arg(long, default_value = "1080")]
144 height: u32,
145
146 /// Device pixel ratio (web capture only).
147 #[arg(long, default_value = "1")]
148 dpr: u32,
149
150 /// Output file path (web capture only; stdout if omitted).
151 #[arg(long, short)]
152 output: Option<String>,
153 },
154
155 /// Ping the server (health check).
156 Ping,
157
158 /// Get server version and info.
159 Version,
160
161 /// List connected clients (read-only debug query).
162 Clients,
163
164 /// Query extension state (e.g., which-key, cmdline).
165 ExtensionState {
166 /// Extension kind to query (e.g., "whichkey", "cmdline").
167 kind: String,
168
169 /// Target client ID.
170 #[arg(long, short)]
171 client: u64,
172 },
173
174 /// List registered extensions.
175 Extensions,
176}
177
178impl CliArgs {
179 /// Execute the CLI command.
180 ///
181 /// # Errors
182 ///
183 /// Returns an error if the gRPC connection fails or the command fails.
184 #[cfg_attr(coverage_nightly, coverage(off))]
185 pub async fn execute(&self) -> Result<String, GrpcClientError> {
186 let mut client = GrpcClient::connect(&self.grpc).await?;
187
188 match &self.command {
189 CliCommand::Keys {
190 keys,
191 client: target,
192 } => commands::keys(&mut client, keys, *target, self.format).await,
193 CliCommand::Mode { client: target } => {
194 commands::mode(&mut client, *target, self.format).await
195 }
196 CliCommand::Cursor { client: target } => {
197 commands::cursor(&mut client, *target, self.format).await
198 }
199 CliCommand::Buffers => commands::buffers(&mut client, self.format).await,
200 CliCommand::Buffer { id } => commands::buffer(&mut client, *id, self.format).await,
201 CliCommand::Registers { name } => {
202 commands::registers(&mut client, name.clone(), self.format).await
203 }
204 CliCommand::Capture {
205 client: client_id,
206 capture_format,
207 web_url,
208 width,
209 height,
210 dpr,
211 output,
212 } => {
213 let address = &self.grpc;
214 commands::capture(
215 &mut client,
216 *client_id,
217 capture_format,
218 web_url.as_deref(),
219 address,
220 *width,
221 *height,
222 *dpr,
223 output.as_deref(),
224 self.format,
225 )
226 .await
227 }
228 CliCommand::Ping => commands::ping(&mut client, self.format).await,
229 CliCommand::Version => commands::version(&mut client, self.format).await,
230 CliCommand::Clients => commands::clients(&mut client, self.format).await,
231 CliCommand::ExtensionState {
232 kind,
233 client: target,
234 } => commands::extension_state(&mut client, kind, *target, self.format).await,
235 CliCommand::Extensions => commands::extensions(&mut client, self.format).await,
236 }
237 }
238}
239
240#[cfg(test)]
241#[path = "lib_tests.rs"]
242mod tests;