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(author, version, about, long_about)]
21pub struct Cli {
22 #[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 }
134 None => {
135 match executable_name {
136 "xournalpp" => {
137 install_xournalpp();
138 }
139 _ => todo!(),
140 }
141 std::process::exit(1);
142 }
143 }
144}
145
146fn 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
172fn 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()) .stderr(Stdio::null()) .spawn()
233 .expect("Failure to execute xournallpp");
234 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 }
303}