summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
author魏曹先生 <1992414357@qq.com>2026-01-22 08:32:29 +0800
committer魏曹先生 <1992414357@qq.com>2026-01-22 08:32:29 +0800
commitaca8b408755f9041da9ee083c625de2a8d8c6785 (patch)
tree5747d389d5218ccf39e2153ae1346f7b5bfe8fb8
parent0d614f3e2104e9b840ebc7e53a6caa6af1671636 (diff)
Refactor CLI command processing with new architecture
-rw-r--r--.cargo/config.toml7
-rw-r--r--.gitignore1
-rw-r--r--Cargo.lock1
-rw-r--r--Cargo.toml5
-rw-r--r--Registry.toml3
-rw-r--r--build.rs104
-rw-r--r--resources/locales/jvn/en.yml35
-rw-r--r--resources/locales/jvn/zh-CN.yml34
-rw-r--r--src/bin/jvn.rs119
-rw-r--r--src/lib.rs3
-rw-r--r--src/subcmd.rs5
-rw-r--r--src/subcmd/cmd.rs85
-rw-r--r--src/subcmd/cmds.rs2
-rw-r--r--src/subcmd/cmds/status.rs45
-rw-r--r--src/subcmd/cmds/template.rs45
-rw-r--r--src/subcmd/errors.rs107
-rw-r--r--src/subcmd/processer.rs40
-rw-r--r--src/subcmd/renderer.rs54
-rw-r--r--templates/_registry.rs.template25
19 files changed, 708 insertions, 12 deletions
diff --git a/.cargo/config.toml b/.cargo/config.toml
index ca6a8cb..f79321e 100644
--- a/.cargo/config.toml
+++ b/.cargo/config.toml
@@ -4,6 +4,7 @@ target-dir = "./.temp/target/"
[publish]
target-dir = "./.temp/deploy/"
binaries = [
+ # The legacy command line
"jv",
"jv.exe",
@@ -11,7 +12,11 @@ binaries = [
"jvv.exe",
"jvii",
- "jvii.exe"
+ "jvii.exe",
+
+ # The new command line
+ "jvn",
+ "jvn.exe"
]
############
diff --git a/.gitignore b/.gitignore
index adb64ee..fd44ea1 100644
--- a/.gitignore
+++ b/.gitignore
@@ -23,3 +23,4 @@
# Generated from templates
/src/data/compile_info.rs
/setup/windows/setup_jv_cli.iss
+/src/subcmd/cmds/_registry.rs
diff --git a/Cargo.lock b/Cargo.lock
index 34542d7..20b0ee1 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -1010,6 +1010,7 @@ dependencies = [
"serde",
"serde_json",
"strip-ansi-escapes",
+ "thiserror",
"tokio",
"toml 0.9.8",
"walkdir",
diff --git a/Cargo.toml b/Cargo.toml
index 2a0bb00..5b66066 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -3,7 +3,7 @@ name = "just_enough_vcs_cli"
edition = "2024"
build = "build.rs"
authors = ["JustEnoughVCS Team"]
-homepage = "http://jvcs.cc/"
+homepage = "https://github.com/JustEnoughVCS/CommandLine/"
[workspace]
members = ["tools/build_helper"]
@@ -38,6 +38,9 @@ toml = "0.9"
# Just Enough VCS
just_enough_vcs = { path = "../VersionControl", features = ["all"] }
+# Error
+thiserror = "2.0.17"
+
# Serialize
serde = { version = "1", features = ["derive"] }
serde_json = "1"
diff --git a/Registry.toml b/Registry.toml
new file mode 100644
index 0000000..be1fab9
--- /dev/null
+++ b/Registry.toml
@@ -0,0 +1,3 @@
+[status]
+node = "status"
+type = "subcmd::cmds::status::JVStatusCommand"
diff --git a/build.rs b/build.rs
index 75e5cc9..2cb7b77 100644
--- a/build.rs
+++ b/build.rs
@@ -8,6 +8,10 @@ const COMPILE_INFO_RS_TEMPLATE: &str = "./templates/compile_info.rs";
const SETUP_JV_CLI_ISS: &str = "./setup/windows/setup_jv_cli.iss";
const SETUP_JV_CLI_ISS_TEMPLATE: &str = "./templates/setup_jv_cli.iss";
+const REGISTRY_RS: &str = "./src/subcmd/cmds/_registry.rs";
+const REGISTRY_RS_TEMPLATE: &str = "./templates/_registry.rs.template";
+const REGISTRY_TOML: &str = "./Registry.toml";
+
fn main() {
println!("cargo:rerun-if-env-changed=FORCE_BUILD");
@@ -25,6 +29,11 @@ fn main() {
eprintln!("Failed to generate compile info: {}", e);
std::process::exit(1);
}
+
+ if let Err(e) = generate_registry_file(&repo_root) {
+ eprintln!("Failed to generate registry file: {}", e);
+ std::process::exit(1);
+ }
}
/// Generate Inno Setup installer script (Windows only)
@@ -212,3 +221,98 @@ fn get_git_commit() -> Result<String, Box<dyn std::error::Error>> {
Err("Failed to get git commit".into())
}
+
+/// Generate registry file from Registry.toml configuration
+fn generate_registry_file(repo_root: &PathBuf) -> Result<(), Box<dyn std::error::Error>> {
+ let template_path = repo_root.join(REGISTRY_RS_TEMPLATE);
+ let output_path = repo_root.join(REGISTRY_RS);
+ let config_path = repo_root.join(REGISTRY_TOML);
+
+ // Read the template
+ let template = std::fs::read_to_string(&template_path)?;
+
+ // Read and parse the TOML configuration
+ let config_content = std::fs::read_to_string(&config_path)?;
+ let config: toml::Value = toml::from_str(&config_content)?;
+
+ // Collect all command configurations
+ let mut commands = Vec::new();
+ let mut nodes = Vec::new();
+
+ if let Some(table) = config.as_table() {
+ for (key, value) in table {
+ if let Some(cmd_table) = value.as_table() {
+ if let (Some(node), Some(cmd_type)) = (
+ cmd_table.get("node").and_then(|v| v.as_str()),
+ cmd_table.get("type").and_then(|v| v.as_str()),
+ ) {
+ let n = node.replace(".", " ");
+ nodes.push(n.clone());
+ commands.push((key.to_string(), n, cmd_type.to_string()));
+ }
+ }
+ }
+ }
+
+ // Extract the node_if template from the template content
+ const PROCESS_MARKER: &str = "// PROCESS";
+ const TEMPLATE_START: &str = "// -- TEMPLATE START --";
+ const TEMPLATE_END: &str = "// -- TEMPLATE END --";
+ const LINE: &str = "<<LINE>>";
+ const NODES: &str = "<<NODES>>";
+
+ let template_start_index = template
+ .find(TEMPLATE_START)
+ .ok_or("Template start marker not found")?;
+ let template_end_index = template
+ .find(TEMPLATE_END)
+ .ok_or("Template end marker not found")?;
+
+ let template_slice = &template[template_start_index..template_end_index + TEMPLATE_END.len()];
+ let node_if_template = template_slice
+ .trim_start_matches(TEMPLATE_START)
+ .trim_end_matches(TEMPLATE_END)
+ .trim_matches('\n');
+
+ // Generate the match arms for each command
+ let match_arms: String = commands
+ .iter()
+ .map(|(key, node, cmd_type)| {
+ node_if_template
+ .replace("<<KEY>>", key)
+ .replace("<<NODE_NAME>>", node)
+ .replace("<<COMMAND_TYPE>>", cmd_type)
+ .trim_matches('\n')
+ .to_string()
+ })
+ .collect::<Vec<_>>()
+ .join("\n");
+
+ let nodes_str = format!(
+ "[\n {}\n ]",
+ nodes
+ .iter()
+ .map(|node| format!("\"{}\".to_string()", node))
+ .collect::<Vec<_>>()
+ .join(", ")
+ );
+
+ // Replace the template section with the generated match arms
+ let final_content = template
+ .replace(node_if_template, "")
+ .replace(TEMPLATE_START, "")
+ .replace(TEMPLATE_END, "")
+ .replace(PROCESS_MARKER, &match_arms)
+ .lines()
+ .filter(|line| !line.trim().is_empty())
+ .collect::<Vec<_>>()
+ .join("\n")
+ .replace(LINE, "")
+ .replace(NODES, nodes_str.as_str());
+
+ // Write the generated code
+ std::fs::write(output_path, final_content)?;
+
+ println!("Generated registry file with {} commands", commands.len());
+ Ok(())
+}
diff --git a/resources/locales/jvn/en.yml b/resources/locales/jvn/en.yml
new file mode 100644
index 0000000..525d8c5
--- /dev/null
+++ b/resources/locales/jvn/en.yml
@@ -0,0 +1,35 @@
+process_error:
+ prepare_error: |
+ [[YELLOW]]Preparation Phase Error:[[/]]
+ %{error}
+ execute_error: |
+ [[RED]]Execution Phase Error:[[/]]
+ %{error}
+ render_error: |
+ [[YELLOW]]Rendering Phase Error:[[/]]
+ %{error}
+
+ Tip: If you need to ignore error output,
+ please append the `--no-error-logs` parameter to the command.
+
+ no_matching_command: |
+ No matching command found!
+
+ ambiguous_command: |
+ Multiple commands found, unable to determine which one you want:
+ %{nodes}
+
+ no_node_found: |
+ Unable to find command `%{node}`
+
+ This is usually a compilation-phase issue, not a user input error.
+ Please use `jv -v -C` to get detailed version traceback and contact the developers.
+
+ github: https://github.com/JustEnoughVCS/CommandLine/
+
+ parse_error: |
+ An error occurred while parsing your command arguments!
+ Please use `jv <command> --help` to view help
+
+ other: |
+ %{error}
diff --git a/resources/locales/jvn/zh-CN.yml b/resources/locales/jvn/zh-CN.yml
new file mode 100644
index 0000000..ffb033c
--- /dev/null
+++ b/resources/locales/jvn/zh-CN.yml
@@ -0,0 +1,34 @@
+process_error:
+ prepare_error: |
+ [[YELLOW]]准备阶段错误:[[/]]
+ %{error}
+ execute_error: |
+ [[RED]]执行阶段错误:[[/]]
+ %{error}
+ render_error: |
+ [[YELLOW]]渲染阶段错误:[[/]]
+ %{error}
+
+ 提示:若您需要忽略错误输出,请在命令后追加 `--no-error-logs` 参数
+
+ no_matching_command: |
+ 无法找到匹配的命令!
+
+ ambiguous_command: |
+ 找到多个命令,无法确定您想要哪一个:
+ %{nodes}
+
+ no_node_found: |
+ 无法找到命令 `%{node}`
+
+ 这通常是编译期造成的问题,而非用户的错误输入
+ 请使用 `jv -v -C` 获得详细的版本追溯,并联系开发人员
+
+ github: https://github.com/JustEnoughVCS/CommandLine/
+
+ parse_error: |
+ 在将您的命令参数进行解析时出现错误!
+ 请使用 `jv <命令> --help` 查看帮助
+
+ other: |
+ %{error}
diff --git a/src/bin/jvn.rs b/src/bin/jvn.rs
index cc93fb1..86e3421 100644
--- a/src/bin/jvn.rs
+++ b/src/bin/jvn.rs
@@ -1,24 +1,123 @@
-use just_enough_vcs_cli::{subcmd::cmds::_processer::jv_cmd_process, utils::env::current_locales};
-use rust_i18n::set_locale;
+use just_enough_vcs_cli::subcmd::cmd::JVCommandContext;
+use just_enough_vcs_cli::utils::display::md;
+use just_enough_vcs_cli::{
+ subcmd::{errors::CmdProcessError, processer::jv_cmd_process},
+ utils::env::current_locales,
+};
+use rust_i18n::{set_locale, t};
-rust_i18n::i18n!("resources/locales/jv", fallback = "en");
+rust_i18n::i18n!("resources/locales/jvn", fallback = "en");
+
+macro_rules! special_flag {
+ ($args:expr, $flag:expr) => {{
+ let flag = $flag;
+ let found = $args.iter().any(|arg| arg == flag);
+ $args.retain(|arg| arg != flag);
+ found
+ }};
+}
+
+macro_rules! special_argument {
+ ($args:expr, $flag:expr) => {{
+ let flag = $flag;
+ let mut value: Option<String> = None;
+ let mut i = 0;
+ while i < $args.len() {
+ if $args[i] == flag {
+ if i + 1 < $args.len() {
+ value = Some($args[i + 1].clone());
+ $args.remove(i + 1);
+ $args.remove(i);
+ } else {
+ value = None;
+ $args.remove(i);
+ }
+ break;
+ }
+ i += 1;
+ }
+ value
+ }};
+}
#[tokio::main]
-async fn main() -> Result<(), Box<dyn std::error::Error>> {
+async fn main() {
+ // Collect arguments
+ let mut args: Vec<String> = std::env::args().skip(1).collect();
+
// Init i18n
- set_locale(&current_locales());
+ let lang = special_argument!(args, "--lang").unwrap_or(current_locales());
+ set_locale(&lang);
// Init colored
#[cfg(windows)]
colored::control::set_virtual_terminal(true).unwrap();
- // Collect arguments
- let args: Vec<String> = std::env::args().collect();
+ let no_error_logs = special_flag!(args, "--no-error-logs");
+ let quiet = special_flag!(args, "--quiet") || special_flag!(args, "-q");
+ let help = special_flag!(args, "--help") || special_flag!(args, "-h");
+ let confirmed = special_flag!(args, "--confirm") || special_flag!(args, "-C");
// Process commands
- let render_result = jv_cmd_process(args).await.unwrap_or_default();
+ let render_result = match jv_cmd_process(args, JVCommandContext { help, confirmed }).await {
+ Ok(result) => result,
+ Err(e) => {
+ if !no_error_logs {
+ match e {
+ CmdProcessError::Prepare(cmd_prepare_error) => {
+ eprintln!(
+ "{}",
+ md(t!("process_error.prepare_error", error = cmd_prepare_error))
+ );
+ }
+ CmdProcessError::Execute(cmd_execute_error) => {
+ eprintln!(
+ "{}",
+ md(t!("process_error.execute_error", error = cmd_execute_error))
+ );
+ }
+ CmdProcessError::Render(cmd_render_error) => {
+ eprintln!(
+ "{}",
+ md(t!("process_error.render_error", error = cmd_render_error))
+ );
+ }
+ CmdProcessError::Error(error) => {
+ eprintln!("{}", md(t!("process_error.other", error = error)));
+ }
+ CmdProcessError::NoNodeFound(node) => {
+ eprintln!("{}", md(t!("process_error.no_node_found", node = node)));
+ }
+ CmdProcessError::NoMatchingCommand => {
+ eprintln!("{}", md(t!("process_error.no_matching_command")));
+ }
+ CmdProcessError::AmbiguousCommand(nodes) => {
+ let nodes_list = nodes
+ .iter()
+ .enumerate()
+ .map(|(i, node)| format!("{}. {}", i + 1, node))
+ .collect::<Vec<String>>()
+ .join("\n");
+ eprintln!(
+ "{}",
+ md(t!("process_error.ambiguous_command", nodes = nodes_list))
+ );
+ }
+ CmdProcessError::ParseError(help) => {
+ if help.trim().len() < 1 {
+ eprintln!("{}", md(t!("process_error.parse_error")));
+ } else {
+ eprintln!("{}", help)
+ }
+ }
+ }
+ }
+ std::process::exit(1);
+ }
+ };
// Print
- print!("{}", render_result);
- Ok(())
+ if !quiet {
+ print!("{}", render_result);
+ }
}
diff --git a/src/lib.rs b/src/lib.rs
index a0e7a53..2a06e0d 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -6,3 +6,6 @@ pub mod data;
/// Json Format
pub mod output;
+
+/// Command
+pub mod subcmd;
diff --git a/src/subcmd.rs b/src/subcmd.rs
new file mode 100644
index 0000000..8c2f733
--- /dev/null
+++ b/src/subcmd.rs
@@ -0,0 +1,5 @@
+pub mod cmd;
+pub mod cmds;
+pub mod errors;
+pub mod processer;
+pub mod renderer;
diff --git a/src/subcmd/cmd.rs b/src/subcmd/cmd.rs
new file mode 100644
index 0000000..10ad893
--- /dev/null
+++ b/src/subcmd/cmd.rs
@@ -0,0 +1,85 @@
+use serde::Serialize;
+
+use crate::{
+ r_println,
+ subcmd::{
+ errors::{CmdExecuteError, CmdPrepareError, CmdProcessError},
+ renderer::{JVRenderResult, JVResultRenderer},
+ },
+};
+use std::future::Future;
+
+pub struct JVCommandContext {
+ pub help: bool,
+ pub confirmed: bool,
+}
+
+pub trait JVCommand<Argument, Input, Output, Renderer>
+where
+ Argument: clap::Parser + Send + Sync,
+ Input: Send + Sync,
+ Output: Serialize + Send + Sync,
+ Renderer: JVResultRenderer<Output> + Send + Sync,
+{
+ /// Get help string for the command
+ fn get_help_str() -> String;
+
+ /// performing any necessary post-execution processing
+ fn process(
+ args: Vec<String>,
+ ctx: JVCommandContext,
+ ) -> impl Future<Output = Result<JVRenderResult, CmdProcessError>> + Send + Sync
+ where
+ Self: Sync,
+ {
+ Self::process_with_renderer::<Renderer>(args, ctx)
+ }
+
+ /// Process the command output with a custom renderer,
+ /// performing any necessary post-execution processing
+ fn process_with_renderer<R: JVResultRenderer<Output> + Send + Sync>(
+ args: Vec<String>,
+ ctx: JVCommandContext,
+ ) -> impl Future<Output = Result<JVRenderResult, CmdProcessError>> + Send + Sync
+ where
+ Self: Sync,
+ {
+ async move {
+ let mut full_args = vec!["jv".to_string()];
+ full_args.extend(args);
+ let parsed_args = match Argument::try_parse_from(full_args) {
+ Ok(args) => args,
+ Err(_) => return Err(CmdProcessError::ParseError(Self::get_help_str())),
+ };
+ // If the help flag is used, skip execution and directly print help
+ if ctx.help {
+ let mut r = JVRenderResult::default();
+ r_println!(r, "{}", Self::get_help_str());
+ return Ok(r);
+ }
+ let input = match Self::prepare(parsed_args, ctx).await {
+ Ok(input) => input,
+ Err(e) => return Err(CmdProcessError::from(e)),
+ };
+ let output = match Self::exec(input).await {
+ Ok(output) => output,
+ Err(e) => return Err(CmdProcessError::from(e)),
+ };
+ match R::render(&output).await {
+ Ok(r) => Ok(r),
+ Err(e) => Err(CmdProcessError::from(e)),
+ }
+ }
+ }
+
+ /// Prepare to run the command,
+ /// converting Clap input into the command's supported input
+ fn prepare(
+ args: Argument,
+ ctx: JVCommandContext,
+ ) -> impl Future<Output = Result<Input, CmdPrepareError>> + Send + Sync;
+
+ /// Run the command phase,
+ /// returning an output structure, waiting for rendering
+ fn exec(args: Input) -> impl Future<Output = Result<Output, CmdExecuteError>> + Send + Sync;
+}
diff --git a/src/subcmd/cmds.rs b/src/subcmd/cmds.rs
new file mode 100644
index 0000000..e06480c
--- /dev/null
+++ b/src/subcmd/cmds.rs
@@ -0,0 +1,2 @@
+pub mod _registry;
+pub mod status;
diff --git a/src/subcmd/cmds/status.rs b/src/subcmd/cmds/status.rs
new file mode 100644
index 0000000..2b92df6
--- /dev/null
+++ b/src/subcmd/cmds/status.rs
@@ -0,0 +1,45 @@
+use clap::Parser;
+use serde::Serialize;
+
+use crate::subcmd::{
+ cmd::{JVCommand, JVCommandContext},
+ errors::{CmdExecuteError, CmdPrepareError, CmdRenderError},
+ renderer::{JVRenderResult, JVResultRenderer},
+};
+
+pub struct JVStatusCommand;
+
+#[derive(Parser, Debug)]
+pub struct JVStatusArgument;
+
+pub struct JVStatusInput;
+
+#[derive(Serialize)]
+pub struct JVStatusOutput;
+
+impl JVCommand<JVStatusArgument, JVStatusInput, JVStatusOutput, JVStatusRenderer>
+ for JVStatusCommand
+{
+ async fn prepare(
+ args: JVStatusArgument,
+ ctx: JVCommandContext,
+ ) -> Result<JVStatusInput, CmdPrepareError> {
+ todo!()
+ }
+
+ async fn exec(args: JVStatusInput) -> Result<JVStatusOutput, CmdExecuteError> {
+ todo!()
+ }
+
+ fn get_help_str() -> String {
+ "".to_string()
+ }
+}
+
+pub struct JVStatusRenderer;
+
+impl JVResultRenderer<JVStatusOutput> for JVStatusRenderer {
+ async fn render(data: &JVStatusOutput) -> Result<JVRenderResult, CmdRenderError> {
+ todo!()
+ }
+}
diff --git a/src/subcmd/cmds/template.rs b/src/subcmd/cmds/template.rs
new file mode 100644
index 0000000..8874121
--- /dev/null
+++ b/src/subcmd/cmds/template.rs
@@ -0,0 +1,45 @@
+use clap::Parser;
+use serde::Serialize;
+
+use crate::subcmd::{
+ cmd::JVCommand,
+ errors::{CmdExecuteError, CmdPrepareError, CmdRenderError},
+ renderer::{JVRenderResult, JVResultRenderer},
+};
+
+pub struct JVUnknownCommand;
+
+#[derive(Parser, Debug)]
+pub struct JVUnknownArgument;
+
+pub struct JVUnknownInput;
+
+#[derive(Serialize)]
+pub struct JVUnknownOutput;
+
+impl JVCommand<JVUnknownArgument, JVUnknownInput, JVUnknownOutput, JVStatusRenderer>
+ for JVUnknownCommand
+{
+ async fn prepare(
+ _args: JVUnknownArgument,
+ _ctx: JVCommandContext,
+ ) -> Result<JVUnknownInput, CmdPrepareError> {
+ todo!()
+ }
+
+ async fn exec(_args: JVUnknownInput) -> Result<JVUnknownOutput, CmdExecuteError> {
+ todo!()
+ }
+
+ fn get_help_str() -> String {
+ "".to_string()
+ }
+}
+
+pub struct JVStatusRenderer;
+
+impl JVResultRenderer<JVUnknownOutput> for JVStatusRenderer {
+ async fn render(_data: &JVUnknownOutput) -> Result<JVRenderResult, CmdRenderError> {
+ todo!()
+ }
+}
diff --git a/src/subcmd/errors.rs b/src/subcmd/errors.rs
new file mode 100644
index 0000000..e1cf835
--- /dev/null
+++ b/src/subcmd/errors.rs
@@ -0,0 +1,107 @@
+#[derive(thiserror::Error, Debug)]
+pub enum CmdPrepareError {
+ #[error("IO error: {0}")]
+ Io(#[from] std::io::Error),
+
+ #[error("{0}")]
+ Error(String),
+}
+
+impl CmdPrepareError {
+ pub fn new(msg: impl AsRef<str>) -> Self {
+ CmdPrepareError::Error(msg.as_ref().to_string())
+ }
+}
+
+#[derive(thiserror::Error, Debug)]
+pub enum CmdExecuteError {
+ #[error("IO error: {0}")]
+ Io(#[from] std::io::Error),
+
+ #[error("Content not prepared, cannot run")]
+ Prepare(#[from] CmdPrepareError),
+
+ #[error("{0}")]
+ Error(String),
+}
+
+impl CmdExecuteError {
+ pub fn new(msg: impl AsRef<str>) -> Self {
+ CmdExecuteError::Error(msg.as_ref().to_string())
+ }
+}
+
+#[derive(thiserror::Error, Debug)]
+pub enum CmdRenderError {
+ #[error("IO error: {0}")]
+ Io(#[from] std::io::Error),
+
+ #[error("Preparation failed, cannot render")]
+ Prepare(#[from] CmdPrepareError),
+
+ #[error("Execution failed, no output content obtained before rendering")]
+ Execute(#[from] CmdExecuteError),
+
+ #[error("{0}")]
+ Error(String),
+}
+
+impl CmdRenderError {
+ pub fn new(msg: impl AsRef<str>) -> Self {
+ CmdRenderError::Error(msg.as_ref().to_string())
+ }
+}
+
+#[derive(thiserror::Error, Debug)]
+pub enum CmdProcessError {
+ #[error("Prepare error: {0}")]
+ Prepare(#[from] CmdPrepareError),
+
+ #[error("Execute error: {0}")]
+ Execute(#[from] CmdExecuteError),
+
+ #[error("Render error: {0}")]
+ Render(#[from] CmdRenderError),
+
+ #[error("{0}")]
+ Error(String),
+
+ #[error("Node `{0}` not found!")]
+ NoNodeFound(String),
+
+ #[error("No matching command found")]
+ NoMatchingCommand,
+
+ #[error("Ambiguous command, multiple matches found")]
+ AmbiguousCommand(Vec<String>),
+
+ #[error("Parse error")]
+ ParseError(String),
+}
+
+impl CmdProcessError {
+ pub fn new(msg: impl AsRef<str>) -> Self {
+ CmdProcessError::Error(msg.as_ref().to_string())
+ }
+
+ pub fn prepare_err(&self) -> Option<&CmdPrepareError> {
+ match self {
+ CmdProcessError::Prepare(e) => Some(e),
+ _ => None,
+ }
+ }
+
+ pub fn execute_err(&self) -> Option<&CmdExecuteError> {
+ match self {
+ CmdProcessError::Execute(e) => Some(e),
+ _ => None,
+ }
+ }
+
+ pub fn render_err(&self) -> Option<&CmdRenderError> {
+ match self {
+ CmdProcessError::Render(e) => Some(e),
+ _ => None,
+ }
+ }
+}
diff --git a/src/subcmd/processer.rs b/src/subcmd/processer.rs
new file mode 100644
index 0000000..5849c62
--- /dev/null
+++ b/src/subcmd/processer.rs
@@ -0,0 +1,40 @@
+use crate::subcmd::cmd::JVCommandContext;
+use crate::subcmd::cmds::_registry::{jv_cmd_nodes, jv_cmd_process_node};
+use crate::subcmd::errors::CmdProcessError;
+use crate::subcmd::renderer::JVRenderResult;
+
+pub async fn jv_cmd_process(
+ args: Vec<String>,
+ ctx: JVCommandContext,
+) -> Result<JVRenderResult, CmdProcessError> {
+ let nodes = jv_cmd_nodes();
+ let command = args.join(" ");
+
+ // Find nodes that match the beginning of the command
+ let matching_nodes: Vec<&String> = nodes
+ .iter()
+ .filter(|node| command.starts_with(node.as_str()))
+ .collect();
+
+ match matching_nodes.len() {
+ 0 => {
+ // No matching node found
+ return Err(CmdProcessError::NoMatchingCommand);
+ }
+ 1 => {
+ let matched_prefix = matching_nodes[0];
+ let prefix_len = matched_prefix.split_whitespace().count();
+ let trimmed_args: Vec<String> = args.into_iter().skip(prefix_len).collect();
+ return jv_cmd_process_node(matched_prefix, trimmed_args, ctx).await;
+ }
+ _ => {
+ // Multiple matching nodes found
+ return Err(CmdProcessError::AmbiguousCommand(
+ matching_nodes
+ .iter()
+ .map(|s| s.to_string())
+ .collect::<Vec<String>>(),
+ ));
+ }
+ }
+}
diff --git a/src/subcmd/renderer.rs b/src/subcmd/renderer.rs
new file mode 100644
index 0000000..99a67f1
--- /dev/null
+++ b/src/subcmd/renderer.rs
@@ -0,0 +1,54 @@
+use std::fmt::{Display, Formatter};
+
+use serde::Serialize;
+
+use crate::subcmd::errors::CmdRenderError;
+
+pub trait JVResultRenderer<Data>
+where
+ Data: Serialize,
+{
+ fn render(
+ data: &Data,
+ ) -> impl Future<Output = Result<JVRenderResult, CmdRenderError>> + Send + Sync;
+}
+
+#[derive(Default, Debug, PartialEq)]
+pub struct JVRenderResult {
+ render_text: String,
+}
+
+impl Display for JVRenderResult {
+ fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
+ write!(f, "{}\n", self.render_text.trim())
+ }
+}
+
+impl JVRenderResult {
+ pub fn print(&mut self, text: &str) {
+ self.render_text.push_str(text);
+ }
+
+ pub fn println(&mut self, text: &str) {
+ self.render_text.push_str(text);
+ self.render_text.push('\n');
+ }
+
+ pub fn clear(&mut self) {
+ self.render_text.clear();
+ }
+}
+
+#[macro_export]
+macro_rules! r_print {
+ ($result:expr, $($arg:tt)*) => {
+ $result.print(&format!($($arg)*));
+ };
+}
+
+#[macro_export]
+macro_rules! r_println {
+ ($result:expr, $($arg:tt)*) => {
+ $result.println(&format!($($arg)*));
+ };
+}
diff --git a/templates/_registry.rs.template b/templates/_registry.rs.template
new file mode 100644
index 0000000..0c6d0d5
--- /dev/null
+++ b/templates/_registry.rs.template
@@ -0,0 +1,25 @@
+// Auto generated by build.rs
+use crate::subcmd::cmd::{JVCommand, JVCommandContext};
+use crate::subcmd::errors::CmdProcessError;
+<<LINE>>
+/// Input parameters, execute a command node
+pub async fn jv_cmd_process_node(
+ node: &str,
+ args: Vec<String>,
+ ctx: JVCommandContext
+) -> Result<crate::subcmd::renderer::JVRenderResult, crate::subcmd::errors::CmdProcessError> {
+ match node {
+// PROCESS
+// -- TEMPLATE START --
+ // Command `<<KEY>>`
+ "<<NODE_NAME>>" => return crate::<<COMMAND_TYPE>>::process(args, ctx).await,
+// -- TEMPLATE END --
+ _ => {}
+ }
+ return Err(CmdProcessError::NoNodeFound(node.to_string()));
+}
+<<LINE>>
+/// Get all command nodes
+pub fn jv_cmd_nodes() -> Vec<String> {
+ vec!<<NODES>>
+}