feat: use enum_dispatch for command dispatch and add Open command

Replace manual dynamic dispatch using Box <dyn Runnable> with enum_dispatch
for improved performance and cleaner code.

Add a new Open command to allow opening the TUI as the default behavior
when no subcommand is provided.

Co-Authored-By: fiddlerwoaroof/git-smart-commit (unsloth/gemma-4-26B-A4B-it-GGUF:UD-Q5_K_XL)
This commit is contained in:
Edward Langley
2026-04-08 10:00:41 -07:00
parent a3a74d2787
commit cb24cce1f0
4 changed files with 30 additions and 22 deletions

13
Cargo.lock generated
View File

@ -339,6 +339,18 @@ version = "1.15.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719"
[[package]]
name = "enum_dispatch"
version = "0.3.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "aa18ce2bc66555b3218614519ac839ddb759a7d6720732f979ef8d13be147ecd"
dependencies = [
"once_cell",
"proc-macro2",
"quote",
"syn",
]
[[package]] [[package]]
name = "equivalent" name = "equivalent"
version = "1.0.2" version = "1.0.2"
@ -494,6 +506,7 @@ dependencies = [
"crossterm", "crossterm",
"csv", "csv",
"dirs", "dirs",
"enum_dispatch",
"flate2", "flate2",
"indexmap", "indexmap",
"proptest", "proptest",

View File

@ -23,6 +23,7 @@ unicode-width = "0.2"
dirs = "5" dirs = "5"
csv = "1" csv = "1"
clap = { version = "4.6.0", features = ["derive"] } clap = { version = "4.6.0", features = ["derive"] }
enum_dispatch = "0.3.13"
[dev-dependencies] [dev-dependencies]
proptest = "1" proptest = "1"

View File

@ -42,6 +42,7 @@
pkgs.pkg-config pkgs.pkg-config
pkgs.rust-analyzer pkgs.rust-analyzer
crate2nix.packages.${system}.default crate2nix.packages.${system}.default
pkgs.cargo-expand
]; ];
RUST_BACKTRACE = "1"; RUST_BACKTRACE = "1";
}; };

View File

@ -14,6 +14,7 @@ use std::path::PathBuf;
use anyhow::{Context, Result}; use anyhow::{Context, Result};
use clap::{Parser, Subcommand}; use clap::{Parser, Subcommand};
use enum_dispatch::enum_dispatch;
use draw::run_tui; use draw::run_tui;
use model::Model; use model::Model;
@ -29,7 +30,13 @@ struct Cli {
command: Option<Commands>, command: Option<Commands>,
} }
#[enum_dispatch]
trait Runnable {
fn run(self, model_file: Option<PathBuf>) -> Result<()>;
}
#[derive(Subcommand)] #[derive(Subcommand)]
#[enum_dispatch(Runnable)]
enum Commands { enum Commands {
/// Import JSON or CSV data, then open TUI (or save with --output) /// Import JSON or CSV data, then open TUI (or save with --output)
Import(ImportArgs), Import(ImportArgs),
@ -37,6 +44,8 @@ enum Commands {
Cmd(CmdArgs), Cmd(CmdArgs),
/// Run commands from a script file headless /// Run commands from a script file headless
Script(ScriptArgs), Script(ScriptArgs),
/// Open the TUI (default when no subcommand given)
Open(OpenTui),
} }
#[derive(clap::Args)] #[derive(clap::Args)]
@ -105,23 +114,10 @@ struct ScriptArgs {
file: Option<PathBuf>, file: Option<PathBuf>,
} }
trait Runnable { #[derive(clap::Args)]
fn run(self: Box<Self>, model_file: Option<PathBuf>) -> Result<()>;
}
impl From<Commands> for Box<dyn Runnable> {
fn from(cmd: Commands) -> Self {
match cmd {
Commands::Import(args) => Box::new(args),
Commands::Cmd(args) => Box::new(args),
Commands::Script(args) => Box::new(args),
}
}
}
struct OpenTui; struct OpenTui;
impl Runnable for OpenTui { impl Runnable for OpenTui {
fn run(self: Box<Self>, model_file: Option<PathBuf>) -> Result<()> { fn run(self, model_file: Option<PathBuf>) -> Result<()> {
let model = get_initial_model(&model_file)?; let model = get_initial_model(&model_file)?;
run_tui(model, model_file, None) run_tui(model, model_file, None)
} }
@ -129,15 +125,12 @@ impl Runnable for OpenTui {
fn main() -> Result<()> { fn main() -> Result<()> {
let cli = Cli::parse(); let cli = Cli::parse();
let cmd: Box<dyn Runnable> = cli let cmd = cli.command.unwrap_or(Commands::Open(OpenTui));
.command
.map(|c| -> Box<dyn Runnable> { c.into() })
.unwrap_or_else(|| Box::new(OpenTui));
cmd.run(cli.file) cmd.run(cli.file)
} }
impl Runnable for ImportArgs { impl Runnable for ImportArgs {
fn run(self: Box<Self>, model_file: Option<PathBuf>) -> Result<()> { fn run(self, model_file: Option<PathBuf>) -> Result<()> {
if self.files.is_empty() { if self.files.is_empty() {
anyhow::bail!("No files specified for import"); anyhow::bail!("No files specified for import");
} }
@ -164,13 +157,13 @@ impl Runnable for ImportArgs {
} }
impl Runnable for CmdArgs { impl Runnable for CmdArgs {
fn run(self: Box<Self>, _model_file: Option<PathBuf>) -> Result<()> { fn run(self, _model_file: Option<PathBuf>) -> Result<()> {
run_headless_commands(&self.json, &self.file) run_headless_commands(&self.json, &self.file)
} }
} }
impl Runnable for ScriptArgs { impl Runnable for ScriptArgs {
fn run(self: Box<Self>, _model_file: Option<PathBuf>) -> Result<()> { fn run(self, _model_file: Option<PathBuf>) -> Result<()> {
run_headless_script(&self.path, &self.file) run_headless_script(&self.path, &self.file)
} }
} }