From c75b28d35cc7d92aa3cb9d59ed775b530f5a900c Mon Sep 17 00:00:00 2001 From: DrRac27 Date: Sat, 22 Jun 2024 21:46:24 +0200 Subject: [PATCH] add chapter about CLIs --- Cargo.toml | 1 + examples/cli.rs | 65 ++++++++++++++++++++++++++++++++++++++++++ src/SUMMARY.md | 1 + src/cli.md | 76 +++++++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 143 insertions(+) create mode 100644 examples/cli.rs create mode 100644 src/cli.md diff --git a/Cargo.toml b/Cargo.toml index 21e24d5..f3d27a5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -19,3 +19,4 @@ relm4 = "0.8.0" relm4-components = "0.8.0" tokio = { version = "1.36", features = ["rt", "macros", "time", "rt-multi-thread", "sync"] } tracker = "0.2.1" +clap = { version = "4.5.7", features = ["derive"] } diff --git a/examples/cli.rs b/examples/cli.rs new file mode 100644 index 0000000..97429b9 --- /dev/null +++ b/examples/cli.rs @@ -0,0 +1,65 @@ +/* ANCHOR: all */ +use clap::Parser; +use gtk::prelude::GtkWindowExt; +use relm4::{gtk, ComponentParts, ComponentSender, RelmApp, SimpleComponent}; + +struct AppModel {} + +#[relm4::component] +impl SimpleComponent for AppModel { + type Init = (); + type Input = (); + type Output = (); + + view! { + gtk::Window { + set_title: Some("Hello world with CLI"), + set_default_width: 300, + set_default_height: 100, + + gtk::Label { + set_label: "Hello world!", + } + } + } + + fn init( + _init: Self::Init, + root: Self::Root, + _sender: ComponentSender, + ) -> ComponentParts { + let model = AppModel {}; + let widgets = view_output!(); + + ComponentParts { model, widgets } + } +} + +/* ANCHOR: args_struct */ +#[derive(Parser, Debug)] +#[command(version, about, long_about = None)] +struct Args { + /// some argument to test + #[arg(long)] + non_gtk_arg: bool, + + /// Unknown arguments or everything after -- gets passed through to GTK. + #[arg(allow_hyphen_values = true, trailing_var_arg = true)] + gtk_options: Vec, +} +/* ANCHOR_END: args_struct */ + +fn main() { + let args = Args::parse(); + dbg!(&args); + + /* ANCHOR: main */ + let program_invocation = std::env::args().next().unwrap(); + let mut gtk_args = vec![program_invocation]; + gtk_args.append(&mut args.gtk_options.clone()); + + let app = RelmApp::new("relm4.test.helloworld_cli"); + app.with_args(gtk_args).run::(()); + /* ANCHOR_END: main */ +} +/* ANCHOR_END: all */ diff --git a/src/SUMMARY.md b/src/SUMMARY.md index 427ccb4..cf4f9be 100644 --- a/src/SUMMARY.md +++ b/src/SUMMARY.md @@ -25,6 +25,7 @@ - [Child components](child_components.md) - [Widget templates](widget_templates/index.md) - [Accessing Nested Template Elements](widget_templates/accessing_nested_template_elements.md) +- [Command Line Interfaces](cli.md) - [gtk-rs overview](gtk_rs.md) - [Resource Bundles](resource_bundles.md) - [Continuous Integration guide](continuous_integration.md) diff --git a/src/cli.md b/src/cli.md new file mode 100644 index 0000000..faef258 --- /dev/null +++ b/src/cli.md @@ -0,0 +1,76 @@ +# Command Line Interfaces + +The handling of CLI arguments in Relm4 has some specifics you should be aware of. + +The first one is that Relm4/GTK tries to parse the arguments again even if you parsed them yourself already. +This means the program will crash with an error like `Unknown option --non-gtk-arg`. +To fix this you can use the [`with_args`](https://docs.rs/relm4/latest/relm4/struct.RelmApp.html#method.with_args) method to provide the arguments the GTK app should parse. +The easiest way is to just provide an empty `Vec` but this has the disadvantage that the standard GTK arguments don't work anymore. + +We will now make it work in combination with the popular [`clap`](https://docs.rs/clap/latest/clap/) crate. +To be precise we will use the `derive` feature which you can learn about in the [`clap` documentation](https://docs.rs/clap/latest/clap/_derive/_tutorial/chapter_0/index.html) but it works with the builder pattern too of course. + +To pass a `Vec` of GTK arguments we need to separate the arguments we want to consume ourselfes from those we want to pass to GTK. +In `clap` you can achieve this using a combination of [`allow_hyphen_values`](https://docs.rs/clap/latest/clap/struct.Arg.html#method.allow_hyphen_values) and [`trailing_var_arg`](https://docs.rs/clap/latest/clap/struct.Arg.html#method.trailing_var_arg). +```rust,no_run,noplayground +{{#include ../examples/cli.rs:args_struct }} +``` + +Now in our main function we can parse the CLI arguments using `Args::parse()` and pass `args.gtk_options` to GTK/Relm4. +The first argument is (as per convention) the program invocation so we need to add that first: +```rust,no_run,noplayground +{{#include ../examples/cli.rs:main }} +``` + +## Result +To compile, run and pass arguments to the built binary in one command we can use `cargo run --` and pass our arguments after that. +> If you wonder what the `--` means: This is the [*end of options* convention](https://pubs.opengroup.org/onlinepubs/9699919799/basedefs/V1_chap12.html#tag_12_02): +> *"The first -- argument that is not an option-argument should be accepted as a delimiter indicating the end of options. Any following arguments should be treated as operands, even if they begin with the '-' character."* + +We can now look at the result using `cargo run -- --help`: +``` +Usage: cli [OPTIONS] [GTK_OPTIONS]... + +Arguments: + [GTK_OPTIONS]... Unknown arguments or everything after -- gets passed through to GTK + +Options: + --non-gtk-arg some argument to test + -h, --help Print help + -V, --version Print version +``` + +This is the help text provided by `clap`. +If you want to see the GTK help text you can use `cargo run -- -- --help`: +``` +Usage: + cli [OPTION?] + +Help Options: + -h, --help Show help options + --help-all Show all help options + --help-gapplication Show GApplication options +``` +And if the GTK option is unique and not used by your program the (second) `--` is not needed anymore, e.g. `cargo run -- --help-all`: +``` +Usage: + cli [OPTION?] + +Help Options: + -h, --help Show help options + --help-all Show all help options + --help-gapplication Show GApplication options + +GApplication Options: + --gapplication-service Enter GApplication service mode (use from D-Bus service files) +``` + +Of course you can replace `cargo run --` by your binary name later, e.g.: `your-cool-app --help-all`. + +## The complete code + +Here is a minimal working example code with some debug output: + +```rust,no_run,noplayground +{{#include ../examples/cli.rs:all }} +```