add interactive mode for init command
This commit is contained in:
parent
e8ac54f231
commit
7da26c9590
|
@ -24,7 +24,7 @@ watch = ["dep:notify-debouncer-mini", "dep:ctrlc"]
|
||||||
zip = ["shulkerbox/zip"]
|
zip = ["shulkerbox/zip"]
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
clap = { version = "4.5.4", features = ["derive", "env"] }
|
clap = { version = "4.5.4", features = ["derive", "env", "deprecated"] }
|
||||||
colored = "2.1.0"
|
colored = "2.1.0"
|
||||||
serde = { version = "1.0.197", features = ["derive"] }
|
serde = { version = "1.0.197", features = ["derive"] }
|
||||||
thiserror = "1.0.58"
|
thiserror = "1.0.58"
|
||||||
|
@ -39,3 +39,6 @@ notify-debouncer-mini = { version = "0.4.1", default-features = false, optional
|
||||||
ctrlc = { version = "3.4.4", optional = true }
|
ctrlc = { version = "3.4.4", optional = true }
|
||||||
tracing = "0.1.40"
|
tracing = "0.1.40"
|
||||||
tracing-subscriber = "0.3.18"
|
tracing-subscriber = "0.3.18"
|
||||||
|
# waiting for pull request to be merged
|
||||||
|
inquire = { git = "https://github.com/moritz-hoelting/rust-inquire.git", branch = "main", package = "inquire" }
|
||||||
|
camino = "1.1.7"
|
||||||
|
|
|
@ -18,8 +18,10 @@ Where [PATH] is the path of the folder to initialize in [default: `.`]
|
||||||
Options:
|
Options:
|
||||||
- `--name <NAME>` The name of the project
|
- `--name <NAME>` The name of the project
|
||||||
- `--description <DESCRIPTION>` The description of the project
|
- `--description <DESCRIPTION>` The description of the project
|
||||||
- `--pack-format <FORMAT>` The pack format version
|
- `--pack-format <FORMAT>` The pack format version
|
||||||
|
- `--icon <PATH>` The path to the icon file, leave empty for default icon
|
||||||
- `--force` Force initialization even if the directory is not empty
|
- `--force` Force initialization even if the directory is not empty
|
||||||
|
- `--batch` Do not prompt for input, use default values instead if possible or fail
|
||||||
|
|
||||||
### Build a project
|
### Build a project
|
||||||
```bash
|
```bash
|
||||||
|
|
|
@ -12,30 +12,28 @@ pub struct ProjectConfig {
|
||||||
pub struct PackConfig {
|
pub struct PackConfig {
|
||||||
pub name: String,
|
pub name: String,
|
||||||
pub description: String,
|
pub description: String,
|
||||||
#[serde(
|
#[serde(rename = "format", alias = "pack_format")]
|
||||||
rename = "format",
|
|
||||||
alias = "pack_format",
|
|
||||||
default = "default_pack_format"
|
|
||||||
)]
|
|
||||||
pub pack_format: u8,
|
pub pack_format: u8,
|
||||||
pub version: String,
|
pub version: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl PackConfig {
|
||||||
|
pub const DEFAULT_NAME: &'static str = "shulkerscript-pack";
|
||||||
|
pub const DEFAULT_DESCRIPTION: &'static str = "A Minecraft datapack created with shulkerscript";
|
||||||
|
pub const DEFAULT_PACK_FORMAT: u8 = 48;
|
||||||
|
}
|
||||||
|
|
||||||
impl Default for PackConfig {
|
impl Default for PackConfig {
|
||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
Self {
|
Self {
|
||||||
name: "shulkerscript-pack".to_string(),
|
name: Self::DEFAULT_NAME.to_string(),
|
||||||
description: "A Minecraft datapack created with shulkerscript".to_string(),
|
description: Self::DEFAULT_DESCRIPTION.to_string(),
|
||||||
pack_format: 26,
|
pack_format: Self::DEFAULT_PACK_FORMAT,
|
||||||
version: "0.1.0".to_string(),
|
version: "0.1.0".to_string(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn default_pack_format() -> u8 {
|
|
||||||
26
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
pub struct CompilerConfig {
|
pub struct CompilerConfig {
|
||||||
/// The path of a folder which files and subfolders will be copied to the root of the datapack.
|
/// The path of a folder which files and subfolders will be copied to the root of the datapack.
|
||||||
|
|
|
@ -3,6 +3,7 @@ mod config;
|
||||||
mod error;
|
mod error;
|
||||||
mod subcommands;
|
mod subcommands;
|
||||||
mod terminal_output;
|
mod terminal_output;
|
||||||
|
mod util;
|
||||||
|
|
||||||
use std::process::ExitCode;
|
use std::process::ExitCode;
|
||||||
|
|
||||||
|
|
|
@ -21,7 +21,7 @@ pub struct CleanArgs {
|
||||||
pub force: bool,
|
pub force: bool,
|
||||||
/// Enable verbose output.
|
/// Enable verbose output.
|
||||||
#[arg(short, long)]
|
#[arg(short, long)]
|
||||||
verbose: bool,
|
pub verbose: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn clean(args: &CleanArgs) -> Result<()> {
|
pub fn clean(args: &CleanArgs) -> Result<()> {
|
||||||
|
|
|
@ -1,4 +1,6 @@
|
||||||
use std::{
|
use std::{
|
||||||
|
borrow::Cow,
|
||||||
|
fmt::Display,
|
||||||
fs,
|
fs,
|
||||||
path::{Path, PathBuf},
|
path::{Path, PathBuf},
|
||||||
};
|
};
|
||||||
|
@ -11,7 +13,7 @@ use git2::{
|
||||||
use path_absolutize::Absolutize;
|
use path_absolutize::Absolutize;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
config::ProjectConfig,
|
config::{PackConfig, ProjectConfig},
|
||||||
error::Error,
|
error::Error,
|
||||||
terminal_output::{print_error, print_info, print_success},
|
terminal_output::{print_error, print_info, print_success},
|
||||||
};
|
};
|
||||||
|
@ -28,17 +30,26 @@ pub struct InitArgs {
|
||||||
#[arg(short, long)]
|
#[arg(short, long)]
|
||||||
pub description: Option<String>,
|
pub description: Option<String>,
|
||||||
/// The pack format version.
|
/// The pack format version.
|
||||||
#[arg(short, long, value_name = "FORMAT")]
|
#[arg(short, long, value_name = "FORMAT", visible_alias = "format")]
|
||||||
pub pack_format: Option<u8>,
|
pub pack_format: Option<u8>,
|
||||||
|
/// The path of the icon file.
|
||||||
|
#[arg(short, long = "icon", value_name = "PATH")]
|
||||||
|
pub icon_path: Option<PathBuf>,
|
||||||
/// Force initialization even if the directory is not empty.
|
/// Force initialization even if the directory is not empty.
|
||||||
#[arg(short, long)]
|
#[arg(short, long)]
|
||||||
pub force: bool,
|
pub force: bool,
|
||||||
/// The version control system to initialize.
|
/// The version control system to initialize. [default: git]
|
||||||
#[arg(long, default_value = "git")]
|
#[arg(long)]
|
||||||
pub vcs: VersionControlSystem,
|
pub vcs: Option<VersionControlSystem>,
|
||||||
/// Enable verbose output.
|
/// Enable verbose output.
|
||||||
#[arg(short, long)]
|
#[arg(short, long)]
|
||||||
verbose: bool,
|
pub verbose: bool,
|
||||||
|
/// Enable batch mode.
|
||||||
|
///
|
||||||
|
/// In batch mode, the command will not prompt the user for input and
|
||||||
|
/// will use the default values instead if possible or fail.
|
||||||
|
#[arg(long)]
|
||||||
|
pub batch: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy, Default, ValueEnum)]
|
#[derive(Debug, Clone, Copy, Default, ValueEnum)]
|
||||||
|
@ -48,54 +59,258 @@ pub enum VersionControlSystem {
|
||||||
None,
|
None,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl Display for VersionControlSystem {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
match self {
|
||||||
|
VersionControlSystem::Git => write!(f, "git"),
|
||||||
|
VersionControlSystem::None => write!(f, "none"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub fn init(args: &InitArgs) -> Result<()> {
|
pub fn init(args: &InitArgs) -> Result<()> {
|
||||||
|
if args.batch {
|
||||||
|
initialize_batch(args)
|
||||||
|
} else {
|
||||||
|
initialize_interactive(args)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn initialize_batch(args: &InitArgs) -> Result<()> {
|
||||||
let verbose = args.verbose;
|
let verbose = args.verbose;
|
||||||
|
let force = args.force;
|
||||||
let path = args.path.as_path();
|
let path = args.path.as_path();
|
||||||
let description = args.description.as_deref();
|
let description = args.description.as_deref();
|
||||||
let pack_format = args.pack_format;
|
let pack_format = args.pack_format;
|
||||||
let force = args.force;
|
let vcs = args.vcs.unwrap_or(VersionControlSystem::Git);
|
||||||
|
|
||||||
if !path.exists() {
|
if !path.exists() {
|
||||||
print_error("The specified path does not exist.");
|
if force {
|
||||||
Err(Error::PathNotFoundError(path.to_path_buf()))?
|
fs::create_dir_all(path)?;
|
||||||
|
} else {
|
||||||
|
print_error("The specified path does not exist.");
|
||||||
|
Err(Error::PathNotFoundError(path.to_path_buf()))?;
|
||||||
|
}
|
||||||
|
} else if !path.is_dir() {
|
||||||
|
print_error("The specified path is not a directory.");
|
||||||
|
Err(Error::NotDirectoryError(path.to_path_buf()))?;
|
||||||
|
} else if !force && path.read_dir()?.next().is_some() {
|
||||||
|
print_error("The specified directory is not empty.");
|
||||||
|
Err(Error::NonEmptyDirectoryError(path.to_path_buf()))?;
|
||||||
|
}
|
||||||
|
|
||||||
|
let name = args
|
||||||
|
.name
|
||||||
|
.as_deref()
|
||||||
|
.or_else(|| path.file_name().and_then(|os| os.to_str()));
|
||||||
|
|
||||||
|
print_info("Initializing a new Shulkerscript project in batch mode...");
|
||||||
|
|
||||||
|
// Create the pack.toml file
|
||||||
|
create_pack_config(verbose, path, name, description, pack_format)?;
|
||||||
|
|
||||||
|
// Create the pack.png file
|
||||||
|
create_pack_png(path, args.icon_path.as_deref(), verbose)?;
|
||||||
|
|
||||||
|
// Create the src directory
|
||||||
|
let src_path = path.join("src");
|
||||||
|
create_dir(&src_path, verbose)?;
|
||||||
|
|
||||||
|
// Create the main.shu file
|
||||||
|
create_main_file(
|
||||||
|
path,
|
||||||
|
&name_to_namespace(name.unwrap_or(PackConfig::DEFAULT_NAME)),
|
||||||
|
verbose,
|
||||||
|
)?;
|
||||||
|
|
||||||
|
// Initialize the version control system
|
||||||
|
initalize_vcs(path, vcs, verbose)?;
|
||||||
|
|
||||||
|
print_success("Project initialized successfully.");
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn initialize_interactive(args: &InitArgs) -> Result<()> {
|
||||||
|
const ABORT_MSG: &str = "Project initialization interrupted. Aborting...";
|
||||||
|
|
||||||
|
let verbose = args.verbose;
|
||||||
|
let force = args.force;
|
||||||
|
let path = args.path.as_path();
|
||||||
|
let description = args.description.as_deref();
|
||||||
|
let pack_format = args.pack_format;
|
||||||
|
|
||||||
|
if !path.exists() {
|
||||||
|
if force {
|
||||||
|
fs::create_dir_all(path)?;
|
||||||
|
} else if let Ok(true) =
|
||||||
|
inquire::Confirm::new("The specified path does not exist. Do you want to create it?")
|
||||||
|
.with_default(true)
|
||||||
|
.prompt()
|
||||||
|
{
|
||||||
|
fs::create_dir_all(path)?;
|
||||||
|
} else {
|
||||||
|
print_info(ABORT_MSG);
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
} else if !path.is_dir() {
|
} else if !path.is_dir() {
|
||||||
print_error("The specified path is not a directory.");
|
print_error("The specified path is not a directory.");
|
||||||
Err(Error::NotDirectoryError(path.to_path_buf()))?
|
Err(Error::NotDirectoryError(path.to_path_buf()))?
|
||||||
} else if !force && path.read_dir()?.next().is_some() {
|
} else if !force && path.read_dir()?.next().is_some() {
|
||||||
print_error("The specified directory is not empty.");
|
if let Ok(false) =
|
||||||
Err(Error::NonEmptyDirectoryError(path.to_path_buf()))?
|
inquire::Confirm::new("The specified directory is not empty. Do you want to continue?")
|
||||||
} else {
|
.with_default(false)
|
||||||
let name = args
|
.with_help_message("This may overwrite existing files in the directory.")
|
||||||
.name
|
.prompt()
|
||||||
.as_deref()
|
{
|
||||||
.or_else(|| path.file_name().and_then(|os| os.to_str()));
|
print_info(ABORT_MSG);
|
||||||
|
return Ok(());
|
||||||
print_info("Initializing a new Shulkerscript project...");
|
}
|
||||||
|
|
||||||
// Create the pack.toml file
|
|
||||||
create_pack_config(verbose, path, name, description, pack_format)?;
|
|
||||||
|
|
||||||
// Create the pack.png file
|
|
||||||
create_pack_png(path, verbose)?;
|
|
||||||
|
|
||||||
// Create the src directory
|
|
||||||
let src_path = path.join("src");
|
|
||||||
create_dir(&src_path, verbose)?;
|
|
||||||
|
|
||||||
// Create the main.shu file
|
|
||||||
create_main_file(
|
|
||||||
path,
|
|
||||||
&name_to_namespace(name.unwrap_or("shulkerscript-pack")),
|
|
||||||
verbose,
|
|
||||||
)?;
|
|
||||||
|
|
||||||
// Initialize the version control system
|
|
||||||
initalize_vcs(path, args.vcs, verbose)?;
|
|
||||||
|
|
||||||
print_success("Project initialized successfully.");
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let mut interrupted = false;
|
||||||
|
|
||||||
|
let name = args.name.as_deref().map(Cow::Borrowed).or_else(|| {
|
||||||
|
let default = path
|
||||||
|
.file_name()
|
||||||
|
.and_then(|os| os.to_str())
|
||||||
|
.unwrap_or(PackConfig::DEFAULT_NAME);
|
||||||
|
|
||||||
|
match inquire::Text::new("Enter the name of the project:")
|
||||||
|
.with_help_message("This will be the name of your datapack folder/zip file")
|
||||||
|
.with_default(default)
|
||||||
|
.prompt()
|
||||||
|
{
|
||||||
|
Ok(res) => Some(Cow::Owned(res)),
|
||||||
|
Err(_) => {
|
||||||
|
interrupted = true;
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.or_else(|| {
|
||||||
|
path.file_name()
|
||||||
|
.and_then(|os| os.to_str().map(Cow::Borrowed))
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
if interrupted {
|
||||||
|
print_info(ABORT_MSG);
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
let description = description.map(Cow::Borrowed).or_else(|| {
|
||||||
|
match inquire::Text::new("Enter the description of the project:")
|
||||||
|
.with_help_message("This will be the description of your datapack, visible in the datapack selection screen")
|
||||||
|
.with_default(PackConfig::DEFAULT_DESCRIPTION)
|
||||||
|
.prompt() {
|
||||||
|
Ok(res) => Some(Cow::Owned(res)),
|
||||||
|
Err(_) => {
|
||||||
|
interrupted = true;
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if interrupted {
|
||||||
|
print_info(ABORT_MSG);
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
let pack_format = pack_format.or_else(|| {
|
||||||
|
match inquire::Text::new("Enter the pack format:")
|
||||||
|
.with_help_message("This will determine the Minecraft version compatible with your pack, find more on the Minecraft wiki")
|
||||||
|
.with_default(PackConfig::DEFAULT_PACK_FORMAT.to_string().as_str())
|
||||||
|
.prompt() {
|
||||||
|
Ok(res) => res.parse().ok(),
|
||||||
|
Err(_) => {
|
||||||
|
interrupted = true;
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if interrupted {
|
||||||
|
print_info(ABORT_MSG);
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
let vcs = args.vcs.unwrap_or_else(|| {
|
||||||
|
match inquire::Select::new(
|
||||||
|
"Select the version control system:",
|
||||||
|
vec![VersionControlSystem::Git, VersionControlSystem::None],
|
||||||
|
)
|
||||||
|
.with_help_message("This will initialize a version control system")
|
||||||
|
.prompt()
|
||||||
|
{
|
||||||
|
Ok(res) => res,
|
||||||
|
Err(_) => {
|
||||||
|
interrupted = true;
|
||||||
|
VersionControlSystem::Git
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if interrupted {
|
||||||
|
print_info(ABORT_MSG);
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
let icon_path = args.icon_path.as_deref().map(Cow::Borrowed).or_else(|| {
|
||||||
|
let autocompleter = crate::util::PathAutocomplete::new();
|
||||||
|
match inquire::Text::new("Enter the path of the icon file:")
|
||||||
|
.with_help_message(
|
||||||
|
"This will be the icon of your datapack, visible in the datapack selection screen [use \"-\" for default]",
|
||||||
|
)
|
||||||
|
.with_autocomplete(autocompleter)
|
||||||
|
.with_default("-")
|
||||||
|
// .with_autocomplete()
|
||||||
|
.prompt()
|
||||||
|
{
|
||||||
|
Ok(res) if &res == "-" => None,
|
||||||
|
Ok(res) => Some(Cow::Owned(PathBuf::from(res))),
|
||||||
|
Err(_) => {
|
||||||
|
interrupted = true;
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if interrupted {
|
||||||
|
print_info(ABORT_MSG);
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
print_info("Initializing a new Shulkerscript project...");
|
||||||
|
|
||||||
|
// Create the pack.toml file
|
||||||
|
create_pack_config(
|
||||||
|
verbose,
|
||||||
|
path,
|
||||||
|
name.as_deref(),
|
||||||
|
description.as_deref(),
|
||||||
|
pack_format,
|
||||||
|
)?;
|
||||||
|
|
||||||
|
// Create the pack.png file
|
||||||
|
create_pack_png(path, icon_path.as_deref(), verbose)?;
|
||||||
|
|
||||||
|
// Create the src directory
|
||||||
|
let src_path = path.join("src");
|
||||||
|
create_dir(&src_path, verbose)?;
|
||||||
|
|
||||||
|
// Create the main.shu file
|
||||||
|
create_main_file(
|
||||||
|
path,
|
||||||
|
&name_to_namespace(&name.unwrap_or(Cow::Borrowed("shulkerscript-pack"))),
|
||||||
|
verbose,
|
||||||
|
)?;
|
||||||
|
|
||||||
|
// Initialize the version control system
|
||||||
|
initalize_vcs(path, vcs, verbose)?;
|
||||||
|
|
||||||
|
print_success("Project initialized successfully.");
|
||||||
|
|
||||||
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn create_pack_config(
|
fn create_pack_config(
|
||||||
|
@ -155,14 +370,29 @@ fn create_gitignore(path: &Path, verbose: bool) -> std::io::Result<()> {
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn create_pack_png(path: &Path, verbose: bool) -> std::io::Result<()> {
|
fn create_pack_png(
|
||||||
let pack_png = path.join("pack.png");
|
project_path: &Path,
|
||||||
fs::write(&pack_png, include_bytes!("../../assets/default-icon.png"))?;
|
icon_path: Option<&Path>,
|
||||||
if verbose {
|
verbose: bool,
|
||||||
print_info(format!(
|
) -> std::io::Result<()> {
|
||||||
"Created pack.png file at {}.",
|
let pack_png = project_path.join("pack.png");
|
||||||
pack_png.absolutize()?.display()
|
if let Some(icon_path) = icon_path {
|
||||||
));
|
fs::copy(icon_path, &pack_png)?;
|
||||||
|
if verbose {
|
||||||
|
print_info(format!(
|
||||||
|
"Copied pack.png file from {} to {}.",
|
||||||
|
icon_path.absolutize()?.display(),
|
||||||
|
pack_png.absolutize()?.display()
|
||||||
|
));
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
fs::write(&pack_png, include_bytes!("../../assets/default-icon.png"))?;
|
||||||
|
if verbose {
|
||||||
|
print_info(format!(
|
||||||
|
"Created pack.png file at {}.",
|
||||||
|
pack_png.absolutize()?.display()
|
||||||
|
));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,117 @@
|
||||||
|
use std::collections::HashMap;
|
||||||
|
|
||||||
|
use camino::Utf8PathBuf;
|
||||||
|
|
||||||
|
use inquire::{autocompletion::Replacement, Autocomplete};
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Default, PartialEq, Eq)]
|
||||||
|
pub struct PathAutocomplete {
|
||||||
|
parent: String,
|
||||||
|
current: String,
|
||||||
|
outputs: Vec<String>,
|
||||||
|
|
||||||
|
cache: HashMap<String, Vec<String>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PathAutocomplete {
|
||||||
|
pub fn new() -> Self {
|
||||||
|
Self::default()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn split_input(input: &str) -> (&str, &str) {
|
||||||
|
let (parent, current) = if input.ends_with('/') {
|
||||||
|
(input.trim_end_matches('/'), "")
|
||||||
|
} else if let Some((parent, current)) = input.rsplit_once('/') {
|
||||||
|
if parent.is_empty() {
|
||||||
|
("/", current)
|
||||||
|
} else {
|
||||||
|
(parent, current)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
("", input)
|
||||||
|
};
|
||||||
|
let parent = if parent.is_empty() { "." } else { parent };
|
||||||
|
|
||||||
|
(parent, current)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_cached(&mut self, parent: &str) -> Result<&[String], &'static str> {
|
||||||
|
if !self.cache.contains_key(parent) {
|
||||||
|
tracing::trace!("Cache miss for \"{}\", reading dir", parent);
|
||||||
|
|
||||||
|
let parent_path = Utf8PathBuf::from(parent);
|
||||||
|
if !parent_path.exists() || !parent_path.is_dir() {
|
||||||
|
return Err("Path does not exist");
|
||||||
|
}
|
||||||
|
|
||||||
|
let entries = parent_path
|
||||||
|
.read_dir_utf8()
|
||||||
|
.map_err(|_| "Could not read dir")?
|
||||||
|
.filter_map(|entry| {
|
||||||
|
entry.ok().map(|entry| {
|
||||||
|
entry.file_name().to_string() + if entry.path().is_dir() { "/" } else { "" }
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
|
||||||
|
self.cache.insert(parent.to_string(), entries);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(self
|
||||||
|
.cache
|
||||||
|
.get(parent)
|
||||||
|
.expect("Previous caching did not work"))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn update_input(&mut self, input: &str) -> Result<(), inquire::CustomUserError> {
|
||||||
|
let (parent, current) = Self::split_input(input);
|
||||||
|
|
||||||
|
if self.parent == parent && self.current == current {
|
||||||
|
Ok(())
|
||||||
|
} else {
|
||||||
|
self.parent = parent.to_string();
|
||||||
|
self.current = current.to_string();
|
||||||
|
|
||||||
|
self.outputs = self
|
||||||
|
.get_cached(parent)?
|
||||||
|
.iter()
|
||||||
|
.filter(|entry| entry.starts_with(current))
|
||||||
|
.cloned()
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Autocomplete for PathAutocomplete {
|
||||||
|
fn get_suggestions(&mut self, input: &str) -> Result<Vec<String>, inquire::CustomUserError> {
|
||||||
|
self.update_input(input)?;
|
||||||
|
|
||||||
|
Ok(self.outputs.clone())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_completion(
|
||||||
|
&mut self,
|
||||||
|
input: &str,
|
||||||
|
highlighted_suggestion: Option<String>,
|
||||||
|
) -> Result<inquire::autocompletion::Replacement, inquire::CustomUserError> {
|
||||||
|
let (parent, current) = Self::split_input(input);
|
||||||
|
|
||||||
|
if let Some(highlighted) = highlighted_suggestion {
|
||||||
|
let completion = format!("{parent}/{highlighted}");
|
||||||
|
self.update_input(&completion)?;
|
||||||
|
Ok(Replacement::Some(completion))
|
||||||
|
} else if let Some(first) = self
|
||||||
|
.get_cached(parent)?
|
||||||
|
.iter()
|
||||||
|
.find(|entry| entry.starts_with(current))
|
||||||
|
{
|
||||||
|
let completion = format!("{parent}/{first}");
|
||||||
|
self.update_input(&completion)?;
|
||||||
|
Ok(Replacement::Some(completion))
|
||||||
|
} else {
|
||||||
|
Ok(Replacement::None)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue