Skip to main content

g_tools/
lib.rs

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(
18//     help_template = "{author-with-newline} {about-section}Version: {version} \n {usage-heading} {usage} \n {all-args} {tab}"
19// )]
20#[command(author, version, about, long_about)]
21pub struct Cli {
22    /// Enable verbose output
23    #[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            // println!("'{}' found in PATH at: {:?}", executable_name, path);
142            // Ok(())
143        }
144        None => {
145            match executable_name {
146                "xournalpp" => {
147                    install_xournalpp();
148                }
149                _ => todo!(),
150            }
151            std::process::exit(1);
152        }
153    }
154}
155
156/// Locates a file related to the given hash by searching an index file
157///
158/// # Parameters
159/// * `hash` - SHA256 hash prefix to search for in index.txt
160///
161/// # Returns
162/// `Some(filename)` if a matching file exists, otherwise `None`
163///
164fn 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
182// osascript -e "tell application \"Xournal++\" to activate"
183fn 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()) // Redirect standard output to null
241                        .stderr(Stdio::null()) // Redirect standard error to null
242                        .spawn()
243                        .expect("Failure to execute xournallpp");
244                    // .wait(); // Keep in background
245
246                    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                            // sudo curl -fsSL https://github.com/geraldolsribeiro/microci/releases/latest/download/microCI \
278                            //   -o /usr/bin/microCI
279                            // sudo chmod 755 /usr/bin/microCI
280
281                            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        // ...
362    }
363}