Skip to main content

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;