1pub mod config;
2
3use crate::config::*;
4
5use clap::Parser;
6use clap::Subcommand;
7use 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 #[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 }
120 None => {
121 match executable_name {
122 "xournalpp" => {
123 install_xournalpp();
124 }
125 _ => todo!(),
126 }
127 std::process::exit(1);
128 }
129 }
130}
131
132fn 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
159fn 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()) .stderr(Stdio::null()) .spawn()
188 .expect("Failure to execute xournallpp");
189 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 }
245}