1pub mod config;
2
3use crate::config::*;
4use clap::Parser;
5use clap::Subcommand;
6use cli_clipboard::ClipboardContext;
7use cli_clipboard::ClipboardProvider;
8use colored::Colorize;
9use pathsearch::find_executable_in_path;
10use regex::RegexBuilder;
11use std::error::Error;
12use std::path::Path;
13use std::process::Command;
14use std::process::Stdio;
15
16#[derive(Parser)]
17#[command(author, version, about, long_about)]
21pub struct Cli {
22 #[arg(short, long, global = true)]
24 pub verbose: bool,
25
26 #[command(subcommand)]
27 pub command: Commands,
28}
29
30#[derive(Subcommand)]
31pub enum Commands {
32 #[clap(alias("x"))]
33 Xournal {
34 #[command(subcommand)]
35 action: XournalAction,
36 },
37 Microci {
38 #[command(subcommand)]
39 action: MicroCIAction,
40 },
41}
42
43#[derive(Subcommand)]
44pub enum XournalAction {
45 #[clap(alias("o"))]
46 Open {
47 #[arg(required = true, num_args = 1)]
48 hash: String,
49 },
50 #[clap(alias("s"))]
51 Search {
52 #[arg(required = true, num_args = 1)]
53 text: String,
54 },
55 #[clap(alias("b"))]
56 Bookmark {
57 #[arg(required = true, num_args = 1)]
58 hash: String,
59 },
60}
61
62#[derive(Subcommand)]
63pub enum MicroCIAction {
64 #[clap(alias("m"))]
65 Install,
66}
67
68fn show_command(cmd: String) {
69 println!("CMD: {}", cmd.green().bold());
70}
71
72pub fn copy_text_to_clipboard(text: String) -> Result<(), Box<dyn Error>> {
73 let mut ctx = cli_clipboard::ClipboardContext::new()?;
74 ctx.set_contents(text.to_owned())?;
75 Ok(())
76}
77
78pub fn copy_text_from_clipboard() -> Result<String, Box<dyn Error>> {
79 let mut ctx = ClipboardContext::new()?;
80 let contents = ctx.get_contents()?;
81 Ok(contents)
82}
83
84pub fn bin_xournalpp() -> &'static str {
85 match std::env::consts::OS {
86 "linux" => "/usr/bin/xournalpp",
87 "macos" => "/Applications/Xournal++.app/Contents/MacOS/xournalpp",
88 &_ => todo!(),
89 }
90}
91
92fn install_via_apt(package: &str) {
93 match sudo::escalate_if_needed() {
94 Ok(_) => {
95 show_command(format!("sudo apt install {}", package));
96
97 let _status = Command::new("apt-get")
98 .arg("update")
99 .spawn()
100 .expect("apt-get update failure")
101 .wait();
102
103 let _status = Command::new("apt-get")
104 .arg("install")
105 .arg(package)
106 .spawn()
107 .expect("apt-get install failure")
108 .wait();
109 }
110 Err(e) => {
111 eprintln!("Failed to elevate: {}", e);
112 std::process::exit(1);
113 }
114 }
115}
116
117fn install_xournalpp() {
118 match std::env::consts::OS {
119 "linux" => {
120 install_via_apt("xournalpp");
121 }
122 "macos" => {
123 eprintln!("Install from https://github.com/xournalpp/xournalpp/releases/tag/nightly");
124 eprintln!("xattr -c /Applications/Xournal++.app");
125 eprintln!("codesign --force --deep --sign - /Applications/Xournal++.app");
126 std::process::exit(1);
127 }
128 _ => {
129 eprintln!(
130 "Error: Failure installing xournallpp in {}",
131 std::env::consts::OS
132 );
133 std::process::exit(1);
134 }
135 }
136}
137
138fn check_executable_exists(executable_name: &str) {
139 match find_executable_in_path(executable_name) {
140 Some(_path) => {
141 }
144 None => {
145 match executable_name {
146 "xournalpp" => {
147 install_xournalpp();
148 }
149 _ => todo!(),
150 }
151 std::process::exit(1);
152 }
153 }
154}
155
156fn locate_related_file(hash: &str) -> Option<String> {
165 let index_txt = &MUTABLE_CONFIG.get()?.lock().unwrap().index_txt;
166 let contents = std::fs::read_to_string(index_txt);
167 for line in contents.expect("Failure reading index.txt").lines() {
168 if line.starts_with(hash) {
169 let filename = line.split_whitespace().nth(1).unwrap();
170 let file_path = Path::new(filename);
171 if file_path.exists() {
172 println!("Found {}", filename);
173 return Some(filename.to_string());
174 } else {
175 println!("Not found {}", filename);
176 }
177 }
178 }
179 None
180}
181
182fn bring_app_to_front(app_name: &str) {
184 match std::env::consts::OS {
185 "macos" => {
186 let script = format!("tell application \"{}\" to activate", app_name);
187 Command::new("osascript")
188 .arg("-e")
189 .arg(&script)
190 .output()
191 .expect("Failed to execute AppleScript");
192 }
193 &_ => todo!(),
194 }
195}
196
197pub fn search_text(pattern: &String) -> Option<Vec<String>> {
198 let re = RegexBuilder::new(pattern)
199 .case_insensitive(true)
200 .build()
201 .expect("Invalid regex pattern");
202 let index_txt = &MUTABLE_CONFIG.get()?.lock().unwrap().index_txt;
203 let contents = std::fs::read_to_string(index_txt);
204 let mut list: Vec<String> = Vec::new();
205 for line in contents.expect("Failure reading index.txt").lines() {
206 if re.is_match(line) {
207 list.push(String::from(line));
208 }
209 }
210 if list.is_empty() { None } else { Some(list) }
211}
212
213pub fn show_bookmark(hash: &String) -> Option<Vec<String>> {
214 let re = RegexBuilder::new(hash)
215 .case_insensitive(true)
216 .build()
217 .expect("Invalid regex pattern");
218 let bookmarks_txt = &MUTABLE_CONFIG.get()?.lock().unwrap().bookmarks_txt;
219 let contents = std::fs::read_to_string(bookmarks_txt);
220 let mut list: Vec<String> = Vec::new();
221 for line in contents.expect("Failure reading bookmarks.txt").lines() {
222 if re.is_match(line) {
223 list.push(String::from(line));
224 }
225 }
226 if list.is_empty() { None } else { Some(list) }
227}
228
229pub fn cmd_xournal(action: XournalAction, _verbose: bool) -> Result<(), &'static str> {
230 match action {
231 XournalAction::Open { hash } => {
232 check_executable_exists(bin_xournalpp());
233 match locate_related_file(&hash) {
234 Some(filename) => {
235 let hash_and_filename = format!("{}\n{}", hash, filename);
236 let _ = copy_text_to_clipboard(hash_and_filename);
237
238 let _ = Command::new(bin_xournalpp())
239 .arg(filename)
240 .stdout(Stdio::null()) .stderr(Stdio::null()) .spawn()
243 .expect("Failure to execute xournallpp");
244 bring_app_to_front("Xournal++");
247
248 println!("Please check Xournal++ window");
249 Ok(())
250 }
251 None => Err("Hash not found at index.txt"),
252 }
253 }
254 XournalAction::Search { text } => match search_text(&text) {
255 Some(lines) => {
256 for line in lines {
257 println!("{}", &line);
258 }
259 Ok(())
260 }
261 None => Err("Not found"),
262 },
263 XournalAction::Bookmark { hash } => {
264 show_bookmark(&hash);
265 Ok(())
266 }
267 }
268}
269
270pub fn cmd_microci(action: MicroCIAction) -> Result<(), &'static str> {
271 match action {
272 MicroCIAction::Install => {
273 match std::env::consts::OS {
274 "linux" => {
275 match sudo::escalate_if_needed() {
276 Ok(_) => {
277 let url = "https://github.com/geraldolsribeiro/microci/releases/latest/download/microCI";
282 let _status = Command::new("curl")
283 .arg("-fsSL")
284 .arg(url)
285 .arg("-o")
286 .arg("/usr/bin/microCI")
287 .spawn()
288 .expect("curl microci")
289 .wait();
290 let _status = Command::new("chmod")
291 .arg("755")
292 .arg("/usr/bin/microCI")
293 .spawn()
294 .expect("chmod microci")
295 .wait();
296 Ok(())
297 }
298 Err(e) => {
299 eprintln!("Failed to elevate: {}", e);
300 std::process::exit(1);
301 }
302 }
303 }
304 "macos" => {
305 let _status = Command::new("brew")
306 .arg("install")
307 .arg("geraldolsribeiro/tap/microci")
308 .spawn()
309 .expect("brew install microci")
310 .wait();
311
312 Ok(())
313 }
314 &_ => todo!(),
315 }
316 }
317 }
318}
319
320#[cfg(test)]
321mod tests {
322 use super::*;
323
324 #[test]
325 fn test_config() {
326 let path = "~/pdf_images/index.txt".to_string();
327 initialize_mutable_config(path.clone());
328 let index_txt_path = &MUTABLE_CONFIG
329 .get()
330 .expect("Error in config")
331 .lock()
332 .unwrap()
333 .index_txt_path;
334 assert_eq!(index_txt_path, &path);
335 }
336
337 #[test]
338 fn test_copy_text_to_clipboard() {
339 let text1 = "Ipsum lorem".to_string();
340 let _ = copy_text_to_clipboard(text1);
341 let text2 = copy_text_from_clipboard();
342 assert_eq!(text2.unwrap(), "Ipsum lorem".to_string());
343 }
344
345 #[test]
346 fn test_cmd_xournal() {
347 let result = cmd_xournal(
348 XournalAction::Open {
349 hash: "12345678".to_string(),
350 },
351 false,
352 );
353 assert!(result.is_err());
354 let error = result.unwrap_err();
355 assert_eq!(error, "Hash not found at index.txt");
356 }
357
358 #[test]
359 #[ignore = "not yet implemented"]
360 fn test_locate_related_file() {
361 }
363}