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 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
84pub 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 }
145 None => {
146 match executable_name {
147 "xournalpp" => {
148 install_xournalpp();
149 }
150 _ => todo!(),
151 }
152 std::process::exit(1);
153 }
154 }
155}
156
157pub 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
183fn 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()) .stderr(Stdio::null()) .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 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}