quartz_cli/
lib.rs

1pub mod action;
2pub mod cli;
3pub mod config;
4pub mod cookie;
5pub mod endpoint;
6pub mod env;
7pub mod history;
8pub mod snippet;
9pub mod state;
10pub mod tree;
11pub mod validator;
12
13use std::error::Error;
14use std::fmt::Display;
15use std::hash::Hash;
16use std::io::Write;
17use std::path::{Path, PathBuf};
18use std::process::{ExitCode, Stdio};
19use std::{collections::HashMap, ffi::OsString};
20
21use colored::Colorize;
22
23use config::Config;
24use endpoint::{Endpoint, EndpointHandle};
25use env::Env;
26use state::{State, StateField};
27
28pub type QuartzResult<T = (), E = Box<dyn std::error::Error>> = Result<T, E>;
29
30#[derive(Debug)]
31pub enum QuartzError {
32    Internal,
33}
34
35impl Display for QuartzError {
36    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
37        match self {
38            QuartzError::Internal => writeln!(f, "internal failure"),
39        }
40    }
41}
42
43impl Error for QuartzError {}
44
45pub trait PairMap<'a, K = String, V = String>
46where
47    K: Eq + PartialEq + Hash + From<&'a str>,
48    V: From<&'a str>,
49{
50    const NAME: &'static str = "key-value pair";
51    const EXPECTED: &'static str = "<key>=<value>";
52
53    /// Returns HashMap in the implementation struct.
54    fn map(&mut self) -> &mut HashMap<K, V>;
55
56    /// Breaks string into (key, value) tuple.
57    fn pair(input: &'a str) -> Option<(K, V)> {
58        let (key, value) = input.split_once('=')?;
59        let value = value.trim_matches('\'').trim_matches('\"');
60
61        Some((key.into(), value.into()))
62    }
63
64    /// Inserts key-value pair into map.
65    fn set(&mut self, input: &'a str) {
66        let (key, value) = Self::pair(input)
67            .unwrap_or_else(|| panic!("malformed {}. Expected {}", Self::NAME, Self::EXPECTED));
68
69        self.map().insert(key, value);
70    }
71}
72
73pub struct CtxArgs {
74    pub from_handle: Option<String>,
75    pub early_apply_environment: bool,
76}
77
78pub struct Ctx {
79    pub args: CtxArgs,
80    pub config: Config,
81    pub state: State,
82    path: PathBuf,
83    code: ExitCode,
84}
85
86impl Ctx {
87    const VERSION: &'static str = env!("CARGO_PKG_VERSION");
88
89    pub fn new(args: CtxArgs) -> QuartzResult<Self> {
90        let config = Config::parse();
91        let state = State {
92            handle: args.from_handle.clone(),
93            previous_handle: None,
94        };
95
96        let mut path = std::env::current_dir()?;
97        loop {
98            if path.join(".quartz").exists() {
99                break;
100            }
101
102            if !path.pop() {
103                panic!("could not find a quartz project");
104            }
105        }
106
107        Ok(Ctx {
108            args,
109            config,
110            state,
111            path: path.join(".quartz"),
112            code: ExitCode::default(),
113        })
114    }
115
116    pub fn require_input_handle(&self, handle: &str) -> EndpointHandle {
117        let result = EndpointHandle::from(handle);
118
119        if !result.exists(self) {
120            panic!("could not find {} handle", handle.red());
121        }
122
123        result
124    }
125
126    pub fn require_handle(&self) -> EndpointHandle {
127        if let Some(handle) = &self.args.from_handle {
128            // Overwritten by argument
129            return EndpointHandle::from(handle);
130        }
131
132        let mut result = None;
133        if let Ok(handle) = self.state.get(self, StateField::Endpoint) {
134            if !handle.is_empty() {
135                result = Some(EndpointHandle::from(handle));
136            }
137        }
138
139        match result {
140            Some(handle) => handle,
141            None => panic!("no handle in use. Try {}", "quartz use <HANDLE>".green()),
142        }
143    }
144
145    pub fn require_endpoint(&self) -> (EndpointHandle, Endpoint) {
146        let handle = self.require_handle();
147        let endpoint = self.require_endpoint_from_handle(&handle);
148
149        (handle, endpoint)
150    }
151
152    pub fn require_endpoint_from_handle(&self, handle: &EndpointHandle) -> Endpoint {
153        let mut endpoint = handle.endpoint(self).unwrap_or_else(|| {
154            panic!("no endpoint at {}", handle.handle().red());
155        });
156
157        if self.args.early_apply_environment {
158            let env = self.require_env();
159            endpoint.apply_env(&env);
160        }
161
162        endpoint
163    }
164
165    /// Returns current env.
166    ///
167    /// # Panics
168    ///
169    /// Program is terminated if it is unable to require it.
170    pub fn require_env(&self) -> Env {
171        let state = self
172            .state
173            .get(self, StateField::Env)
174            .unwrap_or("default".into());
175
176        Env::parse(self, &state)
177            .unwrap_or_else(|_| panic!("could not resolve {} environment", state.red()))
178    }
179
180    /// Opens an editor to modified the specified file at `path` in a temporary file.
181    ///
182    /// After the program exits, `validate` function is ran on temporary file before moving it to
183    /// the original file, effectively commiting the edits.
184    ///
185    /// If `validate` returns [`Err`], the temporary file is deleted while original file is preserved as is.
186    ///
187    /// # Arguments
188    ///
189    /// * `path` - A path slice to a file
190    /// * `validate` - Validator method to ensure the edit can be saved without errors
191    pub fn edit<F>(&self, path: &Path, validate: F) -> QuartzResult
192    where
193        F: FnOnce(&str) -> QuartzResult,
194    {
195        self.edit_with_extension::<F>(path, None, validate)
196    }
197
198    /// Opens an editor to modified the specified file at `path` with `extension` in a temporary file.
199    ///
200    /// After the program exits, `validate` function is ran on temporary file before moving it to
201    /// the original file, effectively commiting the edits.
202    ///
203    /// If `validate` returns [`Err`], the temporary file is deleted while original file is preserved as is.
204    ///
205    /// # Arguments
206    ///
207    /// * `path` - A path slice to a file
208    /// * `extension` - Which extension to create temporary file with
209    /// * `validate` - Validator method to ensure the edit can be saved without errors
210    pub fn edit_with_extension<F>(
211        &self,
212        path: &Path,
213        extension: Option<&str>,
214        validate: F,
215    ) -> QuartzResult
216    where
217        F: FnOnce(&str) -> QuartzResult,
218    {
219        let mut temp_path = self.path().join("user").join("EDIT");
220
221        let extension: Option<OsString> = {
222            if let Some(extension) = extension {
223                Some(OsString::from(extension))
224            } else {
225                path.extension().map(|extension| extension.to_os_string())
226            }
227        };
228
229        if let Some(extension) = extension {
230            temp_path.set_extension(extension);
231        }
232
233        if !path.exists() {
234            std::fs::File::create(path)?;
235        }
236
237        std::fs::copy(path, &temp_path)?;
238
239        let editor = self.config.preferences.editor();
240        let _ = std::process::Command::new(&editor)
241            .arg(&temp_path)
242            .status()
243            .unwrap_or_else(|err| {
244                panic!("failed to open editor: {}\n\n{}", editor, err);
245            });
246
247        let content = std::fs::read_to_string(&temp_path)?;
248
249        if let Err(err) = validate(&content) {
250            std::fs::remove_file(&temp_path)?;
251            panic!("{}", err);
252        }
253
254        std::fs::rename(&temp_path, path)?;
255        Ok(())
256    }
257
258    /// Open user's preferred pager with content.
259    pub fn paginate(&self, input: &[u8]) -> QuartzResult {
260        let pager = self.config.preferences.pager();
261
262        let mut child = std::process::Command::new(&pager)
263            .stdin(Stdio::piped())
264            .spawn()
265            .unwrap_or_else(|err| {
266                panic!("failed to open pager: {}\n\n{}", pager, err);
267            });
268
269        child.stdin.as_mut().unwrap().write_all(input)?;
270        child.wait()?;
271
272        Ok(())
273    }
274
275    pub fn user_agent() -> String {
276        let mut agent = String::from("quartz/");
277        agent.push_str(Ctx::VERSION);
278
279        agent
280    }
281
282    pub fn path(&self) -> &Path {
283        self.path.as_ref()
284    }
285
286    pub fn code(&mut self, value: ExitCode) {
287        self.code = value;
288    }
289
290    pub fn confirm(&self, message: &str) -> bool {
291        println!("{} {}", message, "(y/n)".dimmed());
292
293        std::io::stdout().flush().unwrap();
294
295        let term = console::Term::stdout();
296        let ch = term.read_char().unwrap_or('n').to_ascii_lowercase();
297
298        ch == 'y'
299    }
300
301    #[inline]
302    #[must_use]
303    pub fn exit_code(&self) -> &ExitCode {
304        &self.code
305    }
306}