mod local;
mod migrate;
mod remote;
use crate::audio;
use crate::midi;
use clap::{crate_version, Parser, Subcommand};
use std::env;
use std::error::Error;
use std::fmt::Display;
const SYSTEMD_SERVICE: &str = r#"
[Unit]
Description=multitrack player
[Service]
Type=simple
Restart=on-failure
EnvironmentFile=-/etc/default/mtrack
ExecStart={{ CURRENT_EXECUTABLE }} start "$MTRACK_PATH"
ExecReload=/bin/kill -HUP $MAINPID
# User and group. Create with:
# sudo useradd --system --no-create-home --shell /usr/sbin/nologin mtrack
# sudo usermod -aG audio mtrack
User=mtrack
Group=mtrack
SupplementaryGroups=audio
# Allow setting thread/RT priority for real-time audio scheduling.
AmbientCapabilities=CAP_SYS_NICE
CapabilityBoundingSet=CAP_SYS_NICE
# Filesystem restrictions. The filesystem is read-only except for the project
# directory, which mtrack writes to for configuration, songs, playlists, and
# lighting files. ProtectSystem=full makes /usr, /boot, and /efi read-only
# while leaving other paths writable for the mtrack user.
ProtectSystem=full
PrivateTmp=true
# Kernel restrictions.
ProtectKernelTunables=true
ProtectKernelModules=true
ProtectKernelLogs=true
ProtectControlGroups=true
# Additional hardening.
NoNewPrivileges=true
LockPersonality=true
RestrictNamespaces=true
RestrictSUIDSGID=true
MemoryDenyWriteExecute=true
SystemCallArchitectures=native
RestrictAddressFamilies=AF_INET AF_INET6 AF_UNIX
[Install]
WantedBy=multi-user.target
Alias=mtrack.service
"#;
#[derive(Parser)]
#[clap(
author = "Michael Wilson",
version = crate_version!(),
about = "A multitrack player."
)]
struct Cli {
#[clap(subcommand)]
command: Commands,
}
#[derive(Subcommand)]
enum Commands {
Songs {
path: String,
#[arg(long)]
init: bool,
},
Devices {},
MidiDevices {},
Playlist {
repository_path: String,
playlist_path: String,
},
Start {
#[arg(default_value = ".")]
path: String,
playlist_path: Option<String>,
#[arg(long)]
tui: bool,
#[arg(long, default_value = "8080")]
web_port: u16,
#[arg(long, default_value = "0.0.0.0")]
web_address: String,
},
Play {
#[arg(short = 'H', long)]
host_port: Option<String>,
#[arg(long)]
from: Option<String>,
},
Previous {
#[arg(short = 'H', long)]
host_port: Option<String>,
},
Next {
#[arg(short = 'H', long)]
host_port: Option<String>,
},
Stop {
#[arg(short = 'H', long)]
host_port: Option<String>,
},
SwitchToPlaylist {
#[arg(short = 'H', long)]
host_port: Option<String>,
playlist_name: String,
},
Status {
#[arg(short = 'H', long)]
host_port: Option<String>,
},
ActiveEffects {
#[arg(short = 'H', long)]
host_port: Option<String>,
},
Systemd {},
VerifyLightShow {
show_path: String,
#[arg(short, long)]
config: Option<String>,
},
Cues {
#[arg(short = 'H', long)]
host_port: Option<String>,
},
CalibrateTriggers {
device: String,
#[arg(long)]
sample_rate: Option<u32>,
#[arg(long, default_value = "3")]
duration: f32,
#[arg(long)]
sample_format: Option<String>,
#[arg(long)]
bits_per_sample: Option<u16>,
},
Migrate {
#[arg(default_value = ".")]
path: String,
#[arg(long)]
apply: bool,
},
Verify {
config: String,
#[arg(long)]
check: Option<Vec<String>>,
#[arg(long)]
hostname: Option<String>,
},
}
fn format_device_list<T: Display>(devices: &[T], empty_msg: &str) -> String {
if devices.is_empty() {
return empty_msg.to_string();
}
let mut output = String::from("Devices:");
for device in devices {
output.push_str(&format!("\n- {}", device));
}
output
}
fn print_device_list<T: Display>(devices: Vec<T>, empty_msg: &str) {
println!("{}", format_device_list(&devices, empty_msg));
}
fn render_systemd_service(executable_path: &str) -> String {
SYSTEMD_SERVICE.replace("{{ CURRENT_EXECUTABLE }}", executable_path)
}
pub async fn run(tui_mode: bool) -> Result<(), Box<dyn Error>> {
let cli = Cli::parse();
match cli.command {
Commands::Songs { path, init } => local::songs(&path, init)?,
Commands::Devices {} => print_device_list(audio::list_devices()?, "No devices found."),
Commands::MidiDevices {} => print_device_list(midi::list_devices()?, "No devices found."),
Commands::Playlist {
repository_path,
playlist_path,
} => local::playlist(&repository_path, &playlist_path)?,
Commands::Start {
path,
playlist_path,
tui,
web_port,
web_address,
} => {
let web_config = crate::webui::server::WebConfig {
port: web_port,
address: web_address,
};
let effective_tui = tui_mode && tui;
local::start(&path, playlist_path, web_config, effective_tui).await?
}
Commands::Play { host_port, from } => remote::play(host_port, from).await?,
Commands::Previous { host_port } => remote::previous(host_port).await?,
Commands::Next { host_port } => remote::next(host_port).await?,
Commands::Stop { host_port } => remote::stop(host_port).await?,
Commands::SwitchToPlaylist {
host_port,
playlist_name,
} => remote::switch_to_playlist(host_port, &playlist_name).await?,
Commands::Status { host_port } => remote::status(host_port).await?,
Commands::ActiveEffects { host_port } => remote::active_effects(host_port).await?,
Commands::Systemd {} => {
let current_executable_path = env::current_exe()?;
println!(
"{}",
render_systemd_service(¤t_executable_path.to_string_lossy())
)
}
Commands::CalibrateTriggers {
device,
sample_rate,
duration,
sample_format,
bits_per_sample,
} => local::calibrate_triggers(
&device,
sample_rate,
duration,
sample_format,
bits_per_sample,
)?,
Commands::VerifyLightShow { show_path, config } => {
local::verify_light_show(&show_path, config.as_deref())?
}
Commands::Cues { host_port } => remote::cues(host_port).await?,
Commands::Migrate { path, apply } => migrate::migrate(&path, apply)?,
Commands::Verify {
config,
check,
hostname,
} => local::verify(&config, check, hostname)?,
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
use clap::Parser;
mod format_device_list_tests {
use super::*;
#[test]
fn empty_list_returns_empty_msg() {
let devices: Vec<String> = vec![];
assert_eq!(
format_device_list(&devices, "No devices found."),
"No devices found."
);
}
#[test]
fn single_device() {
let devices = vec!["My Speaker"];
let result = format_device_list(&devices, "No devices found.");
assert_eq!(result, "Devices:\n- My Speaker");
}
#[test]
fn multiple_devices() {
let devices = vec!["Speaker A", "Speaker B", "Headphones"];
let result = format_device_list(&devices, "");
assert!(result.starts_with("Devices:"));
assert!(result.contains("- Speaker A"));
assert!(result.contains("- Speaker B"));
assert!(result.contains("- Headphones"));
}
#[test]
fn works_with_display_types() {
let devices: Vec<i32> = vec![1, 2, 3];
let result = format_device_list(&devices, "");
assert!(result.contains("- 1"));
assert!(result.contains("- 2"));
assert!(result.contains("- 3"));
}
#[test]
fn custom_empty_message() {
let devices: Vec<String> = vec![];
assert_eq!(
format_device_list(&devices, "Nothing here!"),
"Nothing here!"
);
}
}
mod render_systemd_service_tests {
use super::*;
#[test]
fn substitutes_executable_path() {
let result = render_systemd_service("/usr/local/bin/mtrack");
assert!(result.contains("ExecStart=/usr/local/bin/mtrack start"));
assert!(!result.contains("{{ CURRENT_EXECUTABLE }}"));
}
#[test]
fn preserves_service_structure() {
let result = render_systemd_service("/usr/bin/mtrack");
assert!(result.contains("[Unit]"));
assert!(result.contains("[Service]"));
assert!(result.contains("[Install]"));
assert!(result.contains("Type=simple"));
}
#[test]
fn preserves_hardening_directives() {
let result = render_systemd_service("/usr/bin/mtrack");
assert!(result.contains("ProtectSystem=full"));
assert!(result.contains("NoNewPrivileges=true"));
assert!(result.contains("MemoryDenyWriteExecute=true"));
}
#[test]
fn handles_path_with_spaces() {
let result = render_systemd_service("/opt/my apps/mtrack");
assert!(result.contains("ExecStart=/opt/my apps/mtrack start"));
}
#[test]
fn does_not_contain_protect_home() {
let result = render_systemd_service("/usr/bin/mtrack");
assert!(
!result.contains("ProtectHome"),
"ProtectHome should not be in the systemd template — the project \
directory may be under /home and mtrack needs write access to it"
);
}
#[test]
fn does_not_contain_protect_system_strict() {
let result = render_systemd_service("/usr/bin/mtrack");
assert!(
!result.contains("ProtectSystem=strict"),
"ProtectSystem=strict prevents mtrack from writing to the project \
directory — use ProtectSystem=full instead"
);
}
#[test]
fn allows_write_access_to_project_dir() {
let result = render_systemd_service("/usr/bin/mtrack");
assert!(
result.contains("ProtectSystem=full"),
"ProtectSystem must be 'full' (not 'strict') so the project \
directory remains writable"
);
assert!(
!result.contains("ProtectSystem=strict"),
"ProtectSystem=strict would make the project directory read-only"
);
}
#[test]
fn uses_environment_file() {
let result = render_systemd_service("/usr/bin/mtrack");
assert!(result.contains("EnvironmentFile=-/etc/default/mtrack"));
assert!(result.contains("\"$MTRACK_PATH\""));
}
}
mod cli_parsing_tests {
use super::*;
#[test]
fn parse_songs_command() {
let cli = Cli::try_parse_from(["mtrack", "songs", "/path/to/songs"]).unwrap();
match cli.command {
Commands::Songs { path, init } => {
assert_eq!(path, "/path/to/songs");
assert!(!init);
}
_ => panic!("expected Songs command"),
}
}
#[test]
fn parse_songs_with_init() {
let cli = Cli::try_parse_from(["mtrack", "songs", "/path", "--init"]).unwrap();
match cli.command {
Commands::Songs { init, .. } => assert!(init),
_ => panic!("expected Songs command"),
}
}
#[test]
fn parse_devices_command() {
let cli = Cli::try_parse_from(["mtrack", "devices"]).unwrap();
assert!(matches!(cli.command, Commands::Devices {}));
}
#[test]
fn parse_midi_devices_command() {
let cli = Cli::try_parse_from(["mtrack", "midi-devices"]).unwrap();
assert!(matches!(cli.command, Commands::MidiDevices {}));
}
#[test]
fn parse_start_command_defaults() {
let cli = Cli::try_parse_from(["mtrack", "start", "config.yaml"]).unwrap();
match cli.command {
Commands::Start {
path,
playlist_path,
tui,
web_port,
web_address,
} => {
assert_eq!(path, "config.yaml");
assert!(playlist_path.is_none());
assert!(!tui);
assert_eq!(web_port, 8080);
assert_eq!(web_address, "0.0.0.0");
}
_ => panic!("expected Start command"),
}
}
#[test]
fn parse_start_with_all_options() {
let cli = Cli::try_parse_from([
"mtrack",
"start",
"config.yaml",
"playlist.yaml",
"--tui",
"--web-port",
"9090",
"--web-address",
"0.0.0.0",
])
.unwrap();
match cli.command {
Commands::Start {
path,
playlist_path,
tui,
web_port,
web_address,
} => {
assert_eq!(path, "config.yaml");
assert_eq!(playlist_path.as_deref(), Some("playlist.yaml"));
assert!(tui);
assert_eq!(web_port, 9090);
assert_eq!(web_address, "0.0.0.0");
}
_ => panic!("expected Start command"),
}
}
#[test]
fn parse_play_with_from() {
let cli = Cli::try_parse_from(["mtrack", "play", "--from", "1:23.456"]).unwrap();
match cli.command {
Commands::Play { host_port, from } => {
assert!(host_port.is_none());
assert_eq!(from.as_deref(), Some("1:23.456"));
}
_ => panic!("expected Play command"),
}
}
#[test]
fn parse_play_with_host() {
let cli =
Cli::try_parse_from(["mtrack", "play", "--host-port", "localhost:50051"]).unwrap();
match cli.command {
Commands::Play { host_port, .. } => {
assert_eq!(host_port.as_deref(), Some("localhost:50051"));
}
_ => panic!("expected Play command"),
}
}
#[test]
fn parse_switch_to_playlist() {
let cli = Cli::try_parse_from(["mtrack", "switch-to-playlist", "all_songs"]).unwrap();
match cli.command {
Commands::SwitchToPlaylist {
playlist_name,
host_port,
} => {
assert_eq!(playlist_name, "all_songs");
assert!(host_port.is_none());
}
_ => panic!("expected SwitchToPlaylist command"),
}
}
#[test]
fn parse_systemd_command() {
let cli = Cli::try_parse_from(["mtrack", "systemd"]).unwrap();
assert!(matches!(cli.command, Commands::Systemd {}));
}
#[test]
fn parse_verify_light_show() {
let cli = Cli::try_parse_from([
"mtrack",
"verify-light-show",
"show.yaml",
"--config",
"mtrack.yaml",
])
.unwrap();
match cli.command {
Commands::VerifyLightShow { show_path, config } => {
assert_eq!(show_path, "show.yaml");
assert_eq!(config.as_deref(), Some("mtrack.yaml"));
}
_ => panic!("expected VerifyLightShow command"),
}
}
#[test]
fn parse_calibrate_triggers() {
let cli = Cli::try_parse_from([
"mtrack",
"calibrate-triggers",
"USB Audio",
"--sample-rate",
"48000",
"--duration",
"5",
])
.unwrap();
match cli.command {
Commands::CalibrateTriggers {
device,
sample_rate,
duration,
..
} => {
assert_eq!(device, "USB Audio");
assert_eq!(sample_rate, Some(48000));
assert_eq!(duration, 5.0);
}
_ => panic!("expected CalibrateTriggers command"),
}
}
#[test]
fn parse_verify_command() {
let cli = Cli::try_parse_from([
"mtrack",
"verify",
"mtrack.yaml",
"--check",
"track-mappings",
"--hostname",
"stage-left",
])
.unwrap();
match cli.command {
Commands::Verify {
config,
check,
hostname,
} => {
assert_eq!(config, "mtrack.yaml");
assert_eq!(check.as_deref(), Some(&["track-mappings".to_string()][..]));
assert_eq!(hostname.as_deref(), Some("stage-left"));
}
_ => panic!("expected Verify command"),
}
}
#[test]
fn missing_required_args_fails() {
assert!(Cli::try_parse_from(["mtrack", "songs"]).is_err());
assert!(Cli::try_parse_from(["mtrack", "verify"]).is_err());
}
#[test]
fn start_defaults_to_current_dir() {
let cli = Cli::try_parse_from(["mtrack", "start"]).unwrap();
match cli.command {
Commands::Start { path, .. } => {
assert_eq!(path, ".");
}
_ => panic!("expected Start command"),
}
}
#[test]
fn unknown_command_fails() {
assert!(Cli::try_parse_from(["mtrack", "nonexistent"]).is_err());
}
#[test]
fn parse_playlist_command() {
let cli =
Cli::try_parse_from(["mtrack", "playlist", "/path/to/songs", "playlist.yaml"])
.unwrap();
match cli.command {
Commands::Playlist {
repository_path,
playlist_path,
} => {
assert_eq!(repository_path, "/path/to/songs");
assert_eq!(playlist_path, "playlist.yaml");
}
_ => panic!("expected Playlist command"),
}
}
#[test]
fn parse_playlist_missing_args_fails() {
assert!(Cli::try_parse_from(["mtrack", "playlist"]).is_err());
assert!(Cli::try_parse_from(["mtrack", "playlist", "/path"]).is_err());
}
#[test]
fn parse_previous_command() {
let cli = Cli::try_parse_from(["mtrack", "previous"]).unwrap();
match cli.command {
Commands::Previous { host_port } => {
assert!(host_port.is_none());
}
_ => panic!("expected Previous command"),
}
}
#[test]
fn parse_previous_with_host() {
let cli = Cli::try_parse_from(["mtrack", "previous", "-H", "localhost:50051"]).unwrap();
match cli.command {
Commands::Previous { host_port } => {
assert_eq!(host_port.as_deref(), Some("localhost:50051"));
}
_ => panic!("expected Previous command"),
}
}
#[test]
fn parse_next_command() {
let cli = Cli::try_parse_from(["mtrack", "next"]).unwrap();
match cli.command {
Commands::Next { host_port } => {
assert!(host_port.is_none());
}
_ => panic!("expected Next command"),
}
}
#[test]
fn parse_next_with_short_host_flag() {
let cli = Cli::try_parse_from(["mtrack", "next", "-H", "192.168.1.1:43234"]).unwrap();
match cli.command {
Commands::Next { host_port } => {
assert_eq!(host_port.as_deref(), Some("192.168.1.1:43234"));
}
_ => panic!("expected Next command"),
}
}
#[test]
fn parse_stop_command() {
let cli = Cli::try_parse_from(["mtrack", "stop"]).unwrap();
match cli.command {
Commands::Stop { host_port } => {
assert!(host_port.is_none());
}
_ => panic!("expected Stop command"),
}
}
#[test]
fn parse_stop_with_host() {
let cli =
Cli::try_parse_from(["mtrack", "stop", "--host-port", "10.0.0.1:43234"]).unwrap();
match cli.command {
Commands::Stop { host_port } => {
assert_eq!(host_port.as_deref(), Some("10.0.0.1:43234"));
}
_ => panic!("expected Stop command"),
}
}
#[test]
fn parse_status_command() {
let cli = Cli::try_parse_from(["mtrack", "status"]).unwrap();
match cli.command {
Commands::Status { host_port } => {
assert!(host_port.is_none());
}
_ => panic!("expected Status command"),
}
}
#[test]
fn parse_status_with_host() {
let cli = Cli::try_parse_from(["mtrack", "status", "-H", "localhost:9999"]).unwrap();
match cli.command {
Commands::Status { host_port } => {
assert_eq!(host_port.as_deref(), Some("localhost:9999"));
}
_ => panic!("expected Status command"),
}
}
#[test]
fn parse_active_effects_command() {
let cli = Cli::try_parse_from(["mtrack", "active-effects"]).unwrap();
match cli.command {
Commands::ActiveEffects { host_port } => {
assert!(host_port.is_none());
}
_ => panic!("expected ActiveEffects command"),
}
}
#[test]
fn parse_active_effects_with_host() {
let cli = Cli::try_parse_from(["mtrack", "active-effects", "--host-port", "host:1234"])
.unwrap();
match cli.command {
Commands::ActiveEffects { host_port } => {
assert_eq!(host_port.as_deref(), Some("host:1234"));
}
_ => panic!("expected ActiveEffects command"),
}
}
#[test]
fn parse_cues_command() {
let cli = Cli::try_parse_from(["mtrack", "cues"]).unwrap();
match cli.command {
Commands::Cues { host_port } => {
assert!(host_port.is_none());
}
_ => panic!("expected Cues command"),
}
}
#[test]
fn parse_cues_with_host() {
let cli = Cli::try_parse_from(["mtrack", "cues", "-H", "server:43234"]).unwrap();
match cli.command {
Commands::Cues { host_port } => {
assert_eq!(host_port.as_deref(), Some("server:43234"));
}
_ => panic!("expected Cues command"),
}
}
#[test]
fn parse_play_no_args() {
let cli = Cli::try_parse_from(["mtrack", "play"]).unwrap();
match cli.command {
Commands::Play { host_port, from } => {
assert!(host_port.is_none());
assert!(from.is_none());
}
_ => panic!("expected Play command"),
}
}
#[test]
fn parse_play_with_both_host_and_from() {
let cli = Cli::try_parse_from([
"mtrack",
"play",
"-H",
"localhost:50051",
"--from",
"2:30.000",
])
.unwrap();
match cli.command {
Commands::Play { host_port, from } => {
assert_eq!(host_port.as_deref(), Some("localhost:50051"));
assert_eq!(from.as_deref(), Some("2:30.000"));
}
_ => panic!("expected Play command"),
}
}
#[test]
fn parse_switch_to_playlist_with_host() {
let cli = Cli::try_parse_from([
"mtrack",
"switch-to-playlist",
"-H",
"server:43234",
"playlist",
])
.unwrap();
match cli.command {
Commands::SwitchToPlaylist {
host_port,
playlist_name,
} => {
assert_eq!(host_port.as_deref(), Some("server:43234"));
assert_eq!(playlist_name, "playlist");
}
_ => panic!("expected SwitchToPlaylist command"),
}
}
#[test]
fn parse_switch_to_playlist_missing_name_fails() {
assert!(Cli::try_parse_from(["mtrack", "switch-to-playlist"]).is_err());
}
#[test]
fn parse_verify_light_show_without_config() {
let cli = Cli::try_parse_from(["mtrack", "verify-light-show", "show.light"]).unwrap();
match cli.command {
Commands::VerifyLightShow { show_path, config } => {
assert_eq!(show_path, "show.light");
assert!(config.is_none());
}
_ => panic!("expected VerifyLightShow command"),
}
}
#[test]
fn parse_calibrate_triggers_defaults() {
let cli = Cli::try_parse_from(["mtrack", "calibrate-triggers", "USB Audio"]).unwrap();
match cli.command {
Commands::CalibrateTriggers {
device,
sample_rate,
duration,
sample_format,
bits_per_sample,
} => {
assert_eq!(device, "USB Audio");
assert!(sample_rate.is_none());
assert_eq!(duration, 3.0); assert!(sample_format.is_none());
assert!(bits_per_sample.is_none());
}
_ => panic!("expected CalibrateTriggers command"),
}
}
#[test]
fn parse_calibrate_triggers_with_all_options() {
let cli = Cli::try_parse_from([
"mtrack",
"calibrate-triggers",
"USB Audio",
"--sample-rate",
"48000",
"--duration",
"5",
"--sample-format",
"float",
"--bits-per-sample",
"32",
])
.unwrap();
match cli.command {
Commands::CalibrateTriggers {
device,
sample_rate,
duration,
sample_format,
bits_per_sample,
} => {
assert_eq!(device, "USB Audio");
assert_eq!(sample_rate, Some(48000));
assert_eq!(duration, 5.0);
assert_eq!(sample_format.as_deref(), Some("float"));
assert_eq!(bits_per_sample, Some(32));
}
_ => panic!("expected CalibrateTriggers command"),
}
}
#[test]
fn parse_verify_without_optional_args() {
let cli = Cli::try_parse_from(["mtrack", "verify", "mtrack.yaml"]).unwrap();
match cli.command {
Commands::Verify {
config,
check,
hostname,
} => {
assert_eq!(config, "mtrack.yaml");
assert!(check.is_none());
assert!(hostname.is_none());
}
_ => panic!("expected Verify command"),
}
}
#[test]
fn parse_verify_with_multiple_checks() {
let cli = Cli::try_parse_from([
"mtrack",
"verify",
"mtrack.yaml",
"--check",
"track-mappings",
"--check",
"other-check",
])
.unwrap();
match cli.command {
Commands::Verify { check, .. } => {
let checks = check.unwrap();
assert_eq!(checks.len(), 2);
assert!(checks.contains(&"track-mappings".to_string()));
assert!(checks.contains(&"other-check".to_string()));
}
_ => panic!("expected Verify command"),
}
}
#[test]
fn parse_start_with_playlist_path() {
let cli =
Cli::try_parse_from(["mtrack", "start", "config.yaml", "custom-playlist.yaml"])
.unwrap();
match cli.command {
Commands::Start {
path,
playlist_path,
..
} => {
assert_eq!(path, "config.yaml");
assert_eq!(playlist_path.as_deref(), Some("custom-playlist.yaml"));
}
_ => panic!("expected Start command"),
}
}
#[test]
fn parse_migrate_defaults() {
let cli = Cli::try_parse_from(["mtrack", "migrate"]).unwrap();
match cli.command {
Commands::Migrate { path, apply } => {
assert_eq!(path, ".");
assert!(!apply);
}
_ => panic!("expected Migrate command"),
}
}
#[test]
fn parse_migrate_with_path_and_apply() {
let cli =
Cli::try_parse_from(["mtrack", "migrate", "/path/to/project", "--apply"]).unwrap();
match cli.command {
Commands::Migrate { path, apply } => {
assert_eq!(path, "/path/to/project");
assert!(apply);
}
_ => panic!("expected Migrate command"),
}
}
#[test]
fn no_subcommand_fails() {
assert!(Cli::try_parse_from(["mtrack"]).is_err());
}
}
}