use clap::{
error::{Error, ErrorKind},
value_parser, ArgAction, ArgMatches, Args, Command, FromArgMatches, Parser, Subcommand,
};
use irp::Protocol;
use log::{Level, LevelFilter, Metadata, Record};
use std::{
ffi::OsString,
io,
path::{Path, PathBuf},
sync::OnceLock,
};
mod commands;
#[derive(Parser)]
#[command(
name = "cir",
version = env!("CARGO_PKG_VERSION"),
author = env!("CARGO_PKG_AUTHORS"),
about = "Consumer Infrared",
subcommand_required = true
)]
struct App {
#[arg(long, short, action = ArgAction::Count, global = true, conflicts_with = "quiet")]
verbose: u8,
#[arg(long, short, global = true, conflicts_with = "verbose")]
quiet: bool,
#[arg(
long = "irp-protocols",
global = true,
default_value = "/usr/share/rc_keymaps/IrpProtocols.xml"
)]
irp_protocols: PathBuf,
#[command(subcommand)]
command: Commands,
}
enum Commands {
#[cfg(target_os = "linux")]
List(List),
#[cfg(target_os = "linux")]
Keymap(Keymap),
Decode(Decode),
Transmit(Transmit),
#[cfg(target_os = "linux")]
Test(Test),
}
#[derive(Args)]
struct Decode {
#[cfg(target_os = "linux")]
#[clap(flatten)]
device: RcDevice,
#[cfg(target_os = "linux")]
#[arg(
long = "learning-mode",
short = 'l',
global = true,
help_heading = "DEVICE"
)]
learning: bool,
#[arg(
long = "file",
short = 'f',
global = true,
name = "FILE",
help_heading = "INPUT"
)]
file: Vec<OsString>,
#[arg(
long = "raw",
short = 'r',
global = true,
name = "RAWIR",
help_heading = "INPUT"
)]
rawir: Vec<String>,
#[arg(long = "irp", short = 'i')]
irp: Vec<String>,
#[arg(long = "keymap", short = 'k')]
keymap: Vec<PathBuf>,
#[arg(long = "builtin-kernel-protocols", short = 'b')]
linux_kernel: bool,
#[clap(flatten)]
options: DecodeOptions,
}
#[derive(Args)]
struct DecodeOptions {
#[arg(
long = "absolute-tolerance",
value_parser = value_parser!(u32).range(0..100000),
global = true,
name = "AEPS",
help_heading = "DECODING"
)]
aeps: Option<u32>,
#[arg(
long = "relative-tolerance",
value_parser = value_parser!(u32).range(0..1000),
global = true,
name = "EPS",
help_heading = "DECODING"
)]
eps: Option<u32>,
#[arg(long = "save-nfa", global = true, help_heading = "DEBUGGING")]
save_nfa: bool,
#[arg(long = "save-dfa", global = true, help_heading = "DEBUGGING")]
save_dfa: bool,
}
#[derive(Args)]
struct BpfDecodeOptions {
#[arg(long = "save-llvm-ir", help_heading = "DEBUGGING")]
save_llvm_ir: bool,
#[arg(long = "save-asm", help_heading = "DEBUGGING")]
save_assembly: bool,
#[arg(long = "save-object", help_heading = "DEBUGGING")]
save_object: bool,
}
#[cfg(target_os = "linux")]
#[derive(Args)]
struct RcDevice {
#[arg(
long = "device",
short = 'd',
conflicts_with = "RCDEV",
name = "LIRCDEV",
global = true,
help_heading = "DEVICE"
)]
lirc_dev: Option<String>,
#[arg(
long = "rcdev",
short = 's',
conflicts_with = "LIRCDEV",
name = "RCDEV",
global = true,
help_heading = "DEVICE"
)]
rc_dev: Option<String>,
}
#[cfg(target_os = "linux")]
#[derive(Args)]
struct List {
#[cfg(target_os = "linux")]
#[clap(flatten)]
device: RcDevice,
#[arg(long = "read-mapping", short = 'r')]
mapping: bool,
}
#[cfg(target_os = "linux")]
fn parse_scankey(arg: &str) -> Result<(u64, String), String> {
if let Some((scancode, keycode)) = arg.split_once([':', '=']) {
let scancode = if let Some(hex) = scancode.strip_prefix("0x") {
u64::from_str_radix(hex, 16)
} else {
str::parse(scancode)
}
.map_err(|e| format!("{e}"))?;
Ok((scancode, keycode.to_owned()))
} else {
Err("missing `=` separator".into())
}
}
fn parse_scancode(arg: &str) -> Result<(String, u64), String> {
if let Some((protocol, scancode)) = arg.split_once(':') {
let scancode = if let Some(hex) = scancode.strip_prefix("0x") {
u64::from_str_radix(hex, 16)
} else {
str::parse(scancode)
}
.map_err(|e| format!("{e}"))?;
Ok((protocol.to_owned(), scancode))
} else {
Err("missing `:` separator".into())
}
}
#[cfg(target_os = "linux")]
#[derive(Args)]
struct Keymap {
#[clap(flatten)]
device: RcDevice,
#[arg(
long = "auto-load",
short = 'a',
conflicts_with_all = ["clear", "KEYMAP", "IRP", "PROTOCOL", "SCANKEY"],
num_args = 0..=1,
default_missing_value = "/etc/rc_maps.cfg",
required_unless_present_any = ["auto_load", "clear", "DELAY", "KEYMAP", "IRP", "PERIOD", "PROTOCOL", "SCANKEY"]
)]
auto_load: Option<PathBuf>,
#[arg(
long = "clear",
short = 'c',
required_unless_present_any = ["auto_load", "clear", "DELAY", "KEYMAP", "IRP", "PERIOD", "PROTOCOL", "SCANKEY"]
)]
clear: bool,
#[arg(
long = "timeout",
short = 't',
required_unless_present_any = ["auto_load", "clear", "DELAY", "KEYMAP", "IRP", "PERIOD", "PROTOCOL", "SCANKEY"]
)]
timeout: Option<u32>,
#[arg(
long = "delay",
short = 'D',
name = "DELAY",
required_unless_present_any = ["auto_load", "clear", "DELAY", "KEYMAP", "IRP", "PERIOD", "PROTOCOL", "SCANKEY"]
)]
delay: Option<u32>,
#[arg(
long = "period",
short = 'P',
name = "PERIOD",
required_unless_present_any = ["auto_load", "clear", "DELAY", "KEYMAP", "IRP", "PERIOD", "PROTOCOL", "SCANKEY"]
)]
period: Option<u32>,
#[arg(
long = "irp",
short = 'i',
name = "IRP",
required_unless_present_any = ["auto_load", "clear", "DELAY", "KEYMAP", "IRP", "PERIOD", "PROTOCOL", "SCANKEY"]
)]
irp: Option<String>,
#[arg(
long = "protocol",
short = 'p',
value_delimiter = ',',
name = "PROTOCOL",
required_unless_present_any = ["auto_load", "clear", "DELAY", "KEYMAP", "IRP", "PERIOD", "PROTOCOL", "SCANKEY"]
)]
protocol: Vec<String>,
#[arg(
long = "set-key",
short = 'k',
value_parser = parse_scankey,
value_delimiter = ',',
name = "SCANKEY",
required_unless_present_any = ["auto_load", "clear", "DELAY", "KEYMAP", "IRP", "PERIOD", "PROTOCOL", "SCANKEY"]
)]
scankey: Vec<(u64, String)>,
#[arg(
long = "write",
short = 'w',
name = "KEYMAP",
required_unless_present_any = ["auto_load", "clear", "DELAY", "KEYMAP", "IRP", "PERIOD", "PROTOCOL", "SCANKEY"]
)]
write: Vec<PathBuf>,
#[clap(flatten)]
options: DecodeOptions,
#[clap(flatten)]
bpf_options: BpfDecodeOptions,
}
#[cfg(target_os = "linux")]
#[derive(Args)]
struct Test {
#[cfg(target_os = "linux")]
#[clap(flatten)]
device: RcDevice,
#[arg(long = "learning", short = 'l')]
learning: bool,
#[arg(long = "timeout", short = 't')]
timeout: Option<u32>,
#[arg(long = "one-shot", short = '1')]
one_shot: bool,
#[arg(long = "raw", short = 'r')]
raw: bool,
}
#[cfg(target_os = "linux")]
#[derive(Args)]
struct Auto {
#[clap(flatten)]
device: RcDevice,
#[arg(name = "CFGFILE", default_value = "/etc/rc_maps.cfg")]
cfgfile: PathBuf,
}
#[derive(Args)]
struct Transmit {
#[cfg(target_os = "linux")]
#[clap(flatten)]
device: RcDevice,
#[cfg(target_os = "linux")]
#[arg(
long = "transmitters",
short = 'e',
value_delimiter = ',',
help_heading = "DEVICE"
)]
transmitters: Vec<u32>,
#[arg(long = "dry-run", short = 'n')]
dry_run: bool,
#[arg(long = "list-codes", short = 'l', requires = "KEYMAP")]
list_codes: bool,
#[arg(long = "file", short = 'f', name = "FILE", help_heading = "INPUT")]
files: Vec<OsString>,
#[arg(
long = "scancode",
short = 'S',
name = "SCANCODE",
value_parser = parse_scancode,
help_heading = "INPUT"
)]
scancodes: Vec<(String, u64)>,
#[arg(long = "gap", short = 'g', name = "GAP", help_heading = "INPUT")]
gaps: Vec<u32>,
#[arg(long = "pronto", short = 'p', name = "PRONTO", help_heading = "INPUT")]
pronto: Vec<String>,
#[arg(long = "raw", short = 'r', name = "RAWIR", help_heading = "INPUT")]
rawir: Vec<String>,
#[arg(
long = "repeats",
short = 'R',
value_parser = value_parser!(u64).range(0..99),
default_value_t = 0,
help_heading = "INPUT"
)]
repeats: u64,
#[arg(
long = "argument",
short = 'a',
value_delimiter = ',',
help_heading = "INPUT",
name = "ARGUMENT"
)]
arguments: Vec<String>,
#[arg(long = "irp", short = 'i', name = "IRP", help_heading = "INPUT")]
irp: Vec<String>,
#[arg(name = "KEYMAP", long = "keymap", short = 'k', help_heading = "INPUT")]
keymap: Option<PathBuf>,
#[arg(name = "REMOTE", long = "remote", short = 'm', help_heading = "INPUT")]
remote: Option<String>,
#[arg(name = "CODE", long = "keycode", short = 'K', help_heading = "INPUT")]
codes: Vec<String>,
#[cfg(target_os = "linux")]
#[arg(long = "carrier", short = 'c', value_parser = value_parser!(i64).range(1..1_000_000), help_heading = "DEVICE")]
carrier: Option<i64>,
#[cfg(target_os = "linux")]
#[arg(long = "duty-cycle", short = 'u', value_parser = value_parser!(u8).range(1..99), help_heading = "DEVICE")]
duty_cycle: Option<u8>,
#[arg(skip)]
transmitables: Vec<Transmitables>,
}
impl Transmit {
fn transmitables(&mut self, matches: &ArgMatches) {
let mut part = Vec::new();
macro_rules! arg {
($id:literal, $ty:ty, $transmitable:ident) => {{}
if let Some(values) = matches.get_many::<$ty>($id) {
let mut indices = matches.indices_of($id).unwrap();
for value in values {
part.push((
Transmitables::$transmitable(value.clone()),
indices.next().unwrap(),
));
}
}};
}
arg!("FILE", OsString, File);
arg!("RAWIR", String, RawIR);
arg!("PRONTO", String, Pronto);
arg!("CODE", String, Code);
arg!("IRP", String, Irp);
arg!("GAP", u32, Gap);
arg!("SCANCODE", (String, u64), Scancode);
part.sort_by(|a, b| a.1.cmp(&b.1));
self.transmitables = part.into_iter().map(|(t, _)| t).collect();
}
}
enum Transmitables {
File(OsString),
RawIR(String),
Pronto(String),
Code(String),
Irp(String),
Gap(u32),
Scancode((String, u64)),
}
impl FromArgMatches for Commands {
fn from_arg_matches(matches: &ArgMatches) -> Result<Self, Error> {
match matches.subcommand() {
Some(("decode", args)) => Ok(Self::Decode(Decode::from_arg_matches(args)?)),
Some(("transmit", args)) => {
let mut tx = Transmit::from_arg_matches(args)?;
tx.transmitables(args);
Ok(Self::Transmit(tx))
}
#[cfg(target_os = "linux")]
Some(("list", args)) => Ok(Self::List(List::from_arg_matches(args)?)),
#[cfg(target_os = "linux")]
Some(("keymap", args)) => Ok(Self::Keymap(Keymap::from_arg_matches(args)?)),
#[cfg(target_os = "linux")]
Some(("test", args)) => Ok(Self::Test(Test::from_arg_matches(args)?)),
Some((_, _)) => Err(Error::raw(
ErrorKind::InvalidSubcommand,
"Valid subcommands are `decode`, `transmit`, `list`, `keymap`, and `test`",
)),
None => Err(Error::raw(
ErrorKind::MissingSubcommand,
"Valid subcommands are `decode`, `transmit`, `list`, `keymap`, and `test``",
)),
}
}
fn update_from_arg_matches(&mut self, matches: &ArgMatches) -> Result<(), Error> {
match matches.subcommand() {
Some(("decode", args)) => *self = Self::Decode(Decode::from_arg_matches(args)?),
Some(("transmit", args)) => {
let mut tx = Transmit::from_arg_matches(args)?;
tx.transmitables(args);
*self = Self::Transmit(tx);
}
#[cfg(target_os = "linux")]
Some(("list", args)) => *self = Self::List(List::from_arg_matches(args)?),
#[cfg(target_os = "linux")]
Some(("keymap", args)) => *self = Self::Keymap(Keymap::from_arg_matches(args)?),
#[cfg(target_os = "linux")]
Some(("test", args)) => *self = Self::Test(Test::from_arg_matches(args)?),
Some((_, _)) => {
return Err(Error::raw(
ErrorKind::InvalidSubcommand,
"Valid subcommands are `decode`, `transmit`, `list`, `keymap`, and `test`",
))
}
None => (),
}
Ok(())
}
}
impl Subcommand for Commands {
#[allow(clippy::let_and_return)]
fn augment_subcommands(cmd: Command) -> Command {
let cmd = cmd
.subcommand(Decode::augment_args(
Command::new("decode").about("Decode IR"),
))
.subcommand(Transmit::augment_args(
Command::new("transmit").about("Transmit IR"),
));
#[cfg(target_os = "linux")]
let cmd = cmd
.subcommand(List::augment_args(
Command::new("list").about("List IR and CEC devices"),
))
.subcommand(Keymap::augment_args(
Command::new("keymap").about("Configure IR and CEC devices"),
))
.subcommand(Test::augment_args(
Command::new("test").about("Receive IR and print to stdout"),
));
cmd
}
#[allow(clippy::let_and_return)]
fn augment_subcommands_for_update(cmd: Command) -> Command {
let cmd = cmd
.subcommand(Decode::augment_args(
Command::new("decode").about("Decode IR"),
))
.subcommand(Transmit::augment_args(
Command::new("transmit").about("Transmit IR"),
));
#[cfg(target_os = "linux")]
let cmd = cmd
.subcommand(List::augment_args(
Command::new("list").about("List IR and CEC devices"),
))
.subcommand(List::augment_args(
Command::new("keymap").about("Configure IR and CEC devices"),
))
.subcommand(List::augment_args(
Command::new("test").about("Receive IR and print to stdout"),
));
cmd
}
fn has_subcommand(_name: &str) -> bool {
false
}
}
fn main() {
let args = App::parse();
log::set_logger(&CLI_LOGGER).unwrap();
let level = if args.quiet {
LevelFilter::Error
} else {
match args.verbose {
0 => LevelFilter::Info,
1 => LevelFilter::Debug,
_ => LevelFilter::Trace,
}
};
log::set_max_level(level);
match &args.command {
Commands::Decode(decode) => commands::decode::decode(&args, decode),
Commands::Transmit(tx) => commands::transmit::transmit(&args, tx),
#[cfg(target_os = "linux")]
Commands::List(args) => commands::list::list(args),
#[cfg(target_os = "linux")]
Commands::Keymap(args) => commands::keymap::keymap(args),
#[cfg(target_os = "linux")]
Commands::Test(args) => commands::test::test(args),
}
}
static IRP_PROTOCOLS: OnceLock<io::Result<Vec<Protocol>>> = OnceLock::new();
fn get_irp_protocols(path: &Path) -> &'static io::Result<Vec<Protocol>> {
IRP_PROTOCOLS.get_or_init(|| Protocol::parse(path))
}
static CLI_LOGGER: CliLogger = CliLogger;
struct CliLogger;
impl log::Log for CliLogger {
fn enabled(&self, _metadata: &Metadata) -> bool {
true
}
fn log(&self, record: &Record) {
if self.enabled(record.metadata()) {
eprintln!(
"{}: {}",
match record.level() {
Level::Trace => "trace",
Level::Debug => "debug",
Level::Info => "info",
Level::Warn => "warn",
Level::Error => "error",
},
record.args()
);
}
}
fn flush(&self) {}
}