Skip to main content

g_tools/
lib.rs

1pub mod config;
2
3use crate::config::*;
4
5use clap::Parser;
6use clap::Subcommand;
7// use cli_clipboard::Clipboard;
8use cli_clipboard::ClipboardContext;
9use cli_clipboard::ClipboardProvider;
10use colored::Colorize;
11use pathsearch::find_executable_in_path;
12use std::error::Error;
13use std::path::Path;
14use std::process::Command;
15use std::process::Stdio;
16
17#[derive(Parser)]
18#[command(author, version, about, long_about)]
19pub struct Cli {
20    /// Enable verbose output
21    #[arg(short, long, global = true)]
22    pub verbose: bool,
23
24    #[command(subcommand)]
25    pub command: Commands,
26}
27
28#[derive(Subcommand)]
29pub enum Commands {
30    Xournal {
31        #[command(subcommand)]
32        action: XournalAction,
33    },
34}
35
36#[derive(Subcommand)]
37pub enum XournalAction {
38    Open {
39        #[arg(required = true, num_args = 1)]
40        hash: String,
41    },
42}
43
44fn show_command(cmd: String) {
45    println!("CMD: {}", cmd.green().bold());
46}
47
48pub fn copy_text_to_clipboard(text: String) -> Result<(), Box<dyn Error>> {
49    let mut ctx = cli_clipboard::ClipboardContext::new()?;
50    ctx.set_contents(text.to_owned())?;
51    Ok(())
52}
53
54pub fn copy_text_from_clipboard() -> Result<String, Box<dyn Error>> {
55    let mut ctx = ClipboardContext::new()?;
56    let contents = ctx.get_contents()?;
57    Ok(contents)
58}
59
60pub fn bin_xournalpp() -> &'static str {
61    match std::env::consts::OS {
62        "linux" => "/usr/bin/xournalpp",
63        "macos" => "/Applications/Xournal++.app/Contents/MacOS/xournalpp",
64        &_ => todo!(),
65    }
66}
67
68fn install_via_apt(package: &str) {
69    match sudo::escalate_if_needed() {
70        Ok(_) => {
71            show_command(format!("sudo apt install {}", package));
72
73            let _status = Command::new("apt-get")
74                .arg("update")
75                .spawn()
76                .expect("apt-get update failure")
77                .wait();
78
79            let _status = Command::new("apt-get")
80                .arg("install")
81                .arg(package)
82                .spawn()
83                .expect("apt-get install failure")
84                .wait();
85        }
86        Err(e) => {
87            eprintln!("Failed to elevate: {}", e);
88            std::process::exit(1);
89        }
90    }
91}
92
93fn install_xournalpp() {
94    match std::env::consts::OS {
95        "linux" => {
96            install_via_apt("xournalpp");
97        }
98        "macos" => {
99            eprintln!("Install from https://github.com/xournalpp/xournalpp/releases/tag/nightly");
100            eprintln!("xattr -c /Applications/Xournal++.app");
101            eprintln!("codesign --force --deep --sign - /Applications/Xournal++.app");
102            std::process::exit(1);
103        }
104        _ => {
105            eprintln!(
106                "Error: Failure installing xournallpp in {}",
107                std::env::consts::OS
108            );
109            std::process::exit(1);
110        }
111    }
112}
113
114fn check_executable_exists(executable_name: &str) {
115    match find_executable_in_path(executable_name) {
116        Some(_path) => {
117            // println!("'{}' found in PATH at: {:?}", executable_name, path);
118            // Ok(())
119        }
120        None => {
121            match executable_name {
122                "xournalpp" => {
123                    install_xournalpp();
124                }
125                _ => todo!(),
126            }
127            std::process::exit(1);
128        }
129    }
130}
131
132/// Locates a file related to the given hash by searching an index file
133///
134/// # Parameters
135/// * `hash` - SHA256 hash prefix to search for in index.txt
136///
137/// # Returns
138/// `Some(filename)` if a matching file exists, otherwise `None`
139///
140fn locate_related_file(hash: &str) -> Option<String> {
141    let index_txt_path = &MUTABLE_CONFIG.get()?.lock().unwrap().index_txt_path;
142    let expanded_path = shellexpand::tilde(index_txt_path);
143    let contents = std::fs::read_to_string(expanded_path.as_ref());
144    for line in contents.expect("Failure reading index.txt").lines() {
145        if line.starts_with(hash) {
146            let filename = line.split_whitespace().nth(1).unwrap();
147            let file_path = Path::new(filename);
148            if file_path.exists() {
149                println!("Found {}", filename);
150                return Some(filename.to_string());
151            } else {
152                println!("Not found {}", filename);
153            }
154        }
155    }
156    None
157}
158
159// osascript -e "tell application \"Xournal++\" to activate"
160fn bring_app_to_front(app_name: &str) {
161    match std::env::consts::OS {
162        "macos" => {
163            let script = format!("tell application \"{}\" to activate", app_name);
164            Command::new("osascript")
165                .arg("-e")
166                .arg(&script)
167                .output()
168                .expect("Failed to execute AppleScript");
169        }
170        &_ => todo!(),
171    }
172}
173
174pub fn cmd_xournal(action: XournalAction, _verbose: bool) -> Result<(), &'static str> {
175    match action {
176        XournalAction::Open { hash } => {
177            check_executable_exists(bin_xournalpp());
178            match locate_related_file(&hash) {
179                Some(filename) => {
180                    let hash_and_filename = format!("{}\n{}", hash, filename);
181                    let _ = copy_text_to_clipboard(hash_and_filename);
182
183                    let _ = Command::new(bin_xournalpp())
184                        .arg(filename)
185                        .stdout(Stdio::null()) // Redirect standard output to null
186                        .stderr(Stdio::null()) // Redirect standard error to null
187                        .spawn()
188                        .expect("Failure to execute xournallpp");
189                    // .wait(); // Keep in background
190
191                    bring_app_to_front("Xournal++");
192
193                    println!("Please check Xournal++ window");
194                    Ok(())
195                }
196                None => Err("Hash not found at index.txt"),
197            }
198        }
199    }
200}
201
202#[cfg(test)]
203mod tests {
204    use super::*;
205
206    #[test]
207    fn test_config() {
208        let path = "~/pdf_images/index.txt".to_string();
209        initialize_mutable_config(path.clone());
210        let index_txt_path = &MUTABLE_CONFIG
211            .get()
212            .expect("Error in config")
213            .lock()
214            .unwrap()
215            .index_txt_path;
216        assert_eq!(index_txt_path, &path);
217    }
218
219    #[test]
220    fn test_copy_text_to_clipboard() {
221        let text1 = "Ipsum lorem".to_string();
222        let _ = copy_text_to_clipboard(text1);
223        let text2 = copy_text_from_clipboard();
224        assert_eq!(text2.unwrap(), "Ipsum lorem".to_string());
225    }
226
227    #[test]
228    fn test_cmd_xournal() {
229        let result = cmd_xournal(
230            XournalAction::Open {
231                hash: "12345678".to_string(),
232            },
233            false,
234        );
235        assert!(result.is_err());
236        let error = result.unwrap_err();
237        assert_eq!(error, "Hash not found at index.txt");
238    }
239
240    #[test]
241    #[ignore = "not yet implemented"]
242    fn test_locate_related_file() {
243        // ...
244    }
245}