aboutsummaryrefslogtreecommitdiff
path: root/examples
diff options
context:
space:
mode:
authorWeicao-CatilGrass <1992414357@qq.com>2026-06-09 06:34:55 +0800
committerWeicao-CatilGrass <1992414357@qq.com>2026-06-09 06:34:55 +0800
commitab796e81ab4e3e9fa1a26f8217635eebec658b5e (patch)
treebbaf957bd9f952e93b767f5d1228d3c99d450a0a /examples
parent78f282007980fe9c9ef143a6bc6fb76282957ab6 (diff)
Add full-todolist example project
Diffstat (limited to 'examples')
-rw-r--r--examples/full-todolist/.gitignore1
-rw-r--r--examples/full-todolist/Cargo.lock149
-rw-r--r--examples/full-todolist/Cargo.toml16
-rw-r--r--examples/full-todolist/page.toml10
-rw-r--r--examples/full-todolist/src/help.rs93
-rw-r--r--examples/full-todolist/src/main.rs197
-rw-r--r--examples/full-todolist/src/todolist.rs66
7 files changed, 532 insertions, 0 deletions
diff --git a/examples/full-todolist/.gitignore b/examples/full-todolist/.gitignore
new file mode 100644
index 0000000..5af20de
--- /dev/null
+++ b/examples/full-todolist/.gitignore
@@ -0,0 +1 @@
+.todo.json
diff --git a/examples/full-todolist/Cargo.lock b/examples/full-todolist/Cargo.lock
new file mode 100644
index 0000000..7a1ce38
--- /dev/null
+++ b/examples/full-todolist/Cargo.lock
@@ -0,0 +1,149 @@
+# This file is automatically @generated by Cargo.
+# It is not intended for manual editing.
+version = 4
+
+[[package]]
+name = "full-todolist"
+version = "0.1.0"
+dependencies = [
+ "mingling",
+ "serde",
+ "serde_json",
+]
+
+[[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 = "memchr"
+version = "2.8.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6b947ae49db0d222b1dbc6b113ce7248a3fc3a6ca21b696717bfc000ba4484d8"
+
+[[package]]
+name = "mingling"
+version = "0.2.0"
+dependencies = [
+ "mingling_core",
+ "mingling_macros",
+ "serde",
+ "size",
+]
+
+[[package]]
+name = "mingling_core"
+version = "0.2.0"
+dependencies = [
+ "just_fmt",
+ "serde",
+ "serde_json",
+]
+
+[[package]]
+name = "mingling_macros"
+version = "0.2.0"
+dependencies = [
+ "just_fmt",
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[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 = "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.150"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e8014e44b4736ed0538adeecded0fce2a272f22dc9578a7eb6b2d9993c74cfb9"
+dependencies = [
+ "itoa",
+ "memchr",
+ "serde",
+ "serde_core",
+ "zmij",
+]
+
+[[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 = "unicode-ident"
+version = "1.0.24"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75"
+
+[[package]]
+name = "zmij"
+version = "1.0.21"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa"
diff --git a/examples/full-todolist/Cargo.toml b/examples/full-todolist/Cargo.toml
new file mode 100644
index 0000000..e55d5e9
--- /dev/null
+++ b/examples/full-todolist/Cargo.toml
@@ -0,0 +1,16 @@
+[package]
+name = "full-todolist"
+version = "0.1.0"
+edition = "2024"
+
+[dependencies]
+serde = { version = "1.0.228", features = ["derive"] }
+serde_json = "1.0.150"
+
+[dependencies.mingling]
+path = "../../mingling"
+features = [
+ "parser",
+ "extra_macros",
+ "general_renderer",
+]
diff --git a/examples/full-todolist/page.toml b/examples/full-todolist/page.toml
new file mode 100644
index 0000000..9bdc7f1
--- /dev/null
+++ b/examples/full-todolist/page.toml
@@ -0,0 +1,10 @@
+[example]
+id = "full-todolist"
+name = "Todo List"
+icon = "📝"
+category = "full"
+desc = """
+This is a complete example project that demonstrates how to develop a todo list application using Mingling
+"""
+tags = ["todolist", "CRUD"]
+files = ["src/main.rs", "src/todolist.rs", "src/help.rs", "Cargo.toml"]
diff --git a/examples/full-todolist/src/help.rs b/examples/full-todolist/src/help.rs
new file mode 100644
index 0000000..ab33176
--- /dev/null
+++ b/examples/full-todolist/src/help.rs
@@ -0,0 +1,93 @@
+//! This module provides help information for the `todolist` command line program
+
+use mingling::macros::{help, r_println};
+
+use crate::{EntryAdd, EntryClean, EntryComplete, EntryList, ErrorDispatcherNotFound};
+
+#[help]
+pub fn help_global(_p: ErrorDispatcherNotFound) {
+ r_println!(
+ "{}",
+ r"
+Usage: todolist [command] [args]
+
+Commands:
+ add -- Add a new task
+ list -- List all tasks
+ complete -- Mark a task as complete
+ clean -- Clean up completed tasks
+
+Args:
+ -h, --help -- Show this help message
+ -V, --version -- Show the version
+ -A, --all -- All tasks (Clean all / List all)
+ "
+ .trim()
+ );
+}
+
+#[help]
+pub fn help_add(_p: EntryAdd) {
+ r_println!(
+ "{}",
+ r"
+Usage: todolist add [task description]
+
+Add a new task to the todo list.
+
+Example:
+ todolist add 'Buy groceries'
+ todolist add 'Finish Rust project'
+ "
+ .trim()
+ );
+}
+
+#[help]
+pub fn help_list(_p: EntryList) {
+ r_println!(
+ "{}",
+ r"
+Usage: todolist list
+
+List all tasks.
+
+Example:
+ todolist list
+ "
+ .trim()
+ );
+}
+
+#[help]
+pub fn help_complete(_p: EntryComplete) {
+ r_println!(
+ "{}",
+ r"
+Usage: todolist complete [task_id]
+
+Mark a task as complete by its ID.
+
+Example:
+ todolist complete 1
+ todolist complete 3
+ "
+ .trim()
+ );
+}
+
+#[help]
+pub fn help_clean(_p: EntryClean) {
+ r_println!(
+ "{}",
+ r"
+Usage: todolist clean
+
+Remove all completed tasks from the list.
+
+Example:
+ todolist clean
+ "
+ .trim()
+ );
+}
diff --git a/examples/full-todolist/src/main.rs b/examples/full-todolist/src/main.rs
new file mode 100644
index 0000000..fdac1c7
--- /dev/null
+++ b/examples/full-todolist/src/main.rs
@@ -0,0 +1,197 @@
+//! Full Example - Todo List
+//!
+//! This example introduces how to use Mingling's features to build a complete CLI program
+//!
+//! > HAHA:
+//! >
+//! > This is truly a cliché example, as common as `Hello World`!
+
+use mingling::{
+ macros::route,
+ prelude::*,
+ res::ResExitCode,
+ setup::{ExitCodeSetup, GeneralRendererSetup, HelpFlagSetup},
+ LazyInit, LazyRes,
+};
+
+mod help;
+pub use help::*;
+
+mod todolist;
+pub use todolist::*;
+
+#[derive(Default, Clone)]
+pub struct ResProgramFlags {
+ pub all: bool,
+}
+
+// Define dispatchers
+
+dispatcher!("add");
+dispatcher!("list");
+dispatcher!("complete");
+dispatcher!("clean");
+
+// Define states
+
+pack!(StateAddTodo = String);
+pack!(StateCompleteTodo = i32);
+pack!(StateListTodo = bool);
+
+// Define errors
+
+pack!(ErrorNoTaskDescriptionProvided = ());
+pack!(ErrorNoIndexProvided = ());
+pack!(ErrorIndexOutOfBounds = ());
+
+fn main() {
+ let mut program = ThisProgram::new();
+
+ // Setups
+ program.with_setup(ExitCodeSetup::default());
+ program.with_setup(GeneralRendererSetup);
+ program.with_setup(HelpFlagSetup::new(["--help", "-h"]));
+
+ // Flags
+ let all = program.pick_global_flag(["-A", "--all"]);
+
+ // Resources
+ program.with_resource(
+ // Load on use
+ ResTodoList::lazy_init(read_todo_list)
+ // Write on drop
+ .with_on_drop(write_todo_list),
+ );
+ program.with_resource(ResProgramFlags { all });
+
+ // Dispatchers
+ program.with_dispatcher(CMDAdd);
+ program.with_dispatcher(CMDComplete);
+ program.with_dispatcher(CMDList);
+ program.with_dispatcher(CMDClean);
+
+ program.exec_and_exit();
+}
+
+// Command `add`
+
+#[chain]
+fn handle_add(args: EntryAdd) -> Next {
+ let task: String = route! {
+ args
+ .pick_or_route((), ErrorNoTaskDescriptionProvided::new(()).to_render())
+ .unpack()
+ };
+ StateAddTodo::new(task).to_chain()
+}
+
+#[chain]
+fn handle_state_add_todo(
+ state: StateAddTodo,
+ // Inject ResTodoList resource
+ todolist: &mut LazyRes<ResTodoList>,
+) -> Next {
+ let todolist = todolist.get_mut();
+
+ // Unpack state and read description
+ let description = state.inner;
+
+ todolist.items.push(Todo {
+ item: description,
+ completed: false,
+ });
+
+ // Clone todolist to render
+ let todolist = todolist.clone();
+ todolist.to_render()
+}
+
+// Command `list`
+
+#[chain]
+fn handle_list(_args: EntryList, todolist: &mut LazyRes<ResTodoList>) -> Next {
+ let todolist = todolist.get_mut().clone();
+ todolist.to_render()
+}
+
+// Command `complete`
+
+#[chain]
+fn handle_complete(args: EntryComplete) -> Next {
+ let index: i32 = route! {
+ args.pick_or_route((), ErrorNoIndexProvided::new(()).to_render()).unpack()
+ };
+ StateCompleteTodo::new(index).to_chain()
+}
+
+#[chain]
+fn handle_state_complete_todo(
+ state: StateCompleteTodo,
+ todolist: &mut LazyRes<ResTodoList>,
+) -> Next {
+ let todolist = todolist.get_mut();
+ let index = state.inner as usize;
+ if index < todolist.items.len() {
+ todolist.items[index].completed = true;
+ todolist.clone().to_render()
+ } else {
+ ErrorIndexOutOfBounds::new(()).to_render()
+ }
+}
+
+// Command `clean`
+
+#[chain]
+fn handle_clean(
+ _args: EntryClean,
+ todolist: &mut LazyRes<ResTodoList>,
+ program_flags: &ResProgramFlags,
+) -> Next {
+ let todolist = todolist.get_mut();
+ if program_flags.all {
+ todolist.items.clear();
+ } else {
+ todolist.items.retain(|item| !item.completed);
+ }
+ if todolist.items.is_empty() {
+ empty_result!()
+ } else {
+ todolist.clone().to_render()
+ }
+}
+
+#[renderer]
+fn render_error_no_task_description_provided(
+ _err: ErrorNoTaskDescriptionProvided,
+ // ExitCode
+ ec: &mut ResExitCode,
+) {
+ r_println!("No task description provided!");
+ r_println!("");
+ r_println!("Use `todolist add <desc>` to add a task");
+ ec.exit_code = 1;
+}
+
+#[renderer]
+fn render_error_no_index_provided(
+ _err: ErrorNoIndexProvided,
+ // ExitCode
+ ec: &mut ResExitCode,
+) {
+ r_println!("No index provided!");
+ r_println!("");
+ r_println!("Use `todolist list` to query indexes");
+ ec.exit_code = 2;
+}
+
+#[renderer]
+fn render_error_index_out_of_bounds(
+ _err: ErrorIndexOutOfBounds,
+ // ExitCode
+ ec: &mut ResExitCode,
+) {
+ r_println!("Index out of bounds!");
+ ec.exit_code = 3;
+}
+
+gen_program!();
diff --git a/examples/full-todolist/src/todolist.rs b/examples/full-todolist/src/todolist.rs
new file mode 100644
index 0000000..e447f5d
--- /dev/null
+++ b/examples/full-todolist/src/todolist.rs
@@ -0,0 +1,66 @@
+//! Data structures, read and write logic for the todo list
+
+use mingling::{
+ macros::{r_println, renderer},
+ Groupped,
+};
+use serde::{Deserialize, Serialize};
+use std::{env::current_dir, path::PathBuf};
+
+use crate::ResProgramFlags;
+
+#[derive(Debug, Serialize, Deserialize, Clone, Default, Groupped)]
+pub struct ResTodoList {
+ pub items: Vec<Todo>,
+}
+
+#[derive(Debug, Serialize, Deserialize, Clone, Default)]
+pub struct Todo {
+ pub item: String,
+ pub completed: bool,
+}
+
+fn get_todo_list_path() -> PathBuf {
+ current_dir().unwrap().join(".todo.json")
+}
+
+pub fn read_todo_list() -> ResTodoList {
+ let path = get_todo_list_path();
+ if !path.exists() {
+ return ResTodoList::default();
+ }
+ let file = match std::fs::File::open(&path) {
+ Ok(f) => f,
+ Err(_) => return ResTodoList::default(),
+ };
+ let reader = std::io::BufReader::new(file);
+ serde_json::from_reader(reader).unwrap_or_default()
+}
+
+#[renderer]
+pub fn render_res_todo_list(todo_list: ResTodoList, program_flags: &ResProgramFlags) {
+ let mut idx = 0;
+ r_println!("TODO: ");
+ for item in &todo_list.items {
+ if item.completed && !program_flags.all {
+ idx += 1;
+ continue;
+ }
+ r_println!(
+ " {idx}. [{}] - \"{}\"",
+ if item.completed { "x" } else { " " },
+ item.item
+ );
+ idx += 1;
+ }
+}
+
+pub fn write_todo_list(todo_list: ResTodoList) {
+ let path = get_todo_list_path();
+ if let Some(parent) = path.parent() {
+ std::fs::create_dir_all(parent).unwrap();
+ }
+ let file = std::fs::File::create(path).unwrap();
+ let writer = std::io::BufWriter::new(file);
+ serde_json::to_writer(writer, &todo_list).unwrap();
+}