From 17217317eaaf57dd5c39538c115e35ddccb8666d Mon Sep 17 00:00:00 2001 From: 魏曹先生 <1992414357@qq.com> Date: Mon, 25 May 2026 22:01:06 +0800 Subject: Restructure docs add template and interactive tutorial, update tool runner --- docs/pages/1-creating-your-first-program.md | 258 -------------------- docs/pages/2-implementing-fallbacks.md | 141 ----------- docs/pages/3-parsing-complex-arguments.md | 364 ---------------------------- docs/pages/4-implementing-help-display.md | 4 - docs/pages/5-implementing-completion.md | 4 - 5 files changed, 771 deletions(-) delete mode 100644 docs/pages/1-creating-your-first-program.md delete mode 100644 docs/pages/2-implementing-fallbacks.md delete mode 100644 docs/pages/3-parsing-complex-arguments.md delete mode 100644 docs/pages/4-implementing-help-display.md delete mode 100644 docs/pages/5-implementing-completion.md (limited to 'docs/pages') diff --git a/docs/pages/1-creating-your-first-program.md b/docs/pages/1-creating-your-first-program.md deleted file mode 100644 index 7348805..0000000 --- a/docs/pages/1-creating-your-first-program.md +++ /dev/null @@ -1,258 +0,0 @@ -

Creating your first Program

-

- Learn Mingling and use it to create your first command-line program -

- -## Intro - - This chapter will guide you through **Mingling** step by step. - - Before we start, let me explain what **Mingling** can do: - - Without extra features, it is a sub-command dispatch system based on `proc-macro`: it matches user input, finds & creates the corresponding data, then pushes that data into a dispatcher that continually transforms its type. When the data can no longer be transformed, the program renders the final result to the terminal. - - In other words, you need to understand a new dev paradigm: **a fully type-based dispatch system**. This may feel **frustrating** at first, but once you get the hang of it, you'll be able to write CLI apps that are super easy to modify and extend. - - - -## Creating a Basic Program - - Next I'll walk you through creating a basic program—I assume you already have an empty Rust project ready! - -#### 1. Add Dependencies - - Add the following deps to `Cargo.toml` ✏️ - -```toml -[dependencies] -mingling = "0.1.9" - -# If you want the latest, try the version hosted on Github -mingling = { git = "https://github.com/catilgrass/mingling", branch = "main" } -``` - -> [!NOTE] -> -> This version matches the **Mingling** version used when writing this doc. Check [crates.io](https://crates.io/crates/mingling) for the latest release! 😄 -> -> **Mingling** docs are actively updated to keep pace with the latest version. - - - -#### 2. Create the Program - - Now, create the program in `src/main.rs` ✏️ - -```rust -fn main() { - // Create ThisProgram and run it - ThisProgram::new().exec(); -} - -// The gen_program! macro collects *all preceding* components & types -// then generates the `ThisProgram` struct -mingling::macros::gen_program!(); -``` - -> [!TIP] -> -> When `gen_program!()` expands, it gathers info from other components & types that were expanded before it. This means you must place `gen_program!()` at the very last expansion point in the crate. -> -> I recommend putting it at the end of `main.rs` or `lib.rs`. - - - -#### 3. Create a Command - - Of course, the program currently does nothing—it won't output anything at runtime. So let's create our first command `greet` and say hi to someone ✏️ - -```rust -fn main() { - // ... -} - -// Create a dispatcher, binding GreetCommand to the "greet" sub-command -// When the user specifies this command, send GreetEntry to the dispatcher -dispatcher!("greet", GreetCommand => GreetEntry); - -// ... -gen_program!(); -``` - - Don't be scared by the sudden macro and two new types! Let me explain what this macro does: - -##### About the `dispatcher!` macro 💡 - -1. It creates a `GreetCommand` struct and implements the `Dispatcher` trait - -​ *This tells the framework: there's a new dispatcher that will handle a sub-command's behavior.* - -2. It implements the `Dispatcher` trait's `node(&self) -> Node` function, setting the node to `"greet"` - -​ *This tells the framework: this dispatcher handles the `"greet"` sub-command.* - -3. It implements the `Dispatcher` trait's `begin` function, converting the user's full input into the first type `GreetEntry` - -​ *This tells the framework: when this dispatcher is matched, it sends a `GreetEntry` type to the dispatcher for further processing.* - - In short: **"When user types `greet`, I create a `GreetEntry` and throw it into the dispatcher for conversion."** - - - -#### 4. Register the Command - - After creating the `Dispatcher`, we have two types: `GreetCommand` and `GreetEntry`. First, register `GreetCommand` with `ThisProgram` ✏️ - -```rust -fn main() { - let mut program = ThisProgram::new(); - - // Register the dispatcher - program.with_dispatcher(GreetCommand); - program.exec(); -} -``` - - Now `ThisProgram` recognizes the `"greet"` sub-command, but the framework still doesn't know what `"greet"` should do. That's where we implement the actual logic: - - - -#### 5. Implement Rendering Behavior - - We want `"greet"` to output `"Hello, World"`: since we're outputting to the screen, we can use another **Mingling** component, `Renderer`, which handles rendering data to the terminal ✏️ - -```rust -// ... -dispatcher!("greet", GreetCommand => GreetEntry); - -// Declare a renderer `render_greet`, specifying the previous type as `GreetEntry` -#[renderer] -fn render_greet(_prev: GreetEntry) { - r_println!("Hello, World!"); -} - -// ... -gen_program!(); // The renderer will be registered with the program -``` - - For functions marked with `#[renderer]`, **Mingling** strictly enforces only one function signature: - -```rust -#[renderer] -fn renderer_name (_prev: PreviousType) { } -``` - - The macro reads the type of the first param and tells `gen_program!` that this function renders that type. - -##### About `r_println!()` 💡 - - You might notice that the print macro used inside `#[renderer]` is `r_println!` instead of `println!`. This is because the framework's rendering logic doesn't happen inside that function: after `#[renderer]` expands, it injects a `__renderer_inner_result: &mut RenderResult` into the function; `r_println!` appends the message to the `RenderResult`, and after the dispatcher closes, the final rendered data is handed to `Program::exec` for output. - - - -#### 6. Add Execution Logic - - I bet you're already itching to implement something like `greet Alice` to output `"Hello, Alice!"`—and this section is about to do just that! - - **Mingling**'s core execution flow is `Dispatcher -> Chain -> Renderer`, and the key part is `Chain`: it converts the input data type into another type, then lets the dispatcher find the next `Chain` or `Renderer` based on the result type ✏️ - -```rust -dispatcher!("greet", GreetCommand => GreetEntry); - -// Wrap the intermediate type `ResultGreetSomeone` -pack!(ResultGreetSomeone = String); - -#[chain] -fn handle_greet_entry(prev: GreetEntry) -> Next { - let args = prev.inner; - let name = args - .first() - .cloned() - .unwrap_or_else(|| "World".to_string()); - - // Wrap into intermediate type - ResultGreetSomeone::new(name) -} - -#[renderer] -fn render_greet_someone(prev: ResultGreetSomeone) { - // Deref prev to get the raw type - r_println!("Hello, {}!", *prev); -} -``` - - Just like `#[renderer]`, we created a `#[chain]` that processes type `GreetEntry` and outputs `ResultGreetSomeone`. - - This inserts a `Chain` between the original `Dispatcher` and `Renderer`: it extracts the user's input params (or falls back to "World"), then passes them to the renderer to print to the terminal. - -##### About `Next` 💡 - - `Next` is a placeholder generated by `gen_program!()`. After `#[chain]` expands, it's replaced by a type-erased type `ChainProcess` that the dispatcher can recognize, helping reduce boilerplate code. - -> [!NOTE] -> -> `Next` is a temporary solution; the next update will wait until Rust's `Impl In Type Aliases` feature is stable. -> -> **But don't worry**: the next `Next` update won't introduce **breaking changes!** - -##### About `pack!` 💡 - - `pack!` is an **extremely** frequently used macro in **Mingling** development: it wraps any type into another type and auto-derives the traits the framework needs. - - Its syntax is as simple as you see: - -```rust -pack!(PackedType = RawType); -``` - - Note: `pack!` doesn't support types with lifetimes, because types are always moved (not borrowed) between dispatchers. - - - -#### 7. Compile & Run - - Alright, we've completed a basic CLI app. Here's the full code—you can paste it and run it directly: - -```rust -use mingling::macros::{chain, dispatcher, gen_program, pack, r_println, renderer}; - -fn main() { - let mut program = ThisProgram::new(); - program.with_dispatcher(GreetCommand); - program.exec(); -} - -dispatcher!("greet", GreetCommand => GreetEntry); - -pack!(ResultGreetSomeone = String); - -#[chain] -fn handle_greet_entry(prev: GreetEntry) -> Next { - let args = prev.inner; - let name = args.first().cloned().unwrap_or_else(|| "World".to_string()); - - ResultGreetSomeone::new(name) -} - -#[renderer] -fn render_greet_someone(prev: ResultGreetSomeone) { - r_println!("Hello, {}!", *prev); -} - -gen_program!(); -``` - - Output: - -```bash -~> your-bin greet -Hello, World! -~> your-bin greet Alice -Hello, Alice! -``` - - At this point, you have successfully created a basic **Mingling** command-line program. The next chapter will explain how to implement a fallback mechanism for your command-line program to handle cases where a command or renderer does not exist. - -

- Written by @Weicao-CatilGrass -

diff --git a/docs/pages/2-implementing-fallbacks.md b/docs/pages/2-implementing-fallbacks.md deleted file mode 100644 index 3f3cb93..0000000 --- a/docs/pages/2-implementing-fallbacks.md +++ /dev/null @@ -1,141 +0,0 @@ -

Implementing Fallbacks

-

- Handling error cases in your program using a fallback mechanism -

- -## Recap - - In the last post, we introduced how to develop a basic CLI program using **Mingling**: you can use the `"greet"` subcommand to output `"Hello, World!"`, or use `"greet Alice"` to output `"Hello, Alice!"` - - But what happens when the user does not enter `"greet"`? Let's type a command and find out ⌨️ - -```bash -~> your-bin hello -~> your-bin hello Alice -``` - - **It does nothing!** 👆 - - Let me explain why: **Mingling** doesn't presume to act; it will not output anything to the terminal no matter what happens (except for `panic!` under `unwind`) - - This means that if you need to actively do something when your CLI program encounters an error, you have to state it explicitly. - - Fortunately, **Mingling** provides a convenient interface for this functionality: inside the `gen_program!` macro, two `FallBack` types are generated - -|Type|When it occurs|How it occurs| -|-|-|-| -|RendererNotFound|When a renderer cannot be found for scheduling|Scheduled as a `Chain`| -|DispatcherNotFound|When a command is entered but no dispatcher matches|Scheduled as a `Chain`| - -### The `DispatcherNotFound` Type - - Let's first focus on the `DispatcherNotFound` type. It is produced as follows: - -```rust -// 1. Define the `greet` command -dispatcher!("greet", GreetCommand => GreetEntry); - -fn main() { - // ->> User enters "hello Alice" - let mut program = ThisProgram::new(); - - // 2. Import the `greet` command - program.with_dispatcher(GreetCommand); - - // 3. Execute the program - program.exec(); -} - -// ... - -// 5. Receive the DispatcherNotFound dispatch -#[renderer] -fn dispatcher_not_found(prev: DispatcherNotFound) { - // 6. Output - r_println!( - "Cannot match any command! Current input: \"{}\"", - prev.join(" ") - ); -} - -// 4. Cannot match any dispatcher named `hello` -// Forward the user's arguments as-is to DispatcherNotFound -gen_program!(); -``` - - The output of the above program is: - -```bash -~> omg hello -Cannot match any command! Current input: "hello" - -~> omg hello Alice -Cannot match any command! Current input: "hello Alice" -``` - - Now, if the user enters a command that doesn't match, **Mingling** will output the appropriate message! - -## The `RendererNotFound` Type - - `RendererNotFound` can be produced in two ways: - - 1. The type was explicitly dispatched to a `Renderer` (using the `.to_render()` function), but the type does not have a renderer implementation - 2. The type was dispatched to a `Chain`, but the type has neither a chain nor a renderer implementation - - Generally, `RendererNotFound` **should not occur in business logic**: its dispatch means your type needs to be rendered but can't be. You can use this type to pinpoint which type is missing a renderer implementation ✏️ - -```rust -dispatcher!("greet", GreetCommand => GreetEntry); - -fn main() { - let mut program = ThisProgram::new(); - - program.with_dispatcher(GreetCommand); - program.exec(); -} - -pack!(ResultGreetSomeone = String); - -#[chain] -fn handle_greet_entry(prev: GreetEntry) -> Next { - let args = prev.inner; - let name = args.first().cloned().unwrap_or_else(|| "World".to_string()); - - ResultGreetSomeone::new(name) -} - -// Let's intentionally remove the renderer implementation for `ResultGreetSomeone` -// #[renderer] -// fn render_greet_someone(prev: ResultGreetSomeone) { -// r_println!("Hello, {}!", *prev); -// } - -#[renderer] -fn renderer_not_found(prev: RendererNotFound) { - if *prev == "DispatcherNotFound" { - return; // Exclude the "DispatcherNotFound" type - } - - // Trigger `panic!` when a renderer is not found - panic!("Renderer \"{}\" not found!", *prev); -} - -gen_program!(); - -``` - - The output of the above program is: - -```bash -~> your-bin greet Alice - -thread 'main' (90772) panicked at src/bin/your-bin.rs:30:5: -Renderer "ResultGreetSomeone" not found! -note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace -``` - - The above is the fallback mechanism of **Mingling**. In the next chapter, you will learn how to use `Picker` to parse complex user inputs. - -

- Written by @Weicao-CatilGrass -

diff --git a/docs/pages/3-parsing-complex-arguments.md b/docs/pages/3-parsing-complex-arguments.md deleted file mode 100644 index 141c571..0000000 --- a/docs/pages/3-parsing-complex-arguments.md +++ /dev/null @@ -1,364 +0,0 @@ -

Parsing Complex Args

-

- Use Mingling Picker to parse complex user input -

- -## Intro - - In the prev. example, we built a CLI app with a `"greet"` subcommand that outputs the user's first arg. - - You may have noticed the approach used was almost direct string manipulation—not very semantic, and hard to maintain long-term. - -```rust -let name = args.first().cloned().unwrap_or_else(|| "World".to_string()); -``` - - This chapter introduces a new **Mingling** feature: `Picker`. It provides a lightweight parsing solution that meshes well with **Mingling**'s typed routing. - - To enable `Picker`, edit `Cargo.toml` ✏️ - -```toml -[dependencies] -mingling = { - version = "...", - features = ["parser"] -} -``` - - Enough talk, let's get coding and rewrite the parsing logic from the prev. section ✏️ - -```rust -#[chain] -fn handle_greet_entry(prev: GreetEntry) -> Next { - // Prev. approach: - // let args = prev.inner; - // let name = args.first().cloned().unwrap_or_else(|| "World".to_string()); - - // New approach with Picker - let name = prev.pick_or((), "World").unpack(); - - ResultGreetSomeone::new(name) -} -``` - - `Picker` implements `pick`, `pick_or`, and `pick_or_route` for anything `Into>`. These functions let you semantically **pick** args from a string list and convert them into structured data. - - In the code above: - -```rust -prev.pick_or((), "World").unpack(); -``` - - Its meaning: - -```rust - prev.pick_or((), "World").unpack(); -// ~~~~ ~~~~~~~ ~~ ~~~~~~~ ~~~~~~~~ -// | | | | |_ unpack to String -// | | | |__________ default value is "World" -// | | |______________ pick the first positional arg (no flag) -// | |______________________ pick or use default -// |___________________________ from the prev. input -``` - -## Parsing Flag Args - - If your app needs to parse flag args (e.g., `greet --name Alice`), do: - -```rust -prev.pick_or(["--name", "-n"], "World").unpack(); -``` - - Its meaning: - -```rust - prev.pick_or(["--name", "-n"], "World").unpack(); -// ~~~~ ~~~~~~~ ~~~~~~~~~~~~~~~~ ~~~~~~~ ~~~~~~~~ -// | | | | |_ unpack to String -// | | | |__________ default value is "World" -// | | |____________________________ pick the value after "--name" or "-n" -// | |____________________________________ pick or use default -// |_________________________________________ from the prev. input -``` - -## About `.unpack()` 💡 - - You may have noticed `Picker` calls `.unpack()` at the end of parsing. It converts the parsed result into structured info. - - For a single pick, `.unpack()` returns a single value. For multiple picks, `Picker` returns a tuple: - -```rust -let name_single: String = prev.clone().pick_or((), "World").unpack(); -let (name, age, id) = prev - .pick::(["--name", "-n"]) - .pick::(["--age", "-a"]) - .pick::(["--id", "-I"]) - .unpack(); - -// Parses: --name Alice --age 21 --id 0711251 -``` - -> [!IMPORTANT] -> `Picker` is very order-sensitive, esp. with positional args: it parses sequentially. -> -> If you need to parse positional args, make sure to pick & consume all **flag args** first. - -## Using `pick_or_route` for Edge Cases - - Ha, as the old saying goes: "Never trust your users." Missing required args, type mismatches, enabling mutually exclusive options—these are all headache-inducing edge cases. - - `pick_or_route` handles these by routing the chain to a dedicated error-handling type, giving you fine-grained error control. - - Let's write a simple example showing basic usage: - -```rust -dispatcher!("greet", GreetCommand => GreetEntry); - -pack!(ResultGreetSomeone = String); -pack!(ErrorGreetNoNameProvided = ()); - -#[chain] -fn handle_greet_entry(prev: GreetEntry) -> Next { - // Use `pick_or_route` to extract the `--name` arg - // If missing or parse fails, route to ErrorGreetNoNameProvided - let pick_result = prev - .pick_or_route( - ["--name", "-n"], - ErrorGreetNoNameProvided::default().to_render(), - ) - // After using any routable method, `unpack` returns `Result` - .unpack(); - - // Use the `route!` macro to expand `pick_result`, - // If it's `Err`, the chain returns here, routing to the specified type - let name = route!(pick_result); - ResultGreetSomeone::new(name).to_chain() -} - -// Handles rendering for `ErrorGreetNoNameProvided` -#[renderer] -fn render_err_greet_no_name_provided(_prev: ErrorGreetNoNameProvided) { - r_println!("Error: No name provided.") -} - -#[renderer] -fn render_greet_someone(prev: ResultGreetSomeone) { - r_println!("Hello, {}!", *prev); -} -``` - - Using `pick_or_route` makes the code a bit more complex: `.unpack()` no longer returns the value directly, but `Result`. - - However, **Mingling** provides the `route!` macro to simplify expansion. It's not complex—just cuts some boilerplate: - -```rust -let name = route!(pick_result); - -// Expands to -let name = match pick_result { - Ok(r) => r, - Err(e) => return e, -}; -``` - -## Post-Processing Extracted Values - - After using `pick` to extract user input, you can use `after` or `after_or_route` to process the arg immediately ✏️ - -```rust -#[chain] -fn handle_greet_entry(prev: GreetEntry) -> Next { - let name = prev - .pick_or(["--name", "-n"], "World") - // After extracting `--name`, format it immediately - .after(|name: String| { - name.replace(['-', '_', '.'], " ") - .to_lowercase() - .trim() - .to_string() - }) - .unpack(); - - ResultGreetSomeone::new(name) // name is now formatted -} -``` - - Similarly, use `after_or_route` to handle format errors in input args ✏️ - -```rust -dispatcher!("greet", GreetCommand => GreetEntry); - -pack!(ResultGreetSomeone = String); -pack!(ErrorGreetNameTooLong = usize); - -#[chain] -fn handle_greet_entry(prev: GreetEntry) -> Next { - let pick_result = prev - .pick_or(["--name", "-n"], "World") - // Unlike `after`, this borrows &String - .after_or_route(|name: &String| { - name.replace(['-', '_', '.'], " ") - .to_lowercase() - .trim() - .to_string(); - - // Check name length, route to error type if too long - let len = name.len(); - if len < 32 { - Ok(name.clone()) - } else { - Err(ErrorGreetNameTooLong::new(len).to_render()) - } - }) - .unpack(); - let name = route!(pick_result); - - ResultGreetSomeone::new(name).to_chain() -} - -#[renderer] -fn render_error_greet_name_too_long(prev: ErrorGreetNameTooLong) { - let len = *prev; - r_println!("Error: name too long (length: {} > 32)", len); -} - -#[renderer] -fn render_greet_someone(prev: ResultGreetSomeone) { - r_println!("Hello, {}!", *prev); -} -``` - -## Parsing Booleans - - `Picker` can parse **bool** types too, but with both explicit and implicit modes: - - |Mode|Format| - |-|-| - |Explicit|`--confirm true` or `--confirm yes`| - |Implicit|`--confirmed`| - - - Using `.pick` on `bool` uses implicit parsing: flag present → `true` - - Using `.pick` on `mingling::parser::Yes` or `mingling::parser::True` uses explicit parsing; the value must be `true` / `yes` to be recognized as `true` - - Generally, implicit parsing is enough, but for positional args or important confirmations, explicit logic might be more semantic. - -```rust -#[chain] -fn handle_some_entry(prev: SomeEntry) -> Next { - let confirmed: bool = prev.pick::(()).unpack().is_yes(); - let confirm: bool = prev.pick::(["--confirm", "-C"]).unpack(); - - // other logic -} -``` - -## Special Use: `usize` Parsing - - **Mingling** has a special use for `usize`: parsing strings like `25G`, `32mb`, etc. ✏️ - -```rust -#[test] -fn parse_size() { - let vec = vec!["--size".to_string(), "25mib".to_string()]; - let size: usize = vec.pick(["--size", "-S"]).unpack(); - assert_eq!(size, 25 * 1024 * 1024); -} -``` - -## Custom Parsable Types - - Use the `Pickable` trait to make your types parsable by `Picker`. This is where `Picker`'s extensibility comes from ✏️ - -```rust -// Must implement Default: parse failures record the default directly -#[derive(Default)] -pub struct Address { - ip: String, - port: u16, -} - -impl Pickable for Address { - type Output = Self; - fn pick(args: &mut Argument, flag: Flag) -> Option { - // Extract raw string from Argument using Flag - let raw = args.pick_argument(flag)?; - - // Parse raw string into structured data - let parts: Vec<&str> = raw.split(':').collect(); - let ip = parts.first()?.to_string(); - let port: u16 = parts.get(1)?.parse().ok()?; - - Some(Address { ip, port }) - } -} -``` - - With `Pickable` implemented for `Address`, we can now use `ip:port` format for input ✏️ - -```rust -dispatcher!("connect", ConnectCommand => ConnectEntry); - -pack!(ResultConnected = Address); - -#[chain] -fn handle_connect_entry(prev: ConnectEntry) -> Next { - let address: Address = prev.pick("--addr").unpack(); - ResultConnected::new(address) -} - -#[renderer] -fn render_connected(prev: ResultConnected) { - let addr = prev.inner; - r_println!("Connected: IP: {} PORT: {}", addr.ip, addr.port); -} -``` - - Running it: - -```bash -~> your-bin connect --addr 127.0.0.1:8080 -Connected: IP: 127.0.0.1 PORT: 8080 -``` - -## Auto-Implementing Pickable for Enums - - No need to manually implement `Pickable` for enums: `Picker` auto-implements it for any type that implements `PickableEnum`, as long as it also implements `EnumTag` ✏️ - -```rust -// Debug : for rendering -// Default: for Picker parsing -// EnumTag: for implementing PickableEnum -#[derive(Debug, Default, EnumTag)] -pub enum Fruits { - #[default] - Apple, - Banana, - Orange, -} - -// Implement PickableEnum for Fruits -impl PickableEnum for Fruits {} -``` - - Now you can directly use `Picker` to parse this type ✏️ - -```rust -pack!(ResultFruit = Fruits); - -#[chain] -fn handle_eat_fruit_entry(prev: EatFruitEntry) -> Next { - let fruit: Fruits = prev.pick("--fruit").unpack(); - ResultFruit::new(fruit) -} - -#[renderer] -fn render_ate_fruit(prev: ResultFruit) { - r_println!("Picked fruit: {:?}", *prev); -} -``` - - That's all for `Picker`'s usage. In the next chapter, I'll introduce how to implement help docs for commands in **Mingling**. - -

- Written by @Weicao-CatilGrass -

diff --git a/docs/pages/4-implementing-help-display.md b/docs/pages/4-implementing-help-display.md deleted file mode 100644 index 625863e..0000000 --- a/docs/pages/4-implementing-help-display.md +++ /dev/null @@ -1,4 +0,0 @@ -

Impl Help Display

-

- Implement help documentation for commands using the help macro -

diff --git a/docs/pages/5-implementing-completion.md b/docs/pages/5-implementing-completion.md deleted file mode 100644 index 7622775..0000000 --- a/docs/pages/5-implementing-completion.md +++ /dev/null @@ -1,4 +0,0 @@ -

Impl Shell Completion

-

- Implementing a fully dynamic completion system using Mingling Completion -

-- cgit