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"]
 | 
			
		||||
 | 
			
		||||
[dependencies]
 | 
			
		||||
clap = { version = "4.5.4", features = ["derive", "env"] }
 | 
			
		||||
clap = { version = "4.5.4", features = ["derive", "env", "deprecated"] }
 | 
			
		||||
colored = "2.1.0"
 | 
			
		||||
serde = { version = "1.0.197", features = ["derive"] }
 | 
			
		||||
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 }
 | 
			
		||||
tracing = "0.1.40"
 | 
			
		||||
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"
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -19,7 +19,9 @@ Options:
 | 
			
		|||
- `--name <NAME>`                The name of the project
 | 
			
		||||
- `--description <DESCRIPTION>`  The description of the project
 | 
			
		||||
- `--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
 | 
			
		||||
- `--batch`                      Do not prompt for input, use default values instead if possible or fail
 | 
			
		||||
 | 
			
		||||
### Build a project
 | 
			
		||||
```bash
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -12,30 +12,28 @@ pub struct ProjectConfig {
 | 
			
		|||
pub struct PackConfig {
 | 
			
		||||
    pub name: String,
 | 
			
		||||
    pub description: String,
 | 
			
		||||
    #[serde(
 | 
			
		||||
        rename = "format",
 | 
			
		||||
        alias = "pack_format",
 | 
			
		||||
        default = "default_pack_format"
 | 
			
		||||
    )]
 | 
			
		||||
    #[serde(rename = "format", alias = "pack_format")]
 | 
			
		||||
    pub pack_format: u8,
 | 
			
		||||
    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 {
 | 
			
		||||
    fn default() -> Self {
 | 
			
		||||
        Self {
 | 
			
		||||
            name: "shulkerscript-pack".to_string(),
 | 
			
		||||
            description: "A Minecraft datapack created with shulkerscript".to_string(),
 | 
			
		||||
            pack_format: 26,
 | 
			
		||||
            name: Self::DEFAULT_NAME.to_string(),
 | 
			
		||||
            description: Self::DEFAULT_DESCRIPTION.to_string(),
 | 
			
		||||
            pack_format: Self::DEFAULT_PACK_FORMAT,
 | 
			
		||||
            version: "0.1.0".to_string(),
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
fn default_pack_format() -> u8 {
 | 
			
		||||
    26
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#[derive(Debug, Clone, Serialize, Deserialize)]
 | 
			
		||||
pub struct CompilerConfig {
 | 
			
		||||
    /// 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 subcommands;
 | 
			
		||||
mod terminal_output;
 | 
			
		||||
mod util;
 | 
			
		||||
 | 
			
		||||
use std::process::ExitCode;
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -21,7 +21,7 @@ pub struct CleanArgs {
 | 
			
		|||
    pub force: bool,
 | 
			
		||||
    /// Enable verbose output.
 | 
			
		||||
    #[arg(short, long)]
 | 
			
		||||
    verbose: bool,
 | 
			
		||||
    pub verbose: bool,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
pub fn clean(args: &CleanArgs) -> Result<()> {
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,4 +1,6 @@
 | 
			
		|||
use std::{
 | 
			
		||||
    borrow::Cow,
 | 
			
		||||
    fmt::Display,
 | 
			
		||||
    fs,
 | 
			
		||||
    path::{Path, PathBuf},
 | 
			
		||||
};
 | 
			
		||||
| 
						 | 
				
			
			@ -11,7 +13,7 @@ use git2::{
 | 
			
		|||
use path_absolutize::Absolutize;
 | 
			
		||||
 | 
			
		||||
use crate::{
 | 
			
		||||
    config::ProjectConfig,
 | 
			
		||||
    config::{PackConfig, ProjectConfig},
 | 
			
		||||
    error::Error,
 | 
			
		||||
    terminal_output::{print_error, print_info, print_success},
 | 
			
		||||
};
 | 
			
		||||
| 
						 | 
				
			
			@ -28,17 +30,26 @@ pub struct InitArgs {
 | 
			
		|||
    #[arg(short, long)]
 | 
			
		||||
    pub description: Option<String>,
 | 
			
		||||
    /// The pack format version.
 | 
			
		||||
    #[arg(short, long, value_name = "FORMAT")]
 | 
			
		||||
    #[arg(short, long, value_name = "FORMAT", visible_alias = "format")]
 | 
			
		||||
    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.
 | 
			
		||||
    #[arg(short, long)]
 | 
			
		||||
    pub force: bool,
 | 
			
		||||
    /// The version control system to initialize.
 | 
			
		||||
    #[arg(long, default_value = "git")]
 | 
			
		||||
    pub vcs: VersionControlSystem,
 | 
			
		||||
    /// The version control system to initialize. [default: git]
 | 
			
		||||
    #[arg(long)]
 | 
			
		||||
    pub vcs: Option<VersionControlSystem>,
 | 
			
		||||
    /// Enable verbose output.
 | 
			
		||||
    #[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)]
 | 
			
		||||
| 
						 | 
				
			
			@ -48,35 +59,58 @@ pub enum VersionControlSystem {
 | 
			
		|||
    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<()> {
 | 
			
		||||
    if args.batch {
 | 
			
		||||
        initialize_batch(args)
 | 
			
		||||
    } else {
 | 
			
		||||
        initialize_interactive(args)
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
fn initialize_batch(args: &InitArgs) -> Result<()> {
 | 
			
		||||
    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;
 | 
			
		||||
    let force = args.force;
 | 
			
		||||
    let vcs = args.vcs.unwrap_or(VersionControlSystem::Git);
 | 
			
		||||
 | 
			
		||||
    if !path.exists() {
 | 
			
		||||
        if force {
 | 
			
		||||
            fs::create_dir_all(path)?;
 | 
			
		||||
        } else {
 | 
			
		||||
            print_error("The specified path does not exist.");
 | 
			
		||||
        Err(Error::PathNotFoundError(path.to_path_buf()))?
 | 
			
		||||
            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()))?
 | 
			
		||||
        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()))?
 | 
			
		||||
    } else {
 | 
			
		||||
        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...");
 | 
			
		||||
    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, verbose)?;
 | 
			
		||||
    create_pack_png(path, args.icon_path.as_deref(), verbose)?;
 | 
			
		||||
 | 
			
		||||
    // Create the src directory
 | 
			
		||||
    let src_path = path.join("src");
 | 
			
		||||
| 
						 | 
				
			
			@ -85,17 +119,198 @@ pub fn init(args: &InitArgs) -> Result<()> {
 | 
			
		|||
    // Create the main.shu file
 | 
			
		||||
    create_main_file(
 | 
			
		||||
        path,
 | 
			
		||||
            &name_to_namespace(name.unwrap_or("shulkerscript-pack")),
 | 
			
		||||
        &name_to_namespace(name.unwrap_or(PackConfig::DEFAULT_NAME)),
 | 
			
		||||
        verbose,
 | 
			
		||||
    )?;
 | 
			
		||||
 | 
			
		||||
    // Initialize the version control system
 | 
			
		||||
        initalize_vcs(path, args.vcs, verbose)?;
 | 
			
		||||
    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() {
 | 
			
		||||
        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() {
 | 
			
		||||
        if let Ok(false) =
 | 
			
		||||
            inquire::Confirm::new("The specified directory is not empty. Do you want to continue?")
 | 
			
		||||
                .with_default(false)
 | 
			
		||||
                .with_help_message("This may overwrite existing files in the directory.")
 | 
			
		||||
                .prompt()
 | 
			
		||||
        {
 | 
			
		||||
            print_info(ABORT_MSG);
 | 
			
		||||
            return 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(
 | 
			
		||||
| 
						 | 
				
			
			@ -155,8 +370,22 @@ fn create_gitignore(path: &Path, verbose: bool) -> std::io::Result<()> {
 | 
			
		|||
    Ok(())
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
fn create_pack_png(path: &Path, verbose: bool) -> std::io::Result<()> {
 | 
			
		||||
    let pack_png = path.join("pack.png");
 | 
			
		||||
fn create_pack_png(
 | 
			
		||||
    project_path: &Path,
 | 
			
		||||
    icon_path: Option<&Path>,
 | 
			
		||||
    verbose: bool,
 | 
			
		||||
) -> std::io::Result<()> {
 | 
			
		||||
    let pack_png = project_path.join("pack.png");
 | 
			
		||||
    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!(
 | 
			
		||||
| 
						 | 
				
			
			@ -164,6 +393,7 @@ fn create_pack_png(path: &Path, verbose: bool) -> std::io::Result<()> {
 | 
			
		|||
                pack_png.absolutize()?.display()
 | 
			
		||||
            ));
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
    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