Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions crates/app/data/linux/chipmunk.desktop
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ Type=Application
Categories=Utility;Development;
Actions=new-empty-window;
Keywords=chipmunk;log;viewer;trace;dlt;
MimeType=text/plain;text/x-log;application/vnd.tcpdump.pcap;application/x-pcapng;application/x-dlt;

[Desktop Action new-empty-window]
Name=New Empty Window
Expand Down
82 changes: 79 additions & 3 deletions crates/app/src/cli/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,23 @@ version: {version}
";

#[derive(Debug, clap::Parser)]
#[clap(name = "chipmunk", version, about, help_template = HELP_TEMPLATE)]
#[clap(
name = "chipmunk",
version,
about,
help_template = HELP_TEMPLATE,
// Generated usage would read `chipmunk [PATHS]... [COMMAND]`, which suggests paths
// and a command can be combined. Spell the two accepted forms out instead.
override_usage = "chipmunk [COMMAND]\n chipmunk <PATHS>..."
)]
pub struct Cli {
#[command(subcommand)]
pub source: Option<SourcesCommand>,
// clap has no default subcommand. These top-level paths stand in for `files`,
// which is how the OS launches Chipmunk when a file is opened with it.
/// Paths to the source files. Shorthand for the `files` command.
#[arg(name = "PATHS", value_hint = ValueHint::FilePath)]
pub paths: Vec<PathBuf>,
}

#[derive(Debug, Clone, Subcommand)]
Expand All @@ -42,10 +55,19 @@ pub enum SourcesCommand {
}

impl Cli {
pub fn get_commands(mut self) -> Vec<CliCommand> {
pub fn get_commands(self) -> Vec<CliCommand> {
let Self { source, paths } = self;

// Bare paths are the shorthand form of the `files` command.
let source = match (source, paths) {
(Some(source), _) => Some(source),
(None, paths) if !paths.is_empty() => Some(SourcesCommand::Files { paths }),
(None, _) => None,
};

let mut cli_cmds = Vec::new();

if let Some(source) = self.source.take() {
if let Some(source) = source {
let cmd = match source {
SourcesCommand::Files { paths } => CliCommand::OpenFiles { paths },
SourcesCommand::Process { command, cwd } => {
Expand All @@ -63,10 +85,64 @@ impl Cli {
mod tests {
use super::*;

use clap::Parser;

/// Ensure the CLI configurations are valid.
#[test]
fn verify_cli() {
use clap::CommandFactory;
Cli::command().debug_assert();
}

fn parse(args: &[&str]) -> Vec<CliCommand> {
Cli::try_parse_from(args).unwrap().get_commands()
}

#[test]
fn no_args_has_no_commands() {
assert!(parse(&["chipmunk"]).is_empty());
}

#[test]
fn bare_paths_open_files() {
let cmds = parse(&["chipmunk", "a.log", "b.dlt"]);
assert!(matches!(
cmds.as_slice(),
[CliCommand::OpenFiles { paths }] if paths == &[PathBuf::from("a.log"), PathBuf::from("b.dlt")]
));
}

#[test]
fn files_command_still_works() {
let cmds = parse(&["chipmunk", "files", "a.log", "b.dlt"]);
assert!(matches!(
cmds.as_slice(),
[CliCommand::OpenFiles { paths }] if paths == &[PathBuf::from("a.log"), PathBuf::from("b.dlt")]
));
}

/// A file named like a subcommand still reaches the app after `--`.
#[test]
fn escaped_path_is_not_a_subcommand() {
let cmds = parse(&["chipmunk", "--", "files"]);
assert!(matches!(
cmds.as_slice(),
[CliCommand::OpenFiles { paths }] if paths == &[PathBuf::from("files")]
));
}

#[test]
fn files_command_requires_paths() {
assert!(Cli::try_parse_from(["chipmunk", "files"]).is_err());
}

/// Once the first path is taken, later values stay paths and never start a subcommand.
#[test]
fn subcommand_name_after_path_is_a_path() {
let cmds = parse(&["chipmunk", "a.log", "files"]);
assert!(matches!(
cmds.as_slice(),
[CliCommand::OpenFiles { paths }] if paths == &[PathBuf::from("a.log"), PathBuf::from("files")]
));
}
}
Loading