Repository: pop-os/cosmic-player
Pull request #94: CLI with clap_lex
Author: login: LinuxBoy-96
Created: 2025-03-23T09:10:56Z | Merged: 2025-03-26T15:42:40Z

Add --version and --help argument support
Quite useful for when COSMIC will be released.

Stripped, --release Binary size is 21040 KB


Diff:
diff --git a/Cargo.lock b/Cargo.lock
index f7831a8..668ccdf 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -184,6 +184,12 @@ dependencies = [
  "libc",
 ]
 
+[[package]]
+name = "anyhow"
+version = "1.0.97"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dcfed56ad506cb2c684a14971b8861fdc3baaaae314b9e5f9bb532cbe3ba7a4f"
+
 [[package]]
 name = "apply"
 version = "0.3.0"
@@ -844,6 +850,12 @@ dependencies = [
  "windows-targets 0.52.6",
 ]
 
+[[package]]
+name = "clap_lex"
+version = "0.7.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f46ad14479a25103f283c0f10005961cf086d8dc42205bb44c46ac563475dca6"
+
 [[package]]
 name = "clipboard-win"
 version = "5.4.0"
@@ -1070,6 +1082,7 @@ dependencies = [
 name = "cosmic-player"
 version = "0.1.0"
 dependencies = [
+ "clap_lex",
  "dirs",
  "env_logger",
  "fork",
@@ -1084,13 +1097,13 @@ dependencies = [
  "libcosmic",
  "log",
  "mpris-server",
- "pico-args",
  "rust-embed",
  "serde",
  "smol_str",
  "tempfile",
  "tokio",
  "url",
+ "vergen",
 ]
 
 [[package]]
@@ -1292,6 +1305,15 @@ version = "0.3.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "5c297a1c74b71ae29df00c3e22dd9534821d60eb9af5a0192823fa2acea70c2a"
 
+[[package]]
+name = "deranged"
+version = "0.4.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "28cfac68e08048ae1883171632c2aef3ebc555621ae56fbccce1cbf22dd7f058"
+dependencies = [
+ "powerfmt",
+]
+
 [[package]]
 name = "derivative"
 version = "2.2.0"
@@ -3680,6 +3702,12 @@ dependencies = [
  "num-traits",
 ]
 
+[[package]]
+name = "num-conv"
+version = "0.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9"
+
 [[package]]
 name = "num-integer"
 version = "0.1.46"
@@ -3752,6 +3780,15 @@ dependencies = [
  "syn 2.0.96",
 ]
 
+[[package]]
+name = "num_threads"
+version = "0.1.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5c7398b9c8b70908f6371f47ed36737907c87c52af34c268fed0bf0ceb92ead9"
+dependencies = [
+ "libc",
+]
+
 [[package]]
 name = "objc"
 version = "0.2.7"
@@ -4139,6 +4176,12 @@ version = "0.3.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "22686f4785f02a4fcc856d3b3bb19bf6c8160d103f7a99cc258bddd0251dc7f2"
 
+[[package]]
+name = "powerfmt"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391"
+
 [[package]]
 name = "ppv-lite86"
 version = "0.2.20"
@@ -4561,6 +4604,12 @@ dependencies = [
  "windows-sys 0.59.0",
 ]
 
+[[package]]
+name = "rustversion"
+version = "1.0.20"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "eded382c5f5f786b989652c49544c4877d9f015cc22e145a5ea8ea66c2921cd2"
+
 [[package]]
 name = "rustybuzz"
 version = "0.12.1"
@@ -4796,9 +4845,9 @@ dependencies = [
 
 [[package]]
 name = "smallvec"
-version = "1.13.2"
+version = "1.14.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67"
+checksum = "7fcf8323ef1faaee30a44a340193b1ac6814fd9b7b4e88e9d4519a3e4abe1cfd"
 
 [[package]]
 name = "smithay-client-toolkit"
@@ -5138,6 +5187,39 @@ dependencies = [
  "weezl",
 ]
 
+[[package]]
+name = "time"
+version = "0.3.41"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8a7619e19bc266e0f9c5e6686659d394bc57973859340060a69221e57dbc0c40"
+dependencies = [
+ "deranged",
+ "itoa",
+ "libc",
+ "num-conv",
+ "num_threads",
+ "powerfmt",
+ "serde",
+ "time-core",
+ "time-macros",
+]
+
+[[package]]
+name = "time-core"
+version = "0.1.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c9e9a38711f559d9e3ce1cdb06dd7c5b8ea546bc90052da6d06bb76da74bb07c"
+
+[[package]]
+name = "time-macros"
+version = "0.2.22"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3526739392ec93fd8b359c8e98514cb3e8e021beb4e5f597b00a0221f8ed8a49"
+dependencies = [
+ "num-conv",
+ "time-core",
+]
+
 [[package]]
 name = "tiny-keccak"
 version = "2.0.2"
@@ -5575,6 +5657,18 @@ version = "1.0.4"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be"
 
+[[package]]
+name = "vergen"
+version = "8.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2990d9ea5967266ea0ccf413a4aa5c42a93dbcfda9cb49a97de6931726b12566"
+dependencies = [
+ "anyhow",
+ "cfg-if",
+ "rustversion",
+ "time",
+]
+
 [[package]]
 name = "version-compare"
 version = "0.2.0"
diff --git a/Cargo.toml b/Cargo.toml
index a1c90a2..a01c9a9 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -3,6 +3,9 @@ name = "cosmic-player"
 version = "0.1.0"
 edition = "2021"
 
+[build-dependencies]
+vergen = { version = "8", features = ["git", "gitcl"] }
+
 [dependencies]
 dirs = "5"
 gstreamer-tag = "0.23"
@@ -12,11 +15,15 @@ serde = { version = "1", features = ["serde_derive"] }
 tempfile = "3"
 tokio = "1"
 url = "2"
-pico-args = "0.5"
+# CLI arguments
+clap_lex = "0.7"
 # Internationalization
 icu_collator = "1.5"
 icu_provider = { version = "1.5", features = ["sync"] }
-i18n-embed = { version = "0.14", features = ["fluent-system", "desktop-requester"] }
+i18n-embed = { version = "0.14", features = [
+    "fluent-system",
+    "desktop-requester",
+] }
 i18n-embed-fl = "0.7"
 rust-embed = "8"
 # Logging
@@ -61,4 +68,3 @@ debug = true
 # libcosmic = { path = "../libcosmic" }
 # cosmic-config = { path = "../libcosmic/cosmic-config" }
 # cosmic-theme = { path = "../libcosmic/cosmic-theme" }
-
diff --git a/build.rs b/build.rs
new file mode 100644
index 0000000..f2a7518
--- /dev/null
+++ b/build.rs
@@ -0,0 +1,6 @@
+use vergen::EmitBuilder;
+
+fn main() {
+  EmitBuilder::builder().git_sha(true).emit().unwrap();
+}
+
diff --git a/src/argparse.rs b/src/argparse.rs
index 9adb05f..38e2af3 100644
--- a/src/argparse.rs
+++ b/src/argparse.rs
@@ -3,9 +3,57 @@
 
 use std::{fs, io};
 
+use clap_lex::RawArgs;
 use log::warn;
 use url::Url;
 
+pub fn parse() -> Arguments {
+    let raw_args = RawArgs::from_args();
+    let mut cursor = raw_args.cursor();
+    let mut arguments = Arguments::default();
+    let mut urls = Vec::new();
+
+    // Parse the arguments
+    while let Some(arg) = raw_args.next(&mut cursor) {
+        if let Some(mut shorts) = arg.to_short() {
+            while let Some(short) = shorts.next_flag() {
+                match short {
+                    Ok('h') => print_help(),
+                    Ok('V') => print_version(),
+                    Ok(c) => warn!("unexpected flag: -{c}"),
+                    Err(os_str) => warn!("unexpected flag: -{}", os_str.to_string_lossy()),
+                }
+            }
+        } else if let Some((long, _opt_value)) = arg.to_long() {
+            match long {
+                Ok("help") => print_help(),
+                Ok("version") => print_version(),
+                _ => warn!("unexpected flag: {}", arg.display()),
+            }
+        } else {
+            // Freestanding arguments are treated as URLs
+            match arg.to_value().ok().map(Source::try_from) {
+                Some(Ok(source)) => urls.push(source.0),
+                Some(Err(why)) => {
+                    warn!("{}: not a valid URL: {}", arg.display(), why)
+                }
+                None => {
+                    warn!("{}: not a valid string", arg.display())
+                }
+            }
+        }
+    }
+
+    if urls.len() > 1 {
+        arguments.urls = Some(urls);
+    } else {
+        urls.truncate(1);
+        arguments.url_opt = urls.pop();
+    }
+
+    arguments
+}
+
 #[derive(Debug, Default)]
 pub struct Arguments {
     /// Files or directory URLs to play
@@ -14,39 +62,6 @@ pub struct Arguments {
     pub url_opt: Option<Url>,
 }
 
-impl Arguments {
-    pub fn from_args() -> Result<Self, pico_args::Error> {
-        let mut parser = pico_args::Arguments::from_env();
-
-        // Freestanding arguments are treated as URLs
-        let urls: Vec<Url> = std::iter::from_fn(|| {
-            parser
-                .opt_free_from_fn(|arg| Source::try_from(arg))
-                .ok()
-                .flatten()
-        })
-        .map(|source| source.0)
-        .collect();
-
-        let remainder = parser.finish();
-        for arg in remainder {
-            warn!("Unused argument: {arg:?}");
-        }
-
-        if urls.len() > 1 {
-            Ok(Arguments {
-                urls: Some(urls),
-                ..Default::default()
-            })
-        } else {
-            Ok(Arguments {
-                url_opt: urls.into_iter().next(),
-                ..Default::default()
-            })
-        }
-    }
-}
-
 // #[derive(Debug)]
 // pub enum Source {
 //     File(Url),
@@ -81,3 +96,36 @@ impl TryFrom<&str> for Source {
         }
     }
 }
+
+#[cold]
+pub fn print_help() -> ! {
+    let version = env!("CARGO_PKG_VERSION");
+    let git_rev = env!("VERGEN_GIT_SHA");
+
+    println!(
+        r#"cosmic-player {version} (git commit {git_rev})
+System76 <info@system76.com>
+
+Designed for the COSMIC™ desktop environment, cosmic-player is a
+libcosmic-based multimedia player for music and videos.
+
+Project home page: https://github.com/pop-os/cosmic-player
+
+Options:
+  -h, --help     Show this message
+  -V, --version  Show the version of cosmic-player"#
+    );
+
+    std::process::exit(0);
+}
+
+#[cold]
+pub fn print_version() -> ! {
+    println!(
+        "cosmic-player {} (git commit {})",
+        env!("CARGO_PKG_VERSION"),
+        env!("VERGEN_GIT_SHA")
+    );
+
+    std::process::exit(0);
+}
diff --git a/src/main.rs b/src/main.rs
index a657a55..6cf1a9f 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -52,6 +52,8 @@ const GST_PLAY_FLAG_VIDEO: i32 = 1 << 0;
 const GST_PLAY_FLAG_AUDIO: i32 = 1 << 1;
 const GST_PLAY_FLAG_TEXT: i32 = 1 << 2;
 
+use std::error::Error;
+
 fn language_name(code: &str) -> Option<String> {
     let code_c = CString::new(code).ok()?;
     let name_c = unsafe {
@@ -68,7 +70,7 @@ fn language_name(code: &str) -> Option<String> {
 
 /// Runs application with these settings
 #[rustfmt::skip]
-fn main() -> Result<(), Box<dyn std::error::Error>> {
+fn main() -> Result<(), Box<dyn Error>> {
     #[cfg(all(unix, not(target_os = "redox")))]
     match fork::daemon(true, true) {
         Ok(fork::Fork::Child) => (),
@@ -78,17 +80,19 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
             process::exit(1);
         }
     }
-	
+
     env_logger::Builder::from_env(env_logger::Env::default().default_filter_or("warn")).init();
 
     localize::localize();
 
+    let args = argparse::parse();
+
     let (config_handler, config) = match cosmic_config::Config::new(App::APP_ID, CONFIG_VERSION) {
         Ok(config_handler) => {
             let config = match Config::get_entry(&config_handler) {
                 Ok(ok) => ok,
                 Err((errs, config)) => {
-                    log::info!("errors loading config: {:?}", errs);
+                    log::error!("errors loading config: {:?}", errs);
                     config
                 }
             };
@@ -121,15 +125,13 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
     settings = settings.theme(config.app_theme.theme());
     settings = settings.size_limits(Limits::NONE.min_width(360.0).min_height(180.0));
 
-    let argparse::Arguments { urls, url_opt } = argparse::Arguments::from_args().unwrap_or_default();
-
     let flags = Flags {
         config_handler,
         config,
         config_state_handler,
         config_state,
-        url_opt,
-        urls
+        url_opt: args.url_opt,
+        urls: args.urls,
     };
     cosmic::app::run::<App>(settings, flags)?;
 


Reviews:
user: login: mmstick
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-24T21:56:00Z
user: login: LinuxBoy-96
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-24T22:08:27Z
user: login: mmstick
state: CHANGES_REQUESTED
author_association: MEMBER
submitted_at: 2025-03-25T19:47:05Z
user: login: LinuxBoy-96
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-25T20:12:39Z
user: login: mmstick
state: APPROVED
author_association: MEMBER
submitted_at: 2025-03-26T15:42:13Z

Review comments:
diff_hunk: @@ -136,6 +159,21 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
     Ok(())
 }
 
+fn print_help(version: &str, git_rev: &str) {
+    println!(
+        r#"cosmic-greeter {version} (git commit {git_rev})
path: src/main.rs
user: login: mmstick
body: wrong command name here
created_at: 2025-03-24T21:56:00Z
author_association: MEMBER
diff_hunk: @@ -136,6 +159,21 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
     Ok(())
 }
 
+fn print_help(version: &str, git_rev: &str) {
+    println!(
+        r#"cosmic-greeter {version} (git commit {git_rev})
path: src/main.rs
user: login: LinuxBoy-96
body: > wrong command name here

Oupsi
created_at: 2025-03-24T22:08:27Z
author_association: CONTRIBUTOR
diff_hunk: @@ -13,6 +16,8 @@ tempfile = "3"
 tokio = "1"
 url = "2"
 pico-args = "0.5"
path: Cargo.toml
user: login: mmstick
body: This is still being used here
created_at: 2025-03-25T19:46:53Z
author_association: MEMBER
diff_hunk: @@ -13,6 +16,8 @@ tempfile = "3"
 tokio = "1"
 url = "2"
 pico-args = "0.5"
path: Cargo.toml
user: login: LinuxBoy-96
body: Done
created_at: 2025-03-25T20:12:39Z
author_association: CONTRIBUTOR

PR comments:
user: login: LinuxBoy-96
created_at: 2025-03-24T21:20:47Z
body: This feature now has the same structure as in cosmic-greeter, which has been merged.
author_association: CONTRIBUTOR
user: login: DrOptix
created_at: 2025-03-25T07:53:56Z
body: The idea behind the PR is awesome, thank you. Now a bit more about the implementation details. If this will get merged as is `cosmic-player` will end up using both `lexopt` and `pico-args`. When it comes to argument parsing I'm a `clap` person. Now I see `lexopt` and `pico-args` as good alternatives, but I have no actual experience with them. I'm of the opinion that we should choose only one and go forward with it for consistency sake. I did not check other `cosmic` apps to see what they are using, but I think uniformity helps at least at project level.
author_association: CONTRIBUTOR
user: login: LinuxBoy-96
created_at: 2025-03-25T08:05:43Z
body: > . I'm of the opinion that we should choose only one and go forward with it for consistency sake. I did not check other cosmic apps to see what they are using, but I think uniformity helps at least at project level.

I charged myself to implement this feature to most cosmic-app. For the moment, only cosmic-greeter has been merge, you can go see the commit. Many not yet merged. (and you can this it since a few hours)

Clap was ruled out due to the sheer size of the binary and the extra compiling time. 500 KB vs some tens of KB.
They are 100% equivalent for what we are using it.

The conly notable thing is that it need an extra function.

+ Vergen is already used across many of cosmic app.

So yes, lexopt it is.
author_association: CONTRIBUTOR
user: login: LinuxBoy-96
created_at: 2025-03-25T12:33:58Z
body: @mmstick So what do you think?
Lexopt vs pico arg
Standardize or not?
author_association: CONTRIBUTOR
user: login: mmstick
created_at: 2025-03-25T12:54:27Z
body: There shouldn't be two different crates doing argument parsing. Also, there's a comparison of argument parsing crates here: https://github.com/rosetta-rs/argparse-rosetta-rs. clap_lex is probably the best maintained of all the libraries that use <40 KB binary size. It's definitely the most sponsored and supported.
author_association: MEMBER
user: login: LinuxBoy-96
created_at: 2025-03-25T13:04:43Z
body: 


> There shouldn't be two different crates doing argument parsing. Also, there's a comparison of argument parsing crates here: https://github.com/rosetta-rs/argparse-rosetta-rs. clap_lex is probably the best maintained of all the libraries that use <40 KB binary size. It's definitely the most sponsored and supported.

Confirm me that you wish to switch to clap_lex, including for cosmic-greeter
author_association: CONTRIBUTOR
user: login: mmstick
created_at: 2025-03-25T13:35:10Z
body: Sure, may as well use the most supported crate to avoid the need to change it later.
author_association: MEMBER
user: login: LinuxBoy-96
created_at: 2025-03-25T15:39:52Z
body: > Sure, may as well use the most supported crate to avoid the need to change it later.

@mmstick Alongside cosmic-greeter, already merged:
cosmic-player, comic-term, cosmic-files and cosmic-edit are ready.

please make sure they are merged soon. :p 

author_association: CONTRIBUTOR
user: login: LinuxBoy-96
created_at: 2025-03-25T15:58:18Z
body: > I think uniformity helps at least at project level.

@DrOptix We opted for clap_lex.
Ready to merge if you wish to. :)

author_association: CONTRIBUTOR
user: login: DrOptix
created_at: 2025-03-25T17:44:58Z
body: Sure, fine by me.
author_association: CONTRIBUTOR
user: login: LinuxBoy-96
created_at: 2025-03-26T15:33:51Z
body: @mmstick thanks for that commit, it's beyond what I could have done by myself... it make sense.

I was so worried about that code being in main.rs
author_association: CONTRIBUTOR