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
84// Returns the path to the `xournalpp` executable based on the current operating system.
85pub fn bin_xournalpp() -> &'static str {
86    match std::env::consts::OS {
87        "linux" => "/usr/bin/xournalpp",
88        "macos" => "/Applications/Xournal++.app/Contents/MacOS/xournalpp",
89        &_ => todo!(),
90    }
91}
92
93fn install_via_apt(package: &str) {
94    match sudo::escalate_if_needed() {
95        Ok(_) => {
96            show_command(format!("sudo apt install {}", package));
97
98            let _status = Command::new("apt-get")
99                .arg("update")
100                .spawn()
101                .expect("apt-get update failure")
102                .wait();
103
104            let _status = Command::new("apt-get")
105                .arg("install")
106                .arg(package)
107                .spawn()
108                .expect("apt-get install failure")
109                .wait();
110        }
111        Err(e) => {
112            eprintln!("Failed to elevate: {}", e);
113            std::process::exit(1);
114        }
115    }
116}
117
118fn install_xournalpp() {
119    match std::env::consts::OS {
120        "linux" => {
121            install_via_apt("xournalpp");
122        }
123        "macos" => {
124            eprintln!("Install from https://github.com/xournalpp/xournalpp/releases/tag/nightly");
125            eprintln!("xattr -c /Applications/Xournal++.app");
126            eprintln!("codesign --force --deep --sign - /Applications/Xournal++.app");
127            std::process::exit(1);
128        }
129        _ => {
130            eprintln!(
131                "Error: Failure installing xournallpp in {}",
132                std::env::consts::OS
133            );
134            std::process::exit(1);
135        }
136    }
137}
138
139fn check_executable_exists(executable_name: &str) {
140    match find_executable_in_path(executable_name) {
141        Some(_path) => {
142            // println!("'{}' found in PATH at: {:?}", executable_name, path);
143            // Ok(())
144        }
145        None => {
146            match executable_name {
147                "xournalpp" => {
148                    install_xournalpp();
149                }
150                _ => todo!(),
151            }
152            std::process::exit(1);
153        }
154    }
155}
156
157/// Locates a file related to the given hash by searching an index file
158///
159/// # Parameters
160/// * `hash` - SHA256 hash prefix to search for in index.txt
161///
162/// # Returns
163/// `Some(filename)` if a matching file exists, otherwise `None`
164///
165pub fn locate_related_file(hash: &str) -> Option<String> {
166    let index_txt = &MUTABLE_CONFIG.get()?.lock().unwrap().index_txt;
167    let contents = std::fs::read_to_string(index_txt);
168    for line in contents.expect("Failure reading index.txt").lines() {
169        if line.starts_with(hash) {
170            let filename = line.split_whitespace().nth(1).unwrap();
171            let file_path = Path::new(filename);
172            if file_path.exists() {
173                println!("Found {}", filename);
174                return Some(filename.to_string());
175            } else {
176                println!("Not found {}", filename);
177            }
178        }
179    }
180    None
181}
182
183// osascript -e "tell application \"Xournal++\" to activate"
184fn bring_app_to_front(app_name: &str) {
185    match std::env::consts::OS {
186        "macos" => {
187            let script = format!("tell application \"{}\" to activate", app_name);
188            Command::new("osascript")
189                .arg("-e")
190                .arg(&script)
191                .output()
192                .expect("Failed to execute AppleScript");
193        }
194        &_ => todo!(),
195    }
196}
197
198pub fn search_text(pattern: &str) -> Option<Vec<String>> {
199    let re = RegexBuilder::new(pattern)
200        .case_insensitive(true)
201        .build()
202        .expect("Invalid regex pattern");
203    let index_txt = &MUTABLE_CONFIG.get()?.lock().unwrap().index_txt;
204    let contents = std::fs::read_to_string(index_txt);
205    let mut list: Vec<String> = Vec::new();
206    for line in contents.expect("Failure reading index.txt").lines() {
207        if re.is_match(line) {
208            list.push(String::from(line));
209        }
210    }
211    if list.is_empty() { None } else { Some(list) }
212}
213
214pub fn show_bookmark(hash: &str) -> Option<Vec<String>> {
215    let re = RegexBuilder::new(hash)
216        .case_insensitive(true)
217        .build()
218        .expect("Invalid regex pattern");
219    let bookmarks_txt = &MUTABLE_CONFIG.get()?.lock().unwrap().bookmarks_txt;
220    let contents = std::fs::read_to_string(bookmarks_txt);
221    let mut list: Vec<String> = Vec::new();
222    for line in contents.expect("Failure reading bookmarks.txt").lines() {
223        if re.is_match(line) {
224            list.push(String::from(line));
225        }
226    }
227    if list.is_empty() { None } else { Some(list) }
228}
229
230pub fn cmd_xournal(action: XournalAction, _verbose: bool) -> Result<(), &'static str> {
231    match action {
232        XournalAction::Open { hash } => {
233            check_executable_exists(bin_xournalpp());
234            match locate_related_file(&hash) {
235                Some(filename) => {
236                    let hash_and_filename = format!("{}\n{}", hash, filename);
237                    let _ = copy_text_to_clipboard(hash_and_filename);
238
239                    let _ = Command::new(bin_xournalpp())
240                        .arg(filename)
241                        .stdout(Stdio::null()) // Redirect standard output to null
242                        .stderr(Stdio::null()) // Redirect standard error to null
243                        .spawn()
244                        .expect("Failure to execute xournallpp")
245                        .wait();
246
247                    bring_app_to_front("Xournal++");
248
249                    println!("Please check Xournal++ window");
250                    Ok(())
251                }
252                None => Err("Hash not found at index.txt"),
253            }
254        }
255        XournalAction::Search { text } => match search_text(&text) {
256            Some(lines) => {
257                for line in lines {
258                    println!("{}", &line);
259                }
260                Ok(())
261            }
262            None => Err("Not found"),
263        },
264        XournalAction::Bookmark { hash } => {
265            show_bookmark(&hash);
266            Ok(())
267        }
268    }
269}
270
271pub fn cmd_microci(action: MicroCIAction) -> Result<(), &'static str> {
272    match action {
273        MicroCIAction::Install => {
274            match std::env::consts::OS {
275                "linux" => match sudo::escalate_if_needed() {
276                    Ok(_) => {
277                        let url = "https://github.com/geraldolsribeiro/microci/releases/latest/download/microCI";
278                        let _status = Command::new("curl")
279                            .arg("-fsSL")
280                            .arg(url)
281                            .arg("-o")
282                            .arg("/usr/bin/microCI")
283                            .spawn()
284                            .expect("curl microci")
285                            .wait();
286                        let _status = Command::new("chmod")
287                            .arg("755")
288                            .arg("/usr/bin/microCI")
289                            .spawn()
290                            .expect("chmod microci")
291                            .wait();
292                        let _status = Command::new("microCI")
293                            .arg("--version")
294                            .spawn()
295                            .expect("microci --version")
296                            .wait();
297                        Ok(())
298                    }
299                    Err(e) => {
300                        eprintln!("Failed to elevate: {}", e);
301                        std::process::exit(1);
302                    }
303                },
304                "macos" => {
305                    // https://github.com/geraldolsribeiro/homebrew-tap
306                    let _status = Command::new("brew")
307                        .arg("install")
308                        .arg("geraldolsribeiro/tap/microci")
309                        .spawn()
310                        .expect("brew install microci")
311                        .wait();
312                    let _status = Command::new("microCI")
313                        .arg("--version")
314                        .spawn()
315                        .expect("microci --version")
316                        .wait();
317                    Ok(())
318                }
319                &_ => todo!(),
320            }
321        }
322    }
323}