Rust · 5935 bytes Raw Blame History
1 use anyhow::Result;
2 use clap::{Parser, Subcommand};
3
4 use garclip::ipc::protocol::{Command, EntryInfo, PasteResponse, StatusInfo};
5 use garclip::ipc::{client::default_socket_path, send_command_blocking};
6
7 #[derive(Parser)]
8 #[command(name = "garclipctl")]
9 #[command(about = "Control the garclip clipboard daemon")]
10 #[command(version)]
11 struct Cli {
12 #[command(subcommand)]
13 command: Commands,
14
15 /// Custom socket path
16 #[arg(short, long, global = true)]
17 socket: Option<std::path::PathBuf>,
18
19 /// Output as JSON
20 #[arg(long, global = true)]
21 json: bool,
22 }
23
24 #[derive(Subcommand)]
25 enum Commands {
26 /// Copy text to clipboard
27 Copy {
28 /// Text to copy (reads from stdin if not provided)
29 text: Option<String>,
30 },
31
32 /// Get current clipboard content
33 Paste,
34
35 /// Show clipboard history
36 History {
37 /// Maximum entries to show
38 #[arg(short, long, default_value = "20")]
39 limit: usize,
40 },
41
42 /// Select an entry from history (makes it current)
43 Select {
44 /// Entry ID
45 id: u64,
46 },
47
48 /// Delete an entry from history
49 Delete {
50 /// Entry ID
51 id: u64,
52 },
53
54 /// Clear clipboard
55 Clear,
56
57 /// Clear history
58 ClearHistory {
59 /// Keep pinned entries
60 #[arg(long)]
61 keep_pinned: bool,
62 },
63
64 /// Pin an entry (won't be removed by history limit)
65 Pin {
66 /// Entry ID
67 id: u64,
68 },
69
70 /// Unpin an entry
71 Unpin {
72 /// Entry ID
73 id: u64,
74 },
75
76 /// List pinned entries
77 Pinned,
78
79 /// Search history
80 Search {
81 /// Search query
82 query: String,
83
84 /// Maximum results
85 #[arg(short, long, default_value = "20")]
86 limit: usize,
87 },
88
89 /// Show daemon status
90 Status,
91
92 /// Reload daemon configuration
93 Reload,
94
95 /// Stop the daemon
96 Stop,
97 }
98
99 fn main() -> Result<()> {
100 let cli = Cli::parse();
101 let socket = cli.socket.unwrap_or_else(default_socket_path);
102
103 let cmd = match &cli.command {
104 Commands::Copy { text } => {
105 let text = if let Some(t) = text {
106 t.clone()
107 } else {
108 use std::io::Read;
109 let mut buf = String::new();
110 std::io::stdin().read_to_string(&mut buf)?;
111 buf
112 };
113 Command::Copy { text }
114 }
115 Commands::Paste => Command::Paste,
116 Commands::History { limit } => Command::History { limit: *limit },
117 Commands::Select { id } => Command::Select { id: *id },
118 Commands::Delete { id } => Command::Delete { id: *id },
119 Commands::Clear => Command::Clear,
120 Commands::ClearHistory { keep_pinned } => Command::ClearHistory {
121 keep_pinned: *keep_pinned,
122 },
123 Commands::Pin { id } => Command::Pin { id: *id },
124 Commands::Unpin { id } => Command::Unpin { id: *id },
125 Commands::Pinned => Command::ListPinned,
126 Commands::Search { query, limit } => Command::Search {
127 query: query.clone(),
128 limit: *limit,
129 },
130 Commands::Status => Command::Status,
131 Commands::Reload => Command::Reload,
132 Commands::Stop => Command::Quit,
133 };
134
135 let response = send_command_blocking(&socket, &cmd)?;
136
137 if !response.success {
138 eprintln!("Error: {}", response.error.unwrap_or_default());
139 std::process::exit(1);
140 }
141
142 // Handle output based on command
143 match cli.command {
144 Commands::Paste => {
145 if let Some(data) = response.data {
146 if cli.json {
147 println!("{}", serde_json::to_string_pretty(&data)?);
148 } else {
149 let paste: PasteResponse = serde_json::from_value(data)?;
150 if let Some(text) = paste.text {
151 print!("{}", text);
152 } else if paste.image_data.is_some() {
153 eprintln!("[Image - use --json for base64 data]");
154 }
155 }
156 }
157 }
158 Commands::History { .. } | Commands::Pinned | Commands::Search { .. } => {
159 if let Some(data) = response.data {
160 if cli.json {
161 println!("{}", serde_json::to_string_pretty(&data)?);
162 } else {
163 let entries: Vec<EntryInfo> = serde_json::from_value(data)?;
164 if entries.is_empty() {
165 println!("No entries");
166 } else {
167 for entry in entries {
168 let pin = if entry.pinned { "*" } else { " " };
169 println!(
170 "{}{:>4} | {:>5} | {}",
171 pin, entry.id, entry.content_type, entry.preview
172 );
173 }
174 }
175 }
176 }
177 }
178 Commands::Status => {
179 if let Some(data) = response.data {
180 if cli.json {
181 println!("{}", serde_json::to_string_pretty(&data)?);
182 } else {
183 let status: StatusInfo = serde_json::from_value(data)?;
184 println!("History: {} entries", status.history_count);
185 println!("Pinned: {} entries", status.pinned_count);
186 println!("Owns clipboard: {}", status.owns_clipboard);
187 println!("Watch PRIMARY: {}", status.watching_primary);
188 println!("Uptime: {}s", status.uptime_secs);
189 if let Some(preview) = status.current_preview {
190 println!("Current: {}", preview);
191 }
192 }
193 }
194 }
195 _ => {
196 // Commands with no output on success
197 }
198 }
199
200 Ok(())
201 }
202