shulkerscript_cli/
util.rs

1use 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}