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 fn map(&mut self) -> &mut HashMap<K, V>;
55
56 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 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 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 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 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 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 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}