shulkerscript_cli/
util.rs1use std::{
2 borrow::Cow,
3 collections::HashMap,
4 env,
5 path::{Path, PathBuf},
6};
7
8use inquire::{autocompletion::Replacement, Autocomplete};
9use path_absolutize::Absolutize;
10
11pub fn get_project_path<P>(base_path: P) -> Option<PathBuf>
12where
13 P: AsRef<Path>,
14{
15 let base_path = base_path.as_ref();
16 if base_path.is_absolute() {
17 Cow::Borrowed(base_path)
18 } else {
19 base_path.absolutize().ok()?
20 }
21 .ancestors()
22 .find(|p| p.join("pack.toml").exists())
23 .map(|p| p.relativize().unwrap_or_else(|| p.to_path_buf()))
24}
25
26pub trait Relativize {
27 fn relativize(&self) -> Option<PathBuf>;
28}
29impl<P> Relativize for P
30where
31 P: AsRef<Path>,
32{
33 fn relativize(&self) -> Option<PathBuf> {
34 let cwd = env::current_dir().ok()?;
35 pathdiff::diff_paths(self, cwd)
36 }
37}
38
39#[derive(Debug, Clone, Default, PartialEq, Eq)]
40pub struct PathAutocomplete {
41 parent: String,
42 current: String,
43 outputs: Vec<String>,
44
45 cache: HashMap<String, Vec<String>>,
46}
47
48impl PathAutocomplete {
49 pub fn new() -> Self {
50 Self::default()
51 }
52
53 fn split_input(input: &str) -> (&str, &str) {
54 let (parent, current) = if input.ends_with('/') {
55 (input.trim_end_matches('/'), "")
56 } else if let Some((parent, current)) = input.rsplit_once('/') {
57 if parent.is_empty() {
58 ("/", current)
59 } else {
60 (parent, current)
61 }
62 } else {
63 ("", input)
64 };
65 let parent = if parent.is_empty() { "." } else { parent };
66
67 (parent, current)
68 }
69
70 fn get_cached(&mut self, parent: &str) -> Result<&[String], &'static str> {
71 if !self.cache.contains_key(parent) {
72 tracing::trace!("Cache miss for \"{}\", reading dir", parent);
73
74 let parent_path = PathBuf::from(parent);
75 if !parent_path.exists() || !parent_path.is_dir() {
76 return Err("Path does not exist");
77 }
78
79 let entries = parent_path
80 .read_dir()
81 .map_err(|_| "Could not read dir")?
82 .filter_map(|entry| {
83 entry.ok().and_then(|entry| {
84 Some(
85 entry.file_name().into_string().ok()?.to_string()
86 + if entry.path().is_dir() { "/" } else { "" },
87 )
88 })
89 })
90 .collect::<Vec<_>>();
91
92 self.cache.insert(parent.to_string(), entries);
93 }
94
95 Ok(self
96 .cache
97 .get(parent)
98 .expect("Previous caching did not work"))
99 }
100
101 fn update_input(&mut self, input: &str) -> Result<(), inquire::CustomUserError> {
102 let (parent, current) = Self::split_input(input);
103
104 if self.parent == parent && self.current == current {
105 Ok(())
106 } else {
107 self.parent = parent.to_string();
108 self.current = current.to_string();
109
110 self.outputs = self
111 .get_cached(parent)?
112 .iter()
113 .filter(|entry| entry.starts_with(current))
114 .cloned()
115 .collect::<Vec<_>>();
116
117 Ok(())
118 }
119 }
120}
121
122impl Autocomplete for PathAutocomplete {
123 fn get_suggestions(&mut self, input: &str) -> Result<Vec<String>, inquire::CustomUserError> {
124 self.update_input(input)?;
125
126 Ok(self.outputs.clone())
127 }
128
129 fn get_completion(
130 &mut self,
131 input: &str,
132 highlighted_suggestion: Option<String>,
133 ) -> Result<inquire::autocompletion::Replacement, inquire::CustomUserError> {
134 let (parent, current) = Self::split_input(input);
135
136 if let Some(highlighted) = highlighted_suggestion {
137 let completion = format!("{parent}/{highlighted}");
138 self.update_input(&completion)?;
139 Ok(Replacement::Some(completion))
140 } else if let Some(first) = self
141 .get_cached(parent)?
142 .iter()
143 .find(|entry| current.is_empty() || entry.starts_with(current))
144 {
145 let completion = format!("{parent}/{first}");
146 self.update_input(&completion)?;
147 Ok(Replacement::Some(completion))
148 } else {
149 Ok(Replacement::None)
150 }
151 }
152}