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}
38
39#[derive(Subcommand)]
40pub enum XournalAction {
41    #[clap(alias("o"))]
42    Open {
43        #[arg(required = true, num_args = 1)]
44        hash: String,
45    },
46    #[clap(alias("s"))]
47    Search {
48        #[arg(required = true, num_args = 1)]
49        text: String,
50    },
51    #[clap(alias("b"))]
52    Bookmark {
53        #[arg(required = true, num_args = 1)]
54        hash: String,
55    },
56}
57
58fn show_command(cmd: String) {
59    println!("CMD: {}", cmd.green().bold());
60}
61
62pub fn copy_text_to_clipboard(text: String) -> Result<(), Box<dyn Error>> {
63    let mut ctx = cli_clipboard::ClipboardContext::new()?;
64    ctx.set_contents(text.to_owned())?;
65    Ok(())
66}
67
68pub fn copy_text_from_clipboard() -> Result<String, Box<dyn Error>> {
69    let mut ctx = ClipboardContext::new()?;
70    let contents = ctx.get_contents()?;
71    Ok(contents)
72}
73
74pub fn bin_xournalpp() -> &'static str {
75    match std::env::consts::OS {
76        "linux" => "/usr/bin/xournalpp",
77        "macos" => "/Applications/Xournal++.app/Contents/MacOS/xournalpp",
78        &_ => todo!(),
79    }
80}
81
82fn install_via_apt(package: &str) {
83    match sudo::escalate_if_needed() {
84        Ok(_) => {
85            show_command(format!("sudo apt install {}", package));
86
87            let _status = Command::new("apt-get")
88                .arg("update")
89                .spawn()
90                .expect("apt-get update failure")
91                .wait();
92
93            let _status = Command::new("apt-get")
94                .arg("install")
95                .arg(package)
96                .spawn()
97                .expect("apt-get install failure")
98                .wait();
99        }
100        Err(e) => {
101            eprintln!("Failed to elevate: {}", e);
102            std::process::exit(1);
103        }
104    }
105}
106
107fn install_xournalpp() {
108    match std::env::consts::OS {
109        "linux" => {
110            install_via_apt("xournalpp");
111        }
112        "macos" => {
113            eprintln!("Install from https://github.com/xournalpp/xournalpp/releases/tag/nightly");
114            eprintln!("xattr -c /Applications/Xournal++.app");
115            eprintln!("codesign --force --deep --sign - /Applications/Xournal++.app");
116            std::process::exit(1);
117        }
118        _ => {
119            eprintln!(
120                "Error: Failure installing xournallpp in {}",
121                std::env::consts::OS
122            );
123            std::process::exit(1);
124        }
125    }
126}
127
128fn check_executable_exists(executable_name: &str) {
129    match find_executable_in_path(executable_name) {
130        Some(_path) => {
131            // println!("'{}' found in PATH at: {:?}", executable_name, path);
132            // Ok(())
133        }
134        None => {
135            match executable_name {
136                "xournalpp" => {
137                    install_xournalpp();
138                }
139                _ => todo!(),
140            }
141            std::process::exit(1);
142        }
143    }
144}
145
146/// Locates a file related to the given hash by searching an index file
147///
148/// # Parameters
149/// * `hash` - SHA256 hash prefix to search for in index.txt
150///
151/// # Returns
152/// `Some(filename)` if a matching file exists, otherwise `None`
153///
154fn locate_related_file(hash: &str) -> Option<String> {
155    let index_txt = &MUTABLE_CONFIG.get()?.lock().unwrap().index_txt;
156    let contents = std::fs::read_to_string(index_txt);
157    for line in contents.expect("Failure reading index.txt").lines() {
158        if line.starts_with(hash) {
159            let filename = line.split_whitespace().nth(1).unwrap();
160            let file_path = Path::new(filename);
161            if file_path.exists() {
162                println!("Found {}", filename);
163                return Some(filename.to_string());
164            } else {
165                println!("Not found {}", filename);
166            }
167        }
168    }
169    None
170}
171
172// osascript -e "tell application \"Xournal++\" to activate"
173fn bring_app_to_front(app_name: &str) {
174    match std::env::consts::OS {
175        "macos" => {
176            let script = format!("tell application \"{}\" to activate", app_name);
177            Command::new("osascript")
178                .arg("-e")
179                .arg(&script)
180                .output()
181                .expect("Failed to execute AppleScript");
182        }
183        &_ => todo!(),
184    }
185}
186
187pub fn search_text(pattern: &String) -> Option<Vec<String>> {
188    let re = RegexBuilder::new(pattern)
189        .case_insensitive(true)
190        .build()
191        .expect("Invalid regex pattern");
192    let index_txt = &MUTABLE_CONFIG.get()?.lock().unwrap().index_txt;
193    let contents = std::fs::read_to_string(index_txt);
194    let mut list: Vec<String> = Vec::new();
195    for line in contents.expect("Failure reading index.txt").lines() {
196        if re.is_match(line) {
197            list.push(String::from(line));
198        }
199    }
200    if list.is_empty() { None } else { Some(list) }
201}
202
203pub fn show_bookmark(hash: &String) -> Option<Vec<String>> {
204    let re = RegexBuilder::new(hash)
205        .case_insensitive(true)
206        .build()
207        .expect("Invalid regex pattern");
208    let bookmarks_txt = &MUTABLE_CONFIG.get()?.lock().unwrap().bookmarks_txt;
209    let contents = std::fs::read_to_string(bookmarks_txt);
210    let mut list: Vec<String> = Vec::new();
211    for line in contents.expect("Failure reading bookmarks.txt").lines() {
212        if re.is_match(line) {
213            list.push(String::from(line));
214        }
215    }
216    if list.is_empty() { None } else { Some(list) }
217}
218
219pub fn cmd_xournal(action: XournalAction, _verbose: bool) -> Result<(), &'static str> {
220    match action {
221        XournalAction::Open { hash } => {
222            check_executable_exists(bin_xournalpp());
223            match locate_related_file(&hash) {
224                Some(filename) => {
225                    let hash_and_filename = format!("{}\n{}", hash, filename);
226                    let _ = copy_text_to_clipboard(hash_and_filename);
227
228                    let _ = Command::new(bin_xournalpp())
229                        .arg(filename)
230                        .stdout(Stdio::null()) // Redirect standard output to null
231                        .stderr(Stdio::null()) // Redirect standard error to null
232                        .spawn()
233                        .expect("Failure to execute xournallpp");
234                    // .wait(); // Keep in background
235
236                    bring_app_to_front("Xournal++");
237
238                    println!("Please check Xournal++ window");
239                    Ok(())
240                }
241                None => Err("Hash not found at index.txt"),
242            }
243        }
244        XournalAction::Search { text } => match search_text(&text) {
245            Some(lines) => {
246                for line in lines {
247                    println!("{}", &line);
248                }
249                Ok(())
250            }
251            None => Err("Not found"),
252        },
253        XournalAction::Bookmark { hash } => {
254            show_bookmark(&hash);
255            Ok(())
256        }
257    }
258}
259
260#[cfg(test)]
261mod tests {
262    use super::*;
263
264    #[test]
265    fn test_config() {
266        let path = "~/pdf_images/index.txt".to_string();
267        initialize_mutable_config(path.clone());
268        let index_txt_path = &MUTABLE_CONFIG
269            .get()
270            .expect("Error in config")
271            .lock()
272            .unwrap()
273            .index_txt_path;
274        assert_eq!(index_txt_path, &path);
275    }
276
277    #[test]
278    fn test_copy_text_to_clipboard() {
279        let text1 = "Ipsum lorem".to_string();
280        let _ = copy_text_to_clipboard(text1);
281        let text2 = copy_text_from_clipboard();
282        assert_eq!(text2.unwrap(), "Ipsum lorem".to_string());
283    }
284
285    #[test]
286    fn test_cmd_xournal() {
287        let result = cmd_xournal(
288            XournalAction::Open {
289                hash: "12345678".to_string(),
290            },
291            false,
292        );
293        assert!(result.is_err());
294        let error = result.unwrap_err();
295        assert_eq!(error, "Hash not found at index.txt");
296    }
297
298    #[test]
299    #[ignore = "not yet implemented"]
300    fn test_locate_related_file() {
301        // ...
302    }
303}