aboutsummaryrefslogtreecommitdiff
path: root/mling
diff options
context:
space:
mode:
author魏曹先生 <1992414357@qq.com>2026-06-07 02:25:27 +0800
committer魏曹先生 <1992414357@qq.com>2026-06-07 02:25:27 +0800
commit81528b273c18693ebd3f05c6f8057ff8e632f4a0 (patch)
tree85026c27535337c0123d4650c844ae364bc9780a /mling
parente41e8bda221b44d09d7e93ffc43675147ab60a6d (diff)
Refactor mling to use new program architecture and install scripts
Diffstat (limited to 'mling')
-rw-r--r--mling/.gitignore2
-rw-r--r--mling/Cargo.toml10
-rw-r--r--mling/build.rs62
-rw-r--r--mling/res/help-mling.txt15
-rw-r--r--mling/res/version.txt20
-rw-r--r--mling/src/bin/mling.rs86
-rw-r--r--mling/src/cargo_style.rs161
-rw-r--r--mling/src/cli.rs77
-rw-r--r--mling/src/cli/install.rs32
-rw-r--r--mling/src/cli/list.rs123
-rw-r--r--mling/src/cli/namespace_mgr.rs128
-rw-r--r--mling/src/cli/read.rs78
-rw-r--r--mling/src/display.rs32
-rw-r--r--mling/src/helps/mling_help.txt14
-rw-r--r--mling/src/lib.rs12
-rw-r--r--mling/src/main.rs15
-rw-r--r--mling/src/namespace_manager.rs125
-rw-r--r--mling/src/proj_mgr/mod.rs3
-rw-r--r--mling/src/project_installer.rs162
-rw-r--r--mling/src/project_solver.rs113
-rw-r--r--mling/src/res/current_dir.rs6
-rw-r--r--mling/src/res/mod.rs2
-rw-r--r--mling/tmpl/load.fish75
-rw-r--r--mling/tmpl/load.ps190
-rw-r--r--mling/tmpl/load.sh100
25 files changed, 373 insertions, 1170 deletions
diff --git a/mling/.gitignore b/mling/.gitignore
new file mode 100644
index 0000000..b124b7c
--- /dev/null
+++ b/mling/.gitignore
@@ -0,0 +1,2 @@
+debug.rs
+version.txt
diff --git a/mling/Cargo.toml b/mling/Cargo.toml
index df9c675..a08c8d5 100644
--- a/mling/Cargo.toml
+++ b/mling/Cargo.toml
@@ -11,8 +11,12 @@ keywords = ["cli", "scaffolding", "command-line", "framework"]
categories = ["command-line-interface"]
[[bin]]
+name = "cargo-mling"
+path = "src/bin/mling.rs"
+
+[[bin]]
name = "mling"
-path = "src/main.rs"
+path = "src/bin/mling.rs"
[dependencies]
mingling = { path = "../mingling", features = [
@@ -21,6 +25,7 @@ mingling = { path = "../mingling", features = [
"comp",
"general_renderer",
"extra_macros",
+ "dispatch_tree",
] }
serde = { version = "1", features = ["derive"] }
@@ -32,4 +37,5 @@ just_fmt = "0.1.2"
toml.workspace = true
[build-dependencies]
-mingling = { path = "../mingling", features = ["comp"] }
+chrono = "0.4"
+mingling = { path = "../mingling", features = ["comp", "builds"] }
diff --git a/mling/build.rs b/mling/build.rs
new file mode 100644
index 0000000..abbcc50
--- /dev/null
+++ b/mling/build.rs
@@ -0,0 +1,62 @@
+use std::path::Path;
+use std::process::Command;
+
+use mingling::build::build_comp_scripts;
+
+fn main() {
+ build_version_info();
+ build_completion();
+}
+
+fn build_version_info() {
+ // Read version from CARGO_PKG_VERSION (inherited from workspace Cargo.toml)
+ let version = env!("CARGO_PKG_VERSION");
+
+ // Get git commit hash (first 9 characters)
+ let commit_hash = Command::new("git")
+ .args(["rev-parse", "--short=9", "HEAD"])
+ .output()
+ .ok()
+ .and_then(|output| {
+ if output.status.success() {
+ String::from_utf8(output.stdout).ok()
+ } else {
+ None
+ }
+ })
+ .map(|s| s.trim().to_string())
+ .unwrap_or_else(|| "unknown".to_string());
+
+ // Get date from git commit, fallback to current date
+ let date = Command::new("git")
+ .args(["log", "-1", "--format=%ad", "--date=format:%Y-%-m-%-d"])
+ .output()
+ .ok()
+ .and_then(|output| {
+ if output.status.success() {
+ String::from_utf8(output.stdout).ok()
+ } else {
+ None
+ }
+ })
+ .map(|s| s.trim().to_string())
+ .unwrap_or_else(|| chrono::Local::now().format("%Y-%-m-%-d").to_string());
+
+ let version_string = format!("mling {version} ({commit_hash} {date})");
+
+ let out_dir = Path::new(&std::env::var("CARGO_MANIFEST_DIR").unwrap())
+ .join("src")
+ .join("helps")
+ .join("version.txt");
+
+ std::fs::write(&out_dir, version_string).expect("failed to write version.txt");
+
+ println!("cargo::rerun-if-changed=../Cargo.toml");
+ println!("cargo::rerun-if-changed=../Cargo.lock");
+ println!("cargo::rerun-if-changed=.git/HEAD");
+}
+
+fn build_completion() {
+ build_comp_scripts("cargo-mling").unwrap();
+ build_comp_scripts("mling").unwrap();
+}
diff --git a/mling/res/help-mling.txt b/mling/res/help-mling.txt
deleted file mode 100644
index 4d6fa02..0000000
--- a/mling/res/help-mling.txt
+++ /dev/null
@@ -1,15 +0,0 @@
-**Usage**: mling [COMMAND]
-
-**COMMANDS**
-__ *install* Install programs in a Cargo workspace; **[--clean | -c]**
-__ *rm-namespace* Delete a namespace
-__ *ls-namespace* List namespaces
-__ *set-trust* Set trust for a namespace **[--trusted | -t <yes/no>]**
-__ *trust* Set a namespace as trusted
-__ *untrust* Set a namespace as untrusted
-__ *show-binaries* List binary programs in the current Cargo workspace
-__ *show-target-dir* Show the output directory of the current Cargo workspace
-__ *show-workspace-root* Show the root location of the current Cargo workspace
-
-**OPTIONS**
-__ [[b_cyan]]-h, --help[[/]] Print this help message
diff --git a/mling/res/version.txt b/mling/res/version.txt
deleted file mode 100644
index 4b6c716..0000000
--- a/mling/res/version.txt
+++ /dev/null
@@ -1,20 +0,0 @@
- █
- ███
- ██▒██ ┌────────────────────────────────────┐
- ██▒▒▒██ │ Mingling │
- ██▒ ▒██ │ USE: │
- ███▒ ▒███ │ Run command in your project │
- ███▒▒█████▒▒███ │ > cargo add mingling │
- █████▒ ████ ▒█████ │ ^^^^^^^^ │
- ██████▒▒ █▒ ██ ▒█ ▒███████ │ Or add this to your Cargo.toml │
- █████▒▒▒▒█▒ █▒ ██ ▒█ ▒██▒▒▒▒█████ │ > mingling = "0.1.9" │
- ▒▒▒▒▒ ██ ██ ▒██ ▒▒▒▒▒ └────────────────────────────────────┘
- ▒▒███████████████ ┌────────────────────────────────────┐
- ▒▒▒███▒▒▒▒▒▒▒▒ │ INSTALL TOOLS: │
- ████ │ Run command to install │
- ███▒▒ │ > cargo install mingling-cli │
- ████▒ │ ^^^^^^^^^^^^ │
- ▒▒▒█████ │ Then run command to use │
- ▒▒▒▒▒████ │ > mling --help │
- ▒▒▒█████ └────────────────────────────────────┘
- ▒▒▒▒▒
diff --git a/mling/src/bin/mling.rs b/mling/src/bin/mling.rs
new file mode 100644
index 0000000..587aeb9
--- /dev/null
+++ b/mling/src/bin/mling.rs
@@ -0,0 +1,86 @@
+use std::{env::current_dir, process::exit};
+
+use colored::Colorize;
+use mingling::{
+ Program,
+ hook::ProgramHook,
+ macros::program_setup,
+ setup::{ExitCodeSetup, GeneralRendererSetup, HelpFlagSetup, QuietFlagSetup},
+};
+use mingling_cli::{ThisProgram, display::markdown, res::ResCurrentDir};
+
+fn main() {
+ #[cfg(windows)]
+ colored::control::set_virtual_terminal(true).unwrap();
+
+ // Preprocess args to handle cargo-mling invocations
+ let mut args: Vec<String> = std::env::args().collect();
+ if args.first().map_or(false, |a| a.contains("cargo-mling")) {
+ args[0] = "cargo-mling".to_string();
+ }
+ if args.get(1).map_or(false, |a| a == "mling") {
+ args.remove(1);
+ }
+
+ // Build program with preprocessed args
+ let mut program = Program::<ThisProgram>::new_with_args(args);
+
+ // Intercept Version
+ program.global_flag(["-V", "--version"], |_| {
+ eprintln!(include_str!("../helps/version.txt"));
+ exit(0)
+ });
+
+ // Intercept Help
+ program.with_hook(ProgramHook::empty().on_post_dispatch(|c| match c {
+ // When dispatcher is not found
+ ThisProgram::ErrorDispatcherNotFound => {
+ // And user requests Help
+ if ThisProgram::this().user_context.help {
+ // Print help
+ eprintln!("{}", markdown(include_str!("../helps/mling_help.txt")));
+ exit(0)
+ }
+ }
+ _ => {}
+ }));
+
+ // Setups
+ program.with_setup(HelpFlagSetup::new(["-h", "--help"]));
+ program.with_setup(GeneralRendererSetup);
+ program.with_setup(StandardOutputSetup);
+ program.with_setup(ExitCodeSetup::default());
+
+ // Resources
+ program.with_resource(ResCurrentDir {
+ path: current_dir().unwrap(),
+ });
+
+ // Execute
+ let quiet = program.stdout_setting.quiet;
+ let error_output = program.stdout_setting.error_output && !quiet;
+ let render_output = program.stdout_setting.render_output && !quiet;
+ let result = program.exec_without_render().unwrap();
+ if !result.is_empty() {
+ if result.exit_code == 0 && render_output {
+ println!("{}", result.trim());
+ } else if error_output {
+ eprintln!("{}", result.trim().bright_red());
+ }
+ }
+ exit(result.exit_code);
+}
+
+#[program_setup]
+fn standard_output_setup(program: &mut Program<ThisProgram>) {
+ program.with_setup(QuietFlagSetup::new("--silence"));
+ program.global_flag(["--no-error"], |program| {
+ program.stdout_setting.error_output = false;
+ });
+ program.global_flag(["--no-result"], |program| {
+ program.stdout_setting.render_output = false;
+ });
+ program.global_flag(["--silence", "--quiet"], |program| {
+ program.stdout_setting.quiet = true;
+ });
+}
diff --git a/mling/src/cargo_style.rs b/mling/src/cargo_style.rs
new file mode 100644
index 0000000..7663985
--- /dev/null
+++ b/mling/src/cargo_style.rs
@@ -0,0 +1,161 @@
+use colored::Colorize;
+
+/// Formats a message in cargo-style format with a bold green prefix.
+///
+/// The message should be in the format `prefix: content`. The prefix will be
+/// bold green and right-padded to 12 characters. If there is no colon in the
+/// string, the entire string is printed as content with an empty prefix.
+///
+/// # Macros
+///
+/// - `format_cargo!("prefix: {}", arg)` — format-style invocation
+/// - `format_cargo!(expr)` — direct expression invocation
+///
+/// # Panics
+///
+/// Panics if the prefix (text before the first `:`) exceeds 12 characters.
+///
+/// # Examples
+///
+/// ```
+/// format_cargo!("Compiling: hello.rs");
+/// // Output: " Compiling hello.rs" (green bold "Compiling" padded to 12)
+/// ```
+#[macro_export]
+macro_rules! format_cargo {
+ ($fmt:literal, $($arg:tt)*) => {
+ $crate::format_cargo(format!($fmt, $($arg)*))
+ };
+ ($cmd:expr) => {
+ $crate::format_cargo($cmd)
+ };
+}
+
+/// Formats an error message in cargo-style format with a bold red "error" prefix.
+///
+/// # Macros
+///
+/// - `eformat_cargo!("prefix: {}", arg)` — format-style invocation
+/// - `eformat_cargo!(expr)` — direct expression invocation
+///
+/// # Examples
+///
+/// ```
+/// eformat_cargo!("failed to parse input");
+/// // Output: "error: failed to parse input" (red bold "error")
+/// ```
+#[macro_export]
+macro_rules! eformat_cargo {
+ ($fmt:literal, $($arg:tt)*) => {
+ $crate::eformat_cargo(format!($fmt, $($arg)*))
+ };
+ ($cmd:expr) => {
+ $crate::eformat_cargo($cmd)
+ };
+}
+
+/// Print a message in cargo-style format with a bold green prefix.
+///
+/// # Macros
+///
+/// - `println_cargo!("prefix: {}", arg)` — format-style invocation
+/// - `println_cargo!(expr)` — direct expression invocation
+///
+/// # Examples
+///
+/// ```
+/// println_cargo!("Compiling: hello.rs");
+/// ```
+#[macro_export]
+macro_rules! println_cargo {
+ ($fmt:literal, $($arg:tt)*) => {
+ println!("{}", $crate::format_cargo(format!($fmt, $($arg)*)))
+ };
+ ($cmd:expr) => {
+ println!("{}", $crate::format_cargo($cmd))
+ };
+}
+
+/// Print an error message in cargo-style format with a bold red "error" prefix.
+///
+/// # Macros
+///
+/// - `eprintln_cargo!("prefix: {}", arg)` — format-style invocation
+/// - `eprintln_cargo!(expr)` — direct expression invocation
+///
+/// # Examples
+///
+/// ```
+/// eprintln_cargo!("failed to parse input");
+/// ```
+#[macro_export]
+macro_rules! eprintln_cargo {
+ ($fmt:literal, $($arg:tt)*) => {
+ eprintln!("{}", $crate::eformat_cargo(format!($fmt, $($arg)*)))
+ };
+ ($cmd:expr) => {
+ eprintln!("{}", $crate::eformat_cargo($cmd))
+ };
+}
+
+/// Format a message in cargo style format, with bold green prefix.
+///
+/// The input string is split at the first `:`. The part before the colon becomes
+/// the prefix (bold green, right-padded to 12 characters), and the part after
+/// becomes the content.
+///
+/// If no colon is found, the entire string is treated as content and no prefix
+/// is shown.
+///
+/// # Panics
+///
+/// Panics if the prefix (text before the first `:`) exceeds 12 characters.
+///
+/// # Examples
+///
+/// ```ignore
+/// format_cargo("Compiling: my_program.rs");
+/// // returns " Compiling my_program.rs"
+/// ```
+pub fn format_cargo(str: impl Into<String>) -> String {
+ let s = str.into();
+ let (prefix, content) = if let Some(pos) = s.find(':') {
+ (
+ s[..pos].trim().to_string(),
+ s[pos + 1..].trim_start().to_string(),
+ )
+ } else {
+ (String::new(), s.trim().to_string())
+ };
+
+ assert!(
+ prefix.len() <= 12,
+ "prefix length exceeds 12: '{}' has length {}",
+ prefix,
+ prefix.len()
+ );
+
+ let padding = " ".repeat(12 - prefix.len());
+
+ format!(
+ "{}{} {}",
+ padding,
+ prefix.bold().bright_green(),
+ content.trim()
+ )
+}
+
+/// Format an error message in cargo style format, with bold red "error" prefix.
+///
+/// The input string is printed as the error content, prefixed by a bold red
+/// `error:` label.
+///
+/// # Examples
+///
+/// ```ignore
+/// eformat_cargo("something went wrong");
+/// // returns "error: something went wrong"
+/// ```
+pub fn eformat_cargo(str: impl Into<String>) -> String {
+ format!("{}: {}", "error".bold().bright_red(), str.into())
+}
diff --git a/mling/src/cli.rs b/mling/src/cli.rs
deleted file mode 100644
index b628021..0000000
--- a/mling/src/cli.rs
+++ /dev/null
@@ -1,77 +0,0 @@
-use mingling::{
- macros::{r_println, renderer},
- setup::{BasicProgramSetup, GeneralRendererSetup},
-};
-
-use crate::{CMDCompletion, ErrorDispatcherNotFound, ThisProgram, display::markdown};
-
-pub mod list;
-pub use list::*;
-
-pub mod namespace_mgr;
-pub use namespace_mgr::*;
-
-pub mod read;
-pub use read::*;
-
-pub mod install;
-pub use install::*;
-
-/// Entry point for the CLI application.
-///
-/// # Panics
-///
-/// Panics on Windows if the virtual terminal processing cannot be enabled.
-pub fn cli_entry() {
- let mut program = ThisProgram::new();
-
- // Plugins
- program.with_setup(BasicProgramSetup);
- program.with_setup(GeneralRendererSetup);
- program.with_dispatcher(CMDCompletion);
-
- if program.pick_global_flag(["-v", "--version"]) {
- println!("{}", include_str!("../res/version.txt").trim_end());
- return;
- }
-
- // Help
- if program.user_context.help {
- println!(
- "{}",
- markdown(include_str!("../res/help-mling.txt").trim_end())
- );
- return;
- }
-
- // Context query commands
- program.with_dispatcher(ListInstalledCommand);
- program.with_dispatchers((
- ReadTargetDirCommand,
- ReadWorkspaceRootCommand,
- ReadBinariesCommand,
- ));
-
- // Namespace manage commands
- program.with_dispatchers((
- TrustNamespaceCommand,
- UntrustNamespaceCommand,
- SetTrustNamespaceCommand,
- RemoveNamespaceCommand,
- ));
-
- // Install binaries command
- program.with_dispatcher(InstallCommand);
-
- // Colored Setup
- #[cfg(windows)]
- colored::control::set_virtual_terminal(true).unwrap();
-
- let _ = program.exec();
-}
-
-#[renderer]
-pub(crate) fn fallback_disp(prev: ErrorDispatcherNotFound) {
- r_println!("Error: command \"{}\" not found!", prev.join(" "));
- r_println!("Use \"mling --help\" for more information.");
-}
diff --git a/mling/src/cli/install.rs b/mling/src/cli/install.rs
deleted file mode 100644
index cca7483..0000000
--- a/mling/src/cli/install.rs
+++ /dev/null
@@ -1,32 +0,0 @@
-use mingling::{
- ShellContext, Suggest,
- macros::{chain, completion, dispatcher, pack, suggest},
- parser::Picker,
-};
-
-use crate::{Next, project_installer::install_all};
-
-dispatcher!("install", InstallCommand => InstallEntry);
-
-pack!(ResultInstallCompleted = ());
-
-#[completion(InstallEntry)]
-pub(crate) fn comp_install(ctx: &ShellContext) -> Suggest {
- if ctx.typing_argument() {
- return suggest! {
- "--clean": "Clean build artifacts before installation",
- "-c": "Clean build artifacts before installation",
- };
- }
- return suggest!();
-}
-
-#[chain]
-pub(crate) fn handle_install_entry(prev: InstallEntry) -> Next {
- let is_clean_before_build = Picker::new(prev.inner)
- .pick::<bool>(["--clean", "-c"])
- .unpack();
- let _ = install_all(is_clean_before_build);
-
- ResultInstallCompleted::new(())
-}
diff --git a/mling/src/cli/list.rs b/mling/src/cli/list.rs
deleted file mode 100644
index a2a9434..0000000
--- a/mling/src/cli/list.rs
+++ /dev/null
@@ -1,123 +0,0 @@
-use colored::Colorize;
-use mingling::{
- Groupped, RenderResult, ShellContext, Suggest,
- macros::{chain, completion, dispatcher, pack, r_println, renderer, suggest},
- parser::Picker,
-};
-use serde::Serialize;
-
-use crate::{Next, namespace_manager::list_namespaces};
-
-dispatcher!("ls-namespace", ListInstalledCommand => ListInstalledEntry);
-
-#[completion(ListInstalledEntry)]
-pub(crate) fn comp_list_installed(ctx: &ShellContext) -> Suggest {
- if ctx.typing_argument() {
- return suggest! {
- "--trusted": "Show only trusted namespaces",
- "--untrusted": "Show only untrusted namespaces",
- };
- }
- return suggest!();
-}
-
-#[derive(Debug, Serialize, Default, Groupped)]
-pub(crate) enum StateListInstalledOptions {
- #[default]
- All,
- OnlyTrusted,
- OnlyUntrusted,
-}
-
-pack!(MutexErrorListInstalled = ());
-
-#[chain]
-pub(crate) fn handle_list_installed_entry(prev: ListInstalledEntry) -> Next {
- let picker = Picker::new(prev.inner);
- let r = picker
- .pick::<bool>("--trusted")
- .pick::<bool>("--untrusted")
- .unpack();
-
- let option: StateListInstalledOptions = match r {
- // (show_trusted, show_untrusted)
- (true, false) => StateListInstalledOptions::OnlyTrusted,
- (false, true) => StateListInstalledOptions::OnlyUntrusted,
- (false, false) => StateListInstalledOptions::All,
- (true, true) => return MutexErrorListInstalled::default().to_render(),
- };
-
- option.to_chain()
-}
-
-#[renderer]
-pub(crate) fn render_list_installed_mutex_error(_prev: MutexErrorListInstalled) {
- r_println!("Error: cannot use both --trusted and --untrusted options at the same time")
-}
-
-#[derive(Debug, Groupped, Serialize)]
-pub(crate) struct ResultInstalledNamespaces {
- trusted: Vec<String>,
- untrusted: Vec<String>,
- untagged: Vec<String>,
- option: StateListInstalledOptions,
-}
-
-#[chain]
-pub(crate) fn handle_state_list_installed_option(prev: StateListInstalledOptions) -> Next {
- ResultInstalledNamespaces {
- trusted: list_namespaces(true, false, false),
- untrusted: list_namespaces(false, true, false),
- untagged: list_namespaces(false, false, true),
- option: prev,
- }
-}
-
-#[renderer]
-pub(crate) fn render_installed(prev: ResultInstalledNamespaces) {
- match prev.option {
- StateListInstalledOptions::All => {
- print_list(
- &"Trusted".bright_green().bold().to_string(),
- &prev.trusted,
- __renderer_inner_result,
- );
- print_list(
- &"Untrusted".bright_red().bold().to_string(),
- &prev.untrusted,
- __renderer_inner_result,
- );
- print_list(
- &"Untagged".bright_black().bold().to_string(),
- &prev.untagged,
- __renderer_inner_result,
- );
- }
- StateListInstalledOptions::OnlyTrusted => {
- print_list(
- &"Trusted".bright_green().bold().to_string(),
- &prev.trusted,
- __renderer_inner_result,
- );
- }
- StateListInstalledOptions::OnlyUntrusted => {
- print_list(
- &"Untrusted".bright_red().bold().to_string(),
- &prev.untrusted,
- __renderer_inner_result,
- );
- }
- }
-}
-
-fn print_list(title: &str, list: &[String], __renderer_inner_result: &mut RenderResult) {
- if list.is_empty() {
- return;
- }
-
- r_println!("{title}");
-
- for (i, namespace) in (1..).zip(list.iter()) {
- r_println!(" {}. {}", i.to_string(), namespace.bold());
- }
-}
diff --git a/mling/src/cli/namespace_mgr.rs b/mling/src/cli/namespace_mgr.rs
deleted file mode 100644
index 4ea2229..0000000
--- a/mling/src/cli/namespace_mgr.rs
+++ /dev/null
@@ -1,128 +0,0 @@
-use mingling::{
- ShellContext, Suggest, SuggestItem,
- macros::{chain, completion, dispatcher, pack, r_println, renderer, route, suggest},
- parser::{Picker, Yes},
-};
-
-use crate::{
- Next,
- namespace_manager::{list_namespaces, remove_namespace, set_namespace_trusted},
-};
-
-dispatcher!("trust", TrustNamespaceCommand => TrustNamespaceEntry);
-dispatcher!("untrust", UntrustNamespaceCommand => UntrustNamespaceEntry);
-
-dispatcher!("set-trust", SetTrustNamespaceCommand => SetTrustNamespaceEntry);
-
-dispatcher!("rm-namespace", RemoveNamespaceCommand => RemoveNamespaceEntry);
-
-pack!(ErrorNamespaceNotProvided = ());
-pack!(ResultNamespaceTrustChanged = ());
-pack!(ResultNamespaceRemoved = ());
-
-#[completion(TrustNamespaceEntry)]
-pub(crate) fn comp_trust(ctx: &ShellContext) -> Suggest {
- if ctx.previous_word == "trust" {
- return Suggest::Suggest(
- list_namespaces(false, true, true)
- .into_iter()
- .map(SuggestItem::new)
- .collect::<std::collections::BTreeSet<_>>(),
- );
- }
- return suggest!();
-}
-
-#[completion(UntrustNamespaceEntry)]
-pub(crate) fn comp_untrust(ctx: &ShellContext) -> Suggest {
- if ctx.previous_word == "untrust" {
- return Suggest::Suggest(
- list_namespaces(true, false, true)
- .into_iter()
- .map(SuggestItem::new)
- .collect::<std::collections::BTreeSet<_>>(),
- );
- }
- return suggest!();
-}
-
-#[completion(SetTrustNamespaceEntry)]
-pub(crate) fn comp_set_trust(ctx: &ShellContext) -> Suggest {
- if ctx.typing_argument() {
- return suggest!(
- "-t": "Whether to trust this namespace",
- "--trusted": "Whether to trust this namespace",
- );
- }
- if ctx.filling_argument_first(["-t", "--trusted"]) {
- return suggest!("yes", "no");
- }
- if ctx.previous_word == "set-trust" {
- return Suggest::Suggest(
- list_namespaces(true, true, true)
- .into_iter()
- .map(SuggestItem::new)
- .collect::<std::collections::BTreeSet<_>>(),
- );
- }
- return suggest!();
-}
-
-#[completion(RemoveNamespaceEntry)]
-pub(crate) fn comp_remove_namespace(ctx: &ShellContext) -> Suggest {
- if ctx.previous_word == "rm-namespace" {
- return Suggest::Suggest(
- list_namespaces(true, true, true)
- .into_iter()
- .map(SuggestItem::new)
- .collect::<std::collections::BTreeSet<_>>(),
- );
- }
- return suggest!();
-}
-
-#[chain]
-pub(crate) fn handle_set_trust(p: SetTrustNamespaceEntry) -> Next {
- let (trusted, namespace) = route!(
- Picker::new(p.inner)
- .pick::<Yes>(["-t", "--trusted"])
- .pick_or_route((), ErrorNamespaceNotProvided::default().to_render())
- .unpack()
- );
- set_namespace_trusted(namespace, trusted.is_yes());
- ResultNamespaceTrustChanged::default().to_render()
-}
-
-#[chain]
-pub(crate) fn handle_trust(p: TrustNamespaceEntry) -> Next {
- SetTrustNamespaceEntry::new({
- let mut args = p.inner.clone();
- args.extend(vec!["-t".to_string(), "yes".to_string()]);
- args
- })
-}
-
-#[chain]
-pub(crate) fn handle_untrust(p: UntrustNamespaceEntry) -> Next {
- SetTrustNamespaceEntry::new({
- let mut args = p.inner.clone();
- args.extend(vec!["-t".to_string(), "no".to_string()]);
- args
- })
-}
-
-#[chain]
-pub(crate) fn handle_remove_namespace(p: RemoveNamespaceEntry) -> Next {
- let namespace = route!(
- Picker::new(p.inner)
- .pick_or_route((), ErrorNamespaceNotProvided::default().to_render())
- .unpack()
- );
- remove_namespace(namespace);
- ResultNamespaceRemoved::default().to_render()
-}
-
-#[renderer]
-pub(crate) fn render_error_namespace_not_provided(_prev: ErrorNamespaceNotProvided) {
- r_println!("Error: no namespace was provided!")
-}
diff --git a/mling/src/cli/read.rs b/mling/src/cli/read.rs
deleted file mode 100644
index e51e78f..0000000
--- a/mling/src/cli/read.rs
+++ /dev/null
@@ -1,78 +0,0 @@
-use colored::Colorize;
-use std::path::PathBuf;
-
-use mingling::{
- Groupped,
- macros::{chain, dispatcher, pack, r_println, renderer},
-};
-use serde::Serialize;
-
-use crate::{
- Next,
- project_solver::{BinaryItem, solve_current_dir},
-};
-
-dispatcher!("show-target-dir", ReadTargetDirCommand => ReadTargetDirEntry);
-dispatcher!("show-workspace-root", ReadWorkspaceRootCommand => ReadWorkspaceRootEntry);
-dispatcher!("show-binaries", ReadBinariesCommand => ReadBinariesEntry);
-
-pack!(ResultDir = PathBuf);
-pack!(ResultTargetDirNotFound = ());
-
-#[derive(Debug, Serialize, Default, Groupped)]
-pub(crate) struct ResultBinaries {
- bin: Vec<BinaryItem>,
-}
-
-#[chain]
-#[allow(unused_variables)]
-pub(crate) fn handle_target_dir_entry(entry: ReadTargetDirEntry) -> Next {
- match solve_current_dir() {
- Ok(solved) => {
- let dir = solved.target_dir;
- ResultDir::new(dir).to_render()
- }
- Err(_) => ResultTargetDirNotFound::new(()).to_render(),
- }
-}
-
-#[chain]
-#[allow(unused_variables)]
-pub(crate) fn handle_workspace_root_entry(entry: ReadWorkspaceRootEntry) -> Next {
- match solve_current_dir() {
- Ok(solved) => {
- let dir = solved.workspace_root;
- ResultDir::new(dir).to_render()
- }
- Err(_) => ResultTargetDirNotFound::new(()).to_render(),
- }
-}
-
-#[chain]
-#[allow(unused_variables)]
-pub(crate) fn handle_binaries_entry(entry: ReadBinariesEntry) -> Next {
- match solve_current_dir() {
- Ok(solved) => {
- let binaries = solved.binaries;
- ResultBinaries { bin: binaries }.to_render()
- }
- Err(_) => ResultTargetDirNotFound::new(()).to_render(),
- }
-}
-
-#[renderer]
-pub(crate) fn render_dir(prev: ResultDir) {
- r_println!("{}", prev.inner.display())
-}
-
-#[renderer]
-pub(crate) fn render_binaries(prev: ResultBinaries) {
- for (i, item) in (1..).zip(prev.bin.iter()) {
- r_println!(
- "{}. {} ({})",
- i.to_string(),
- item.name.bold(),
- item.path.to_string_lossy().underline().bright_cyan()
- );
- }
-}
diff --git a/mling/src/display.rs b/mling/src/display.rs
index 3816d89..5635cae 100644
--- a/mling/src/display.rs
+++ b/mling/src/display.rs
@@ -356,24 +356,26 @@ fn apply_color(text: impl AsRef<str>, color_name: impl AsRef<str>) -> String {
let text = text.as_ref();
let color_name = color_name.as_ref();
match color_name {
- // Normal colors and their bright short aliases
- "black" | "b_black" => text.black().to_string(),
- "red" | "b_red" => text.red().to_string(),
- "green" | "b_green" => text.green().to_string(),
- "yellow" | "b_yellow" => text.yellow().to_string(),
- "blue" | "b_blue" => text.blue().to_string(),
- "magenta" | "b_magenta" => text.magenta().to_string(),
- "cyan" | "b_cyan" => text.cyan().to_string(),
+ // Normal colors
+ "black" => text.black().to_string(),
+ "red" => text.red().to_string(),
+ "green" => text.green().to_string(),
+ "yellow" => text.yellow().to_string(),
+ "blue" => text.blue().to_string(),
+ "magenta" => text.magenta().to_string(),
+ "cyan" => text.cyan().to_string(),
"white" | "b_white" | "bright_gray" | "bright_grey" | "b_gray" | "b_grey" => {
text.white().to_string()
}
- "bright_black" | "gray" | "grey" => text.bright_black().to_string(),
- "bright_red" => text.bright_red().to_string(),
- "bright_green" => text.bright_green().to_string(),
- "bright_yellow" => text.bright_yellow().to_string(),
- "bright_blue" => text.bright_blue().to_string(),
- "bright_magenta" => text.bright_magenta().to_string(),
- "bright_cyan" => text.bright_cyan().to_string(),
+
+ // Bright colors and their b_ short aliases
+ "bright_black" | "b_black" | "gray" | "grey" => text.bright_black().to_string(),
+ "bright_red" | "b_red" => text.bright_red().to_string(),
+ "bright_green" | "b_green" => text.bright_green().to_string(),
+ "bright_yellow" | "b_yellow" => text.bright_yellow().to_string(),
+ "bright_blue" | "b_blue" => text.bright_blue().to_string(),
+ "bright_magenta" | "b_magenta" => text.bright_magenta().to_string(),
+ "bright_cyan" | "b_cyan" => text.bright_cyan().to_string(),
"bright_white" => text.bright_white().to_string(),
// Default to white if color not recognized
diff --git a/mling/src/helps/mling_help.txt b/mling/src/helps/mling_help.txt
new file mode 100644
index 0000000..64a2f67
--- /dev/null
+++ b/mling/src/helps/mling_help.txt
@@ -0,0 +1,14 @@
+Mingling's scaffolding tool
+
+[[b_green]]**Usage:**[[/]] [[b_cyan]]**cargo mling**[[/]] [[cyan]][COMMAND] [OPTIONS]...[[/]]
+
+[[b_green]]**Options:**[[/]]
+__ [[b_cyan]]**-V**[[/]], [[b_cyan]]**--version**[[/]] Print version info and exit
+__ [[b_cyan]]**-h**[[/]], [[b_cyan]]**--help**[[/]] Print this help message
+
+__ [[b_cyan]]**--silence**[[/]], [[b_cyan]]**--quiet**[[/]] Suppress all output
+__ [[b_cyan]]**--no-error**[[/]] Suppress error output
+__ [[b_cyan]]**--no-result**[[/]] Suppress result output
+
+[[b_green]]**Commands:**[[/]]
+__ [[b_cyan]]**install**[[/]] Install
diff --git a/mling/src/lib.rs b/mling/src/lib.rs
new file mode 100644
index 0000000..2ea6305
--- /dev/null
+++ b/mling/src/lib.rs
@@ -0,0 +1,12 @@
+#![allow(unused_imports)]
+
+use mingling::macros::gen_program;
+
+pub mod cargo_style;
+pub mod display;
+pub mod res;
+
+mod proj_mgr;
+use proj_mgr::*;
+
+gen_program!();
diff --git a/mling/src/main.rs b/mling/src/main.rs
deleted file mode 100644
index 799ad4b..0000000
--- a/mling/src/main.rs
+++ /dev/null
@@ -1,15 +0,0 @@
-use mingling::macros::gen_program;
-
-pub mod cli;
-pub mod display;
-pub mod namespace_manager;
-pub mod project_installer;
-pub mod project_solver;
-
-use crate::cli::*;
-
-fn main() {
- cli_entry();
-}
-
-gen_program!();
diff --git a/mling/src/namespace_manager.rs b/mling/src/namespace_manager.rs
deleted file mode 100644
index d5176dd..0000000
--- a/mling/src/namespace_manager.rs
+++ /dev/null
@@ -1,125 +0,0 @@
-use std::path::PathBuf;
-
-use just_fmt::kebab_case;
-
-#[must_use]
-pub fn list_namespaces(
- show_trusted: bool,
- show_untrusted: bool,
- show_untagged: bool,
-) -> Vec<String> {
- let wdir = working_dir();
- if !wdir.exists() {
- return Vec::new();
- }
-
- let mut namespaces = Vec::new();
- let Ok(entries) = std::fs::read_dir(&wdir) else {
- return Vec::new();
- };
- for entry in entries {
- let Ok(entry) = entry else {
- continue;
- };
- let path = entry.path();
- if path.is_dir()
- && let Some(name) = path.file_name()
- && let Some(name_str) = name.to_str()
- {
- // Skip directories starting with a dot
- if name_str.starts_with('.') {
- continue;
- }
- let namespace = name_str.to_string();
- let is_trusted = is_trusted_namespace(namespace.clone());
- let is_untrusted = is_untrusted_namespace(namespace.clone());
- let is_untagged = is_untagged_namespace(namespace.clone());
-
- if (show_trusted && is_trusted)
- || (show_untrusted && is_untrusted)
- || (show_untagged && is_untagged)
- {
- namespaces.push(namespace);
- }
- }
- }
-
- namespaces
-}
-
-pub fn set_namespace_trusted(namespace: String, trusted: bool) {
- let ndir = namespace_dir(namespace);
- let trusted_file = ndir.join("TRUSTED");
- let untrusted_file = ndir.join("UNTRUSTED");
-
- if trusted {
- // Create TRUSTED file and remove UNTRUSTED if it exists
- let _ = std::fs::write(&trusted_file, "");
- let _ = std::fs::remove_file(&untrusted_file);
- } else {
- // Create UNTRUSTED file and remove TRUSTED if it exists
- let _ = std::fs::write(&untrusted_file, "");
- let _ = std::fs::remove_file(&trusted_file);
- }
-}
-
-pub fn remove_namespace(namespace: String) {
- let ndir = namespace_dir(namespace);
- if ndir.exists() {
- let _ = std::fs::remove_dir_all(&ndir);
- }
-}
-
-/// Returns the mingling data directory.
-///
-/// # Panics
-///
-/// Panics if the platform's data directory cannot be determined.
-#[must_use]
-pub fn working_dir() -> PathBuf {
- dirs::data_dir().unwrap().join("mingling")
-}
-
-#[must_use]
-pub fn namespace_dir(namespace: String) -> PathBuf {
- working_dir().join(kebab_case!(namespace))
-}
-
-#[must_use]
-pub fn is_untrusted_namespace(namespace: String) -> bool {
- let untrusted_file = namespace_dir(namespace).join("UNTRUSTED");
- untrusted_file.exists()
-}
-
-#[must_use]
-pub fn is_trusted_namespace(namespace: String) -> bool {
- let trusted = namespace_dir(namespace).join("TRUSTED");
- trusted.exists()
-}
-
-#[must_use]
-pub fn is_untagged_namespace(namespace: String) -> bool {
- let ndir = namespace_dir(namespace);
- let trusted = ndir.join("TRUSTED");
- let untrusted = ndir.join("UNTRUSTED");
- !trusted.exists() && !untrusted.exists()
-}
-
-#[must_use]
-pub fn bin_dir(namespace: String) -> PathBuf {
- namespace_dir(namespace).join("bin")
-}
-
-#[must_use]
-pub fn comp_dir(namespace: String) -> PathBuf {
- namespace_dir(namespace).join("comp")
-}
-
-#[must_use]
-pub fn exe_path(namespace: String, bin_name_without_ext: String) -> PathBuf {
- if cfg!(target_os = "windows") {
- bin_dir(namespace).join(bin_name_without_ext + ".exe")
- } else {
- bin_dir(namespace).join(bin_name_without_ext)
- }
-}
diff --git a/mling/src/proj_mgr/mod.rs b/mling/src/proj_mgr/mod.rs
new file mode 100644
index 0000000..86c05e3
--- /dev/null
+++ b/mling/src/proj_mgr/mod.rs
@@ -0,0 +1,3 @@
+use mingling::macros::dispatcher;
+
+dispatcher!("install");
diff --git a/mling/src/project_installer.rs b/mling/src/project_installer.rs
deleted file mode 100644
index 983307f..0000000
--- a/mling/src/project_installer.rs
+++ /dev/null
@@ -1,162 +0,0 @@
-use mingling::{ShellFlag, build::build_comp_script_to};
-
-use crate::{
- namespace_manager::{bin_dir, comp_dir, exe_path, working_dir},
- project_solver::solve,
-};
-
-const SCRIPT_LOAD_BASH: &str = include_str!("../tmpl/load.sh");
-const SCRIPT_LOAD_FISH: &str = include_str!("../tmpl/load.fish");
-const SCRIPT_LOAD_PWSH: &str = include_str!("../tmpl/load.ps1");
-
-#[derive(serde::Deserialize)]
-struct CargoToml {
- package: Package,
-}
-
-#[derive(serde::Deserialize)]
-struct Package {
- name: String,
-}
-
-/// Installs all projects and shell scripts.
-///
-/// # Errors
-///
-/// Returns an `io::Error` if the current directory cannot be determined, if the project
-/// installation fails, or if the shell scripts cannot be installed.
-pub fn install_all(clean_before_build: bool) -> Result<(), std::io::Error> {
- let current = std::env::current_dir()?;
- install_this_project(&current, clean_before_build)?;
- install_shell_scripts()?;
- Ok(())
-}
-
-/// Installs a project from the given path.
-///
-/// # Errors
-///
-/// Returns an `io::Error` if the project installation fails, e.g., if `cargo build`
-/// fails, the Cargo.toml cannot be parsed, or file operations (copy, create dir) fail.
-pub fn install_this_project(
- current: &std::path::PathBuf,
- clean_before_build: bool,
-) -> Result<(), std::io::Error> {
- // Obtain context data
- let solved = solve(current)?;
-
- let workspace_root = &solved.workspace_root;
-
- // If clean_before_build, execute cargo clean in workspace_root first
- if clean_before_build {
- let status = std::process::Command::new("cargo")
- .arg("clean")
- .current_dir(workspace_root)
- .status()?;
- if !status.success() {
- return Err(std::io::Error::other("exec `cargo clean` failed"));
- }
- }
-
- // Execute cargo build --release in workspace_root
- let status = std::process::Command::new("cargo")
- .args(["build", "--release"])
- .current_dir(workspace_root)
- .status()?;
- if !status.success() {
- return Err(std::io::Error::other("cargo build --release failed"));
- }
-
- // Parse package.name from workspace_root's Cargo.toml as namespace
- let cargo_toml_content = std::fs::read_to_string(workspace_root.join("Cargo.toml"))?;
- let cargo_toml: CargoToml = toml::from_str(&cargo_toml_content).map_err(|e| {
- std::io::Error::new(
- std::io::ErrorKind::InvalidData,
- format!("failed to parse Cargo.toml: {e}"),
- )
- })?;
- let namespace = cargo_toml.package.name;
-
- // Ensure destination directories exist
- std::fs::create_dir_all(bin_dir(namespace.clone()))?;
- std::fs::create_dir_all(comp_dir(namespace.clone()))?;
-
- // Copy binaries to corresponding exe_path
- for bin in &solved.binaries {
- let dst = exe_path(namespace.clone(), bin.name.clone());
- std::fs::copy(&bin.path, &dst)?;
- }
-
- // Copy all completion scripts containing _comp from target/release to comp_dir
- let target_dir = &solved.target_dir;
- let release_dir = target_dir.join("release");
- if release_dir.exists() {
- for entry in std::fs::read_dir(&release_dir)? {
- let entry = entry?;
- let file_name = entry.file_name();
- let file_name_str = file_name.to_string_lossy();
- if file_name_str.contains("_comp") {
- let dest = comp_dir(namespace.clone()).join(file_name.as_os_str());
- std::fs::copy(entry.path(), &dest)?;
- }
- }
- }
-
- Ok(())
-}
-
-/// Installs shell completion scripts for the `mling` command.
-///
-/// # Errors
-///
-/// Returns an `io::Error` if the shell scripts cannot be built or installed.
-pub fn install_shell_scripts() -> Result<(), std::io::Error> {
- // Get the working directory (mingling data dir)
- let wdir = working_dir();
- std::fs::create_dir_all(&wdir)?;
-
- // Build shell completion scripts for the "mling" command based on the current OS
- let mling_comp = if cfg!(target_os = "windows") {
- vec![ShellFlag::Powershell]
- } else if cfg!(target_os = "macos") || cfg!(target_os = "linux") {
- vec![ShellFlag::Bash, ShellFlag::Zsh, ShellFlag::Fish]
- } else {
- vec![ShellFlag::Bash]
- };
-
- for flag in mling_comp {
- build_comp_script_to(
- &flag,
- "mling",
- wdir.join(".comp").display().to_string().as_str(),
- )?;
- }
-
- // Determine which scripts to write based on platform
- let scripts: Vec<(&str, &str)> = if cfg!(target_os = "windows") {
- vec![("load.ps1", SCRIPT_LOAD_PWSH)]
- } else if cfg!(target_os = "macos") || cfg!(target_os = "linux") {
- vec![
- ("load.sh", SCRIPT_LOAD_BASH),
- ("load.fish", SCRIPT_LOAD_FISH),
- ]
- } else {
- // Fallback: write bash script
- vec![("load.sh", SCRIPT_LOAD_BASH)]
- };
-
- for (filename, content) in scripts {
- let dest = wdir.join(filename);
- std::fs::write(&dest, content)?;
- if cfg!(target_os = "linux") {
- let status = std::process::Command::new("chmod")
- .args(["+x", &dest.to_string_lossy()])
- .status()?;
- if !status.success() {
- eprintln!("Failed to chmod {filename}");
- }
- }
- }
-
- Ok(())
-}
diff --git a/mling/src/project_solver.rs b/mling/src/project_solver.rs
deleted file mode 100644
index 3aec2b4..0000000
--- a/mling/src/project_solver.rs
+++ /dev/null
@@ -1,113 +0,0 @@
-use std::path::PathBuf;
-
-use serde::Serialize;
-
-pub type BinaryName = String;
-pub type BinaryTargetPath = PathBuf;
-
-pub struct ProjectSolveResult {
- pub target_dir: PathBuf,
- pub workspace_root: PathBuf,
- pub binaries: Vec<BinaryItem>,
-}
-
-#[derive(Debug, Serialize)]
-pub struct BinaryItem {
- pub name: String,
- pub path: PathBuf,
-}
-
-/// Solves the current directory for project metadata.
-///
-/// # Errors
-///
-/// Returns an `io::Error` if the current directory cannot be determined
-/// or if `cargo metadata` fails.
-pub fn solve_current_dir() -> Result<ProjectSolveResult, std::io::Error> {
- let current = std::env::current_dir()?;
- solve(&current)
-}
-
-/// Solves the given directory path for project metadata.
-///
-/// # Errors
-///
-/// Returns an `io::Error` if `cargo metadata` fails for the given path.
-pub fn solve(current: &PathBuf) -> Result<ProjectSolveResult, std::io::Error> {
- let (target_dir, workspace_root, binaries) = solve_inner(current)?;
- Ok(ProjectSolveResult {
- target_dir,
- workspace_root,
- binaries,
- })
-}
-
-fn solve_inner(current: &PathBuf) -> Result<(PathBuf, PathBuf, Vec<BinaryItem>), std::io::Error> {
- let output = std::process::Command::new("cargo")
- .arg("metadata")
- .arg("--format-version")
- .arg("1")
- .current_dir(current)
- .output()?;
- if !output.status.success() {
- let stderr = String::from_utf8_lossy(&output.stderr);
- return Err(std::io::Error::other(format!(
- "cargo metadata failed: {stderr}"
- )));
- }
- let metadata: serde_json::Value = serde_json::from_slice(&output.stdout)
- .map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidData, e))?;
-
- let workspace_root_str = metadata["workspace_root"].as_str().ok_or_else(|| {
- std::io::Error::new(std::io::ErrorKind::InvalidData, "missing workspace_root")
- })?;
- let workspace_root = PathBuf::from(workspace_root_str);
-
- let target_dir_str = metadata["target_directory"].as_str().ok_or_else(|| {
- std::io::Error::new(std::io::ErrorKind::InvalidData, "missing target_directory")
- })?;
- let target_dir = PathBuf::from(target_dir_str);
-
- let packages = metadata["packages"].as_array().ok_or_else(|| {
- std::io::Error::new(std::io::ErrorKind::InvalidData, "missing packages array")
- })?;
-
- let mut binaries = Vec::new();
- let cargo_toml_path = workspace_root.join("Cargo.toml");
-
- // Find the package whose manifest_path matches workspace_root/Cargo.toml
- for pkg in packages {
- let manifest_path = pkg["manifest_path"].as_str().ok_or_else(|| {
- std::io::Error::new(std::io::ErrorKind::InvalidData, "missing manifest_path")
- })?;
- let manifest_path_buf = PathBuf::from(manifest_path);
- if manifest_path_buf == cargo_toml_path {
- // Found the workspace root package
- if let Some(targets) = pkg["targets"].as_array() {
- for target in targets {
- let kind = target["kind"].as_array();
- let is_bin = kind.is_some_and(|k| k.iter().any(|v| v.as_str() == Some("bin")));
- if is_bin {
- let name = target["name"].as_str().ok_or_else(|| {
- std::io::Error::new(
- std::io::ErrorKind::InvalidData,
- "missing target name",
- )
- })?;
- let mut binary_path = target_dir.join("release").join(name);
- if cfg!(target_os = "windows") {
- binary_path.set_extension("exe");
- }
- binaries.push(BinaryItem {
- name: name.to_string(),
- path: binary_path,
- });
- }
- }
- }
- break;
- }
- }
-
- Ok((target_dir, workspace_root, binaries))
-}
diff --git a/mling/src/res/current_dir.rs b/mling/src/res/current_dir.rs
new file mode 100644
index 0000000..b928596
--- /dev/null
+++ b/mling/src/res/current_dir.rs
@@ -0,0 +1,6 @@
+use std::path::PathBuf;
+
+#[derive(Default, Clone)]
+pub struct ResCurrentDir {
+ pub path: PathBuf,
+}
diff --git a/mling/src/res/mod.rs b/mling/src/res/mod.rs
new file mode 100644
index 0000000..b6ea60d
--- /dev/null
+++ b/mling/src/res/mod.rs
@@ -0,0 +1,2 @@
+mod current_dir;
+pub use current_dir::*;
diff --git a/mling/tmpl/load.fish b/mling/tmpl/load.fish
deleted file mode 100644
index 19e1ef7..0000000
--- a/mling/tmpl/load.fish
+++ /dev/null
@@ -1,75 +0,0 @@
-#!/usr/bin/env fish
-
-# Save original directory
-set -l _load_original_dir $PWD
-
-# Switch to script directory
-set -l _load_dir (dirname (status filename))
-cd $_load_dir
-
-# Load mling.fish from path
-source .comp/mling_comp.fish
-
-# Add all namespace bin directories to PATH
-for _dir in */bin/
- if test -d $_dir
- set -gx PATH $PWD/$_dir $PATH
- end
-end
-
-function _load_comp_script
- if string match -q '*.fish' -- $argv[1]
- source $argv[1] 2>/dev/null
- end
-end
-
-# Iterate through all namespaces
-for _namespace in */
- set _namespace (string trim -r -c / $_namespace)
-
- # Skip if UNTRUSTED marked or no comp directory
- test -f $_namespace/UNTRUSTED && continue
- test -d $_namespace/comp || continue
-
- # Find all loadable scripts in comp
- set _scripts (find $_namespace/comp -maxdepth 1 -type f \( -name '*.sh' -o -name '*.zsh' -o -name '*.fish' \) 2>/dev/null)
- test -z "$_scripts" && continue
-
- # Count scripts
- set _count (count $_scripts)
-
- # If TRUSTED marked, load directly
- if test -f $_namespace/TRUSTED
- for _script in $_scripts
- _load_comp_script $_script
- end
- continue
- end
-
- # Ask user
- read -l -p 'printf "%s has %d completion script(s) to load, do you trust it? [Y/n] " $_namespace $_count' _answer
- switch $_answer
- case '' Y y
- for _script in $_scripts
- chmod +x $_script
- end
- touch $_namespace/TRUSTED
-
- # Ask whether to load immediately
- read -l -p 'printf "Load it immediately? [Y/n] "' _load_answer
- switch $_load_answer
- case '' Y y
- for _script in $_scripts
- _load_comp_script $_script
- end
- end
- case '*'
- touch $_namespace/UNTRUSTED
- end
-end
-
-# Restore original directory
-cd $_load_original_dir
-
-# Clean up
-functions -e _load_comp_script
diff --git a/mling/tmpl/load.ps1 b/mling/tmpl/load.ps1
deleted file mode 100644
index fb616f4..0000000
--- a/mling/tmpl/load.ps1
+++ /dev/null
@@ -1,90 +0,0 @@
-#!/usr/bin/env pwsh
-
-# Save original directory, restore after execution
-$_load_original_dir = Get-Location
-
-# Resolve script directory (works with dot-source: . ./load.ps1)
-$scriptPath = Split-Path -Parent $MyInvocation.MyCommand.Path
-if (-not $scriptPath) {
- Write-Error "load.ps1: failed to resolve script directory"
- return
-}
-
-# Load completion script mling_comp.ps1 from the .comp subdirectory
-$compScript = Join-Path -Path $scriptPath -ChildPath ".comp" | Join-Path -ChildPath "mling_comp.ps1"
-if (Test-Path $compScript) {
- . $compScript
-}
-
-# Change to script directory
-try {
- Set-Location $scriptPath -ErrorAction Stop
-} catch {
- Write-Error "load.ps1: failed to cd to script directory"
- return
-}
-
-# Add bin directories from all namespaces to PATH
-$allDirs = Get-ChildItem -Directory
-Get-ChildItem -Directory | Where-Object { Test-Path (Join-Path -Path $_.FullName -ChildPath "bin") } | ForEach-Object {
- $binPath = Join-Path -Path $_.FullName -ChildPath "bin"
- $env:PATH = "$binPath;$env:PATH"
-}
-
-# Helper function: execute script with appropriate shell
-function _load_script {
- param([string]$script)
- if ($script -like "*.ps1") {
- & $script 2>$null
- }
-}
-
-# Iterate over all namespaces (top-level directories except .comp)
-$nsDirs = Get-ChildItem -Directory -Exclude ".comp"
-foreach ($_dir in $nsDirs) {
- $ns = $_dir.Name
-
- # Skip if UNTRUSTED marker exists
- $untrustedMarker = Join-Path -Path $ns -ChildPath "UNTRUSTED"
- if (Test-Path $untrustedMarker) { continue }
-
- $compDir = Join-Path -Path $ns -ChildPath "comp"
- if (-not (Test-Path $compDir -PathType Container)) { continue }
-
- # Find all loadable scripts under comp
- $scripts = Get-ChildItem -Path $compDir -Filter "*.ps1" -File -ErrorAction SilentlyContinue
- if (-not $scripts) { continue }
-
- $count = ($scripts | Measure-Object).Count
-
- # If TRUSTED marker exists, load directly
- $trustedMarker = Join-Path -Path $ns -ChildPath "TRUSTED"
- if (Test-Path $trustedMarker) {
- foreach ($_script in $scripts) { _load_script $_script.FullName }
- continue
- }
-
- # No marker, ask user
- $answer = Read-Host "'$ns' has $count completion script(s) to load, do you trust it? [Y/n] "
- if ($answer -eq "" -or $answer -match "^(y|yes)$") {
- # Mark as TRUSTED
- New-Item -ItemType File -Path $trustedMarker -Force | Out-Null
-
- # Ask whether to load immediately
- $load_answer = Read-Host "Load it immediately? [Y/n] "
- if ($load_answer -eq "" -or $load_answer -match "^(y|yes)$") {
- foreach ($_script in $scripts) { _load_script $_script.FullName }
- }
- } else {
- New-Item -ItemType File -Path $untrustedMarker -Force | Out-Null
- }
-}
-
-# Restore original working directory
-try {
- Set-Location $_load_original_dir -ErrorAction Stop
-} catch {}
-
-# Cleanup
-Remove-Variable -Name _load_original_dir -ErrorAction SilentlyContinue
-Remove-Item Function:_load_script -ErrorAction SilentlyContinue
diff --git a/mling/tmpl/load.sh b/mling/tmpl/load.sh
deleted file mode 100644
index 0d48e95..0000000
--- a/mling/tmpl/load.sh
+++ /dev/null
@@ -1,100 +0,0 @@
-#!/usr/bin/env bash
-
-# Save original directory, restore after execution
-_load_original_dir="$PWD"
-
-cd "$(dirname "$0")" 2>/dev/null || {
- echo "load.sh: failed to cd to script directory" >&2
- return 1
-}
-
-# If in zsh, source mling.zsh, otherwise source mling.sh
-if [ -n "$ZSH_VERSION" ]; then
- [ -f "./.comp/mling_comp.zsh" ] && source "./.comp/mling_comp.zsh"
-else
- [ -f "./.comp/mling_comp.sh" ] && source "./.comp/mling_comp.sh"
-fi
-
-# Add bin directories from all namespaces to PATH
-for _dir in */bin/; do
- [ -d "$_dir" ] && export PATH="$PWD/${_dir%/}:$PATH"
-done
-
-# Helper function: execute script with appropriate shell
-_load_script() {
- local script="$1"
- if [ -n "$ZSH_VERSION" ]; then
- case "$script" in
- *.zsh|*.sh)
- source "$script" 2>/dev/null
- ;;
- esac
- else
- case "$script" in
- *.sh)
- bash "$script" 2>/dev/null
- ;;
- esac
- fi
-}
-
-# Iterate over all namespaces
-for _namespace in */; do
- _namespace="${_namespace%/}"
- [ "$_namespace" = "*" ] && continue
-
- # Skip if UNTRUSTED marker exists
- [ -f "$_namespace/UNTRUSTED" ] && continue
-
- _comp_dir="$_namespace/comp"
- [ ! -d "$_comp_dir" ] && continue
-
- # Find all loadable scripts under comp
- _scripts=$(find "$_comp_dir" -maxdepth 1 -type f \( -name '*.sh' -o -name '*.zsh' -o -name '*.fish' \) 2>/dev/null)
- [ -z "$_scripts" ] && continue
-
- # Count scripts
- _count=$(echo "$_scripts" | wc -l)
-
- # If TRUSTED marker exists, load directly
- if [ -f "$_namespace/TRUSTED" ]; then
- echo "$_scripts" | while IFS= read -r _script; do
- _load_script "$_script"
- done
- continue
- fi
-
- # No marker, ask user
- printf "'%s' has %d completion script(s) to load, do you trust it? [Y/n] " "$_namespace" "$_count"
- read _answer
- case "$_answer" in
- [Yy]*|"")
- # Mark as TRUSTED and set executable permissions
- echo "$_scripts" | while IFS= read -r _script; do
- chmod +x "$_script"
- done
- touch "$_namespace/TRUSTED"
-
- # Ask whether to load immediately
- printf "Load it immediately? [Y/n] "
- read _load_answer
- case "$_load_answer" in
- [Yy]*|"")
- echo "$_scripts" | while IFS= read -r _script; do
- _load_script "$_script"
- done
- ;;
- esac
- ;;
- *)
- touch "$_namespace/UNTRUSTED"
- ;;
- esac
-done
-
-# Restore original working directory
-cd "$_load_original_dir" 2>/dev/null || true
-
-# Cleanup
-unset -f _load_script
-unset _load_original_dir _dir _namespace _comp_dir _scripts _count _answer _load_answer _script