From 881e7399b2417c32fa996d94c6b389c1e06d8eb1 Mon Sep 17 00:00:00 2001 From: 魏曹先生 <1992414357@qq.com> Date: Tue, 28 Apr 2026 16:18:12 +0800 Subject: Add scaffolding CLI tool `mling` --- mling/Cargo.lock | 455 +++++++++++++++++++++++++++++++++++++++++ mling/Cargo.toml | 38 ++++ mling/src/cli.rs | 45 ++++ mling/src/cli/list.rs | 117 +++++++++++ mling/src/cli/namespace_mgr.rs | 128 ++++++++++++ mling/src/cli/read.rs | 77 +++++++ mling/src/cli/refresh.rs | 32 +++ mling/src/main.rs | 14 ++ mling/src/namespace_manager.rs | 113 ++++++++++ mling/src/project_installer.rs | 153 ++++++++++++++ mling/src/project_solver.rs | 105 ++++++++++ mling/tmpl/load.fish | 75 +++++++ mling/tmpl/load.ps1 | 85 ++++++++ mling/tmpl/load.sh | 100 +++++++++ 14 files changed, 1537 insertions(+) create mode 100644 mling/Cargo.lock create mode 100644 mling/Cargo.toml create mode 100644 mling/src/cli.rs create mode 100644 mling/src/cli/list.rs create mode 100644 mling/src/cli/namespace_mgr.rs create mode 100644 mling/src/cli/read.rs create mode 100644 mling/src/cli/refresh.rs create mode 100644 mling/src/main.rs create mode 100644 mling/src/namespace_manager.rs create mode 100644 mling/src/project_installer.rs create mode 100644 mling/src/project_solver.rs create mode 100644 mling/tmpl/load.fish create mode 100644 mling/tmpl/load.ps1 create mode 100644 mling/tmpl/load.sh (limited to 'mling') diff --git a/mling/Cargo.lock b/mling/Cargo.lock new file mode 100644 index 0000000..7167d1d --- /dev/null +++ b/mling/Cargo.lock @@ -0,0 +1,455 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "bitflags" +version = "2.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4512299f36f043ab09a583e57bceb5a5aab7a73db1805848e8fef3c9e8c78b3" +dependencies = [ + "serde_core", +] + +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "colored" +version = "3.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "faf9468729b8cbcea668e36183cb69d317348c2e08e994829fb56ebfdfbaac34" +dependencies = [ + "windows-sys", +] + +[[package]] +name = "dirs" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3e8aa94d75141228480295a7d0e7feb620b1a5ad9f12bc40be62411e38cce4e" +dependencies = [ + "dirs-sys", +] + +[[package]] +name = "dirs-sys" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e01a3366d27ee9890022452ee61b2b63a67e6f13f58900b651ff5665f0bb1fab" +dependencies = [ + "libc", + "option-ext", + "redox_users", + "windows-sys", +] + +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + +[[package]] +name = "getrandom" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" +dependencies = [ + "cfg-if", + "libc", + "wasi", +] + +[[package]] +name = "hashbrown" +version = "0.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4f467dd6dccf739c208452f8014c75c18bb8301b050ad1cfb27153803edb0f51" + +[[package]] +name = "indexmap" +version = "2.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9" +dependencies = [ + "equivalent", + "hashbrown", +] + +[[package]] +name = "itoa" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" + +[[package]] +name = "just_fmt" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5454cda0d57db59778608d7a47bff5b16c6705598265869fb052b657f66cf05e" + +[[package]] +name = "just_template" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db3edb658c34b10b69c4b3b58f7ba989cd09c82c0621dee1eef51843c2327225" +dependencies = [ + "just_fmt", +] + +[[package]] +name = "libc" +version = "0.2.186" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66" + +[[package]] +name = "libredox" +version = "0.1.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e02f3bb43d335493c96bf3fd3a321600bf6bd07ed34bc64118e9293bdffea46c" +dependencies = [ + "libc", +] + +[[package]] +name = "memchr" +version = "2.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" + +[[package]] +name = "mingling" +version = "0.1.7" +dependencies = [ + "mingling_core", + "mingling_macros", + "serde", + "size", +] + +[[package]] +name = "mingling-cli" +version = "0.1.0" +dependencies = [ + "colored", + "dirs", + "just_fmt", + "mingling", + "serde", + "serde_json", + "toml 0.9.12+spec-1.1.0", +] + +[[package]] +name = "mingling_core" +version = "0.1.7" +dependencies = [ + "just_fmt", + "just_template", + "once_cell", + "ron", + "serde", + "serde_json", + "serde_yaml", + "thiserror", + "toml 1.1.2+spec-1.1.0", +] + +[[package]] +name = "mingling_macros" +version = "0.1.7" +dependencies = [ + "just_fmt", + "once_cell", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "once_cell" +version = "1.21.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" + +[[package]] +name = "option-ext" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" + +[[package]] +name = "proc-macro2" +version = "1.0.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "redox_users" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4e608c6638b9c18977b00b475ac1f28d14e84b27d8d42f70e0bf1e3dec127ac" +dependencies = [ + "getrandom", + "libredox", + "thiserror", +] + +[[package]] +name = "ron" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4147b952f3f819eca0e99527022f7d6a8d05f111aeb0a62960c74eb283bec8fc" +dependencies = [ + "bitflags", + "once_cell", + "serde", + "serde_derive", + "typeid", + "unicode-ident", +] + +[[package]] +name = "ryu" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f" + +[[package]] +name = "serde" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde_core" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.149" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86" +dependencies = [ + "itoa", + "memchr", + "serde", + "serde_core", + "zmij", +] + +[[package]] +name = "serde_spanned" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6662b5879511e06e8999a8a235d848113e942c9124f211511b16466ee2995f26" +dependencies = [ + "serde_core", +] + +[[package]] +name = "serde_yaml" +version = "0.9.34+deprecated" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a8b1a1a2ebf674015cc02edccce75287f1a0130d394307b36743c2f5d504b47" +dependencies = [ + "indexmap", + "itoa", + "ryu", + "serde", + "unsafe-libyaml", +] + +[[package]] +name = "size" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b6709c7b6754dca1311b3c73e79fcce40dd414c782c66d88e8823030093b02b" + +[[package]] +name = "syn" +version = "2.0.117" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "thiserror" +version = "2.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "toml" +version = "0.9.12+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf92845e79fc2e2def6a5d828f0801e29a2f8acc037becc5ab08595c7d5e9863" +dependencies = [ + "indexmap", + "serde_core", + "serde_spanned", + "toml_datetime 0.7.5+spec-1.1.0", + "toml_parser", + "toml_writer", + "winnow 0.7.15", +] + +[[package]] +name = "toml" +version = "1.1.2+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81f3d15e84cbcd896376e6730314d59fb5a87f31e4b038454184435cd57defee" +dependencies = [ + "indexmap", + "serde_core", + "serde_spanned", + "toml_datetime 1.1.1+spec-1.1.0", + "toml_parser", + "toml_writer", + "winnow 1.0.2", +] + +[[package]] +name = "toml_datetime" +version = "0.7.5+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92e1cfed4a3038bc5a127e35a2d360f145e1f4b971b551a2ba5fd7aedf7e1347" +dependencies = [ + "serde_core", +] + +[[package]] +name = "toml_datetime" +version = "1.1.1+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3165f65f62e28e0115a00b2ebdd37eb6f3b641855f9d636d3cd4103767159ad7" +dependencies = [ + "serde_core", +] + +[[package]] +name = "toml_parser" +version = "1.1.2+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2abe9b86193656635d2411dc43050282ca48aa31c2451210f4202550afb7526" +dependencies = [ + "winnow 1.0.2", +] + +[[package]] +name = "toml_writer" +version = "1.1.1+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "756daf9b1013ebe47a8776667b466417e2d4c5679d441c26230efd9ef78692db" + +[[package]] +name = "typeid" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc7d623258602320d5c55d1bc22793b57daff0ec7efc270ea7d55ce1d5f5471c" + +[[package]] +name = "unicode-ident" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" + +[[package]] +name = "unsafe-libyaml" +version = "0.2.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "673aac59facbab8a9007c7f6108d11f63b603f7cabff99fabf650fea5c32b861" + +[[package]] +name = "wasi" +version = "0.11.1+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link", +] + +[[package]] +name = "winnow" +version = "0.7.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df79d97927682d2fd8adb29682d1140b343be4ac0f08fd68b7765d9c059d3945" + +[[package]] +name = "winnow" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2ee1708bef14716a11bae175f579062d4554d95be2c6829f518df847b7b3fdd0" + +[[package]] +name = "zmij" +version = "1.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" diff --git a/mling/Cargo.toml b/mling/Cargo.toml new file mode 100644 index 0000000..b722420 --- /dev/null +++ b/mling/Cargo.toml @@ -0,0 +1,38 @@ +[package] +name = "mingling-cli" +version = "0.1.0" +edition = "2024" + +[[bin]] +name = "mling" +path = "src/main.rs" + +[profile.dev] +opt-level = 0 +debug = true + +[profile.release] +opt-level = 3 +lto = "fat" +codegen-units = 1 +panic = "abort" +strip = true + + +[dependencies] +mingling = { path = "../mingling", features = [ + "parser", + "comp", + "general_renderer", +] } + +serde = { version = "1", features = ["derive"] } +serde_json = "1" + +colored = "3.1.1" +dirs = "6.0.0" +just_fmt = "0.1.2" +toml = "0.9.8" + +[build-dependencies] +mingling = { path = "../mingling", features = ["comp"] } diff --git a/mling/src/cli.rs b/mling/src/cli.rs new file mode 100644 index 0000000..e0dfbe6 --- /dev/null +++ b/mling/src/cli.rs @@ -0,0 +1,45 @@ +use mingling::{ + macros::renderer, + setup::{BasicProgramSetup, GeneralRendererSetup}, +}; + +use crate::{__completion_gen::CompletionDispatcher, DispatcherNotFound, ThisProgram}; + +pub mod list; +pub use list::*; + +pub mod namespace_mgr; +pub use namespace_mgr::*; + +pub mod read; +pub use read::*; + +pub mod refresh; +pub use refresh::*; + +pub fn cli_entry() { + let mut program = ThisProgram::new(); + + program.with_setup(BasicProgramSetup); + program.with_setup(GeneralRendererSetup); + program.with_dispatcher(CompletionDispatcher); + + program.with_dispatcher(ListInstalledCommand); + program.with_dispatchers(( + TrustNamespaceCommand, + UntrustNamespaceCommand, + SetTrustNamespaceCommand, + RemoveNamespaceCommand, + )); + program.with_dispatcher(RefreshCommand); + program.with_dispatchers(( + ReadTargetDirCommand, + ReadWorkspaceRootCommand, + ReadBinariesCommand, + )); + + program.exec(); +} + +#[renderer] +pub(crate) fn render_help(_prev: DispatcherNotFound) {} diff --git a/mling/src/cli/list.rs b/mling/src/cli/list.rs new file mode 100644 index 0000000..9aff22b --- /dev/null +++ b/mling/src/cli/list.rs @@ -0,0 +1,117 @@ +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::{ThisProgram, 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) -> NextProcess { + let picker = Picker::new(prev.inner); + let r = picker + .pick::("--trusted") + .pick::("--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, + untrusted: Vec, + untagged: Vec, + option: StateListInstalledOptions, +} + +#[chain] +pub(crate) fn handle_state_list_installed_option(prev: StateListInstalledOptions) -> NextProcess { + 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, r); + print_list( + "Unrusted".bright_red().bold().to_string(), + prev.untrusted, + r, + ); + print_list( + "Untagged".bright_black().bold().to_string(), + prev.untagged, + r, + ); + } + StateListInstalledOptions::OnlyTrusted => { + print_list("Trusted".bright_green().bold().to_string(), prev.trusted, r); + } + StateListInstalledOptions::OnlyUntrusted => { + print_list( + "Unrusted".bright_red().bold().to_string(), + prev.untrusted, + r, + ); + } + } +} + +fn print_list(title: String, list: Vec, r: &mut RenderResult) { + if list.is_empty() { + return; + } + + r_println!("{}", title); + + let mut i = 1; + for namespace in list.iter() { + r_println!(" {}. {}\n", i.to_string(), namespace.bold()); + i += 1; + } +} diff --git a/mling/src/cli/namespace_mgr.rs b/mling/src/cli/namespace_mgr.rs new file mode 100644 index 0000000..9781040 --- /dev/null +++ b/mling/src/cli/namespace_mgr.rs @@ -0,0 +1,128 @@ +use mingling::{ + ShellContext, Suggest, SuggestItem, + macros::{chain, completion, dispatcher, pack, r_println, renderer, route, suggest}, + parser::{Picker, Yes}, +}; + +use crate::{ + ThisProgram, + 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(|i| SuggestItem::new(i)) + .collect::>(), + ); + } + 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(|i| SuggestItem::new(i)) + .collect::>(), + ); + } + 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(|i| SuggestItem::new(i)) + .collect::>(), + ); + } + 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(|i| SuggestItem::new(i)) + .collect::>(), + ); + } + return suggest!(); +} + +#[chain] +pub(crate) fn handle_set_trust(p: SetTrustNamespaceEntry) -> NextProcess { + let (trusted, namespace) = route!( + Picker::new(p.inner) + .pick::(["-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) -> NextProcess { + 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) -> NextProcess { + 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) -> NextProcess { + 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 new file mode 100644 index 0000000..8717932 --- /dev/null +++ b/mling/src/cli/read.rs @@ -0,0 +1,77 @@ +use colored::Colorize; +use std::path::PathBuf; + +use mingling::{ + Groupped, + macros::{chain, dispatcher, pack, r_println, renderer}, +}; +use serde::Serialize; + +use crate::{ + ThisProgram, + 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, +} + +#[chain] +pub(crate) fn handle_target_dir_entry(_prev: ReadTargetDirEntry) -> NextProcess { + match solve_current_dir() { + Ok(solved) => { + let dir = solved.target_dir; + ResultDir::new(dir).to_render() + } + Err(_) => ResultTargetDirNotFound::new(()).to_render(), + } +} + +#[chain] +pub(crate) fn handle_workspace_root_entry(_prev: ReadWorkspaceRootEntry) -> NextProcess { + match solve_current_dir() { + Ok(solved) => { + let dir = solved.workspace_root; + ResultDir::new(dir).to_render() + } + Err(_) => ResultTargetDirNotFound::new(()).to_render(), + } +} + +#[chain] +pub(crate) fn handle_binaries_entry(_prev: ReadBinariesEntry) -> NextProcess { + 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) { + let mut i = 1; + for item in prev.bin.iter() { + r_println!( + "{}. {} ({})", + i.to_string(), + item.name.bold(), + item.path.to_string_lossy().underline().bright_cyan() + ); + i += 1; + } +} diff --git a/mling/src/cli/refresh.rs b/mling/src/cli/refresh.rs new file mode 100644 index 0000000..368670e --- /dev/null +++ b/mling/src/cli/refresh.rs @@ -0,0 +1,32 @@ +use mingling::{ + ShellContext, Suggest, + macros::{chain, completion, dispatcher, pack, suggest}, + parser::Picker, +}; + +use crate::{ThisProgram, project_installer::install_all}; + +dispatcher!("refresh", RefreshCommand => RefreshEntry); + +pack!(ResultRefreshCompleted = ()); + +#[completion(RefreshEntry)] +pub(crate) fn comp_refresh(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_refresh_entry(prev: RefreshEntry) -> NextProcess { + let is_clean_before_build = Picker::new(prev.inner) + .pick::(["--clean", "-c"]) + .unpack(); + let _ = install_all(is_clean_before_build); + + ResultRefreshCompleted::new(()) +} diff --git a/mling/src/main.rs b/mling/src/main.rs new file mode 100644 index 0000000..7b72dc1 --- /dev/null +++ b/mling/src/main.rs @@ -0,0 +1,14 @@ +use mingling::macros::gen_program; + +pub mod cli; +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 new file mode 100644 index 0000000..4d36136 --- /dev/null +++ b/mling/src/namespace_manager.rs @@ -0,0 +1,113 @@ +use std::path::PathBuf; + +use just_fmt::kebab_case; + +pub fn list_namespaces( + show_trusted: bool, + show_untrusted: bool, + show_untagged: bool, +) -> Vec { + let wdir = working_dir(); + if !wdir.exists() { + return Vec::new(); + } + + let mut namespaces = Vec::new(); + let entries = match std::fs::read_dir(&wdir) { + Ok(entries) => entries, + Err(_) => return Vec::new(), + }; + for entry in entries { + let entry = match entry { + Ok(e) => e, + Err(_) => continue, + }; + let path = entry.path(); + if path.is_dir() { + if let Some(name) = path.file_name() { + if 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 { + // Remove TRUSTED 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); + } +} + +pub fn working_dir() -> PathBuf { + dirs::data_dir().unwrap().join("mingling") +} + +pub fn namespace_dir(namespace: String) -> PathBuf { + working_dir().join(kebab_case!(namespace)) +} + +pub fn is_untrusted_namespace(namespace: String) -> bool { + let untrusted_file = namespace_dir(namespace).join("UNTRUSTED"); + untrusted_file.exists() +} + +pub fn is_trusted_namespace(namespace: String) -> bool { + let trusted = namespace_dir(namespace).join("TRUSTED"); + trusted.exists() +} + +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() +} + +pub fn bin_dir(namespace: String) -> PathBuf { + namespace_dir(namespace).join("bin") +} + +pub fn comp_dir(namespace: String) -> PathBuf { + namespace_dir(namespace).join("comp") +} + +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/project_installer.rs b/mling/src/project_installer.rs new file mode 100644 index 0000000..d004e40 --- /dev/null +++ b/mling/src/project_installer.rs @@ -0,0 +1,153 @@ +use std::path::PathBuf; + +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, +} + +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(()) +} + +pub fn install_this_project( + current: 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::new( + std::io::ErrorKind::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::new( + std::io::ErrorKind::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(()) +} + +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 new file mode 100644 index 0000000..381bba2 --- /dev/null +++ b/mling/src/project_solver.rs @@ -0,0 +1,105 @@ +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, +} + +#[derive(Debug, Serialize)] +pub struct BinaryItem { + pub name: String, + pub path: PathBuf, +} + +pub fn solve_current_dir() -> Result { + let current = std::env::current_dir()?; + solve(current) +} + +pub fn solve(current: PathBuf) -> Result { + let (target_dir, workspace_root, binaries) = solve_inner(¤t)?; + Ok(ProjectSolveResult { + target_dir, + workspace_root, + binaries, + }) +} + +fn solve_inner(current: &PathBuf) -> Result<(PathBuf, PathBuf, Vec), 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::new( + std::io::ErrorKind::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 + .map(|k| k.iter().any(|v| v.as_str() == Some("bin"))) + .unwrap_or(false); + 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/tmpl/load.fish b/mling/tmpl/load.fish new file mode 100644 index 0000000..19e1ef7 --- /dev/null +++ b/mling/tmpl/load.fish @@ -0,0 +1,75 @@ +#!/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 new file mode 100644 index 0000000..d666338 --- /dev/null +++ b/mling/tmpl/load.ps1 @@ -0,0 +1,85 @@ +#!/usr/bin/env pwsh + +# Save original directory, restore after execution +$_load_original_dir = Get-Location + +# Load completion script mling.ps1 from the current directory +$mlingScript = Join-Path -Path (Get-Location) -ChildPath ".comp/mling_comp.ps1" +if (Test-Path $mlingScript) { + . $mlingScript +} + +# Change to script directory +$scriptPath = Split-Path -Parent $MyInvocation.MyCommand.Path +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 +Get-ChildItem -Directory -Path "*/bin/" | ForEach-Object { + $env:PATH = "$($_.FullName);$env:PATH" +} + +# Helper function: execute script with appropriate shell +function _load_script { + param([string]$script) + # Only handle .ps1 scripts + if ($script -like "*.ps1") { + & $script 2>$null + } +} + +# Iterate over all namespaces +Get-ChildItem -Directory | ForEach-Object { + $_namespace = $_.Name + + # Skip if UNTRUSTED marker exists + if (Test-Path "$_namespace\UNTRUSTED") { return } + + $_comp_dir = "$_namespace\comp" + if (-not (Test-Path $_comp_dir -PathType Container)) { return } + + # Find all loadable scripts under comp + $_scripts = Get-ChildItem -Path $_comp_dir -File -Include "*.ps1" -ErrorAction SilentlyContinue + if (-not $_scripts) { return } + + # Count scripts + $_count = ($_scripts | Measure-Object).Count + + # If TRUSTED marker exists, load directly + if (Test-Path "$_namespace\TRUSTED") { + $_scripts | ForEach-Object { + _load_script $_.FullName + } + return + } + + # No marker, ask user + $answer = Read-Host "'$_namespace' 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 "$_namespace\TRUSTED" -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)$") { + $_scripts | ForEach-Object { + _load_script $_.FullName + } + } + } else { + New-Item -ItemType File -Path "$_namespace\UNTRUSTED" -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 new file mode 100644 index 0000000..0d48e95 --- /dev/null +++ b/mling/tmpl/load.sh @@ -0,0 +1,100 @@ +#!/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 -- cgit