use lillinput::actions::ActionType;
use lillinput::events::ActionEvent;
use clap::error::ErrorKind;
use clap::Parser;
use clap_verbosity_flag::{InfoLevel, Verbosity};
use serde::{Deserialize, Serialize};
use std::fmt;
use std::str::FromStr;
use strum::VariantNames;
#[derive(Debug, Clone, Eq, PartialEq, Deserialize, Serialize)]
#[serde(try_from = "String")]
#[serde(into = "String")]
pub struct StringifiedAction {
pub type_: String,
pub command: String,
}
impl StringifiedAction {
#[must_use]
pub fn new(type_: &str, command: &str) -> Self {
Self {
type_: type_.to_string(),
command: command.to_string(),
}
}
}
#[allow(clippy::from_over_into)]
impl Into<String> for StringifiedAction {
fn into(self) -> String {
format!("{}", self)
}
}
impl TryFrom<String> for StringifiedAction {
type Error = clap::Error;
fn try_from(value: String) -> Result<Self, Self::Error> {
Self::from_str(&value)
}
}
impl FromStr for StringifiedAction {
type Err = clap::Error;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s.split_once(':') {
None | Some((_, "") | ("", _)) => Err(clap::Error::raw(
ErrorKind::ValueValidation,
"The value does not conform to the action string pattern `{type}:{command}`",
)),
Some((action_type, action_command)) => {
if ActionType::VARIANTS.iter().any(|s| s == &action_type) {
Ok(Self {
type_: action_type.into(),
command: action_command.into(),
})
} else {
Err(clap::Error::raw(
ErrorKind::ValueValidation,
format!(
"The value does not start with a valid action ({:?})",
ActionType::VARIANTS
),
))
}
}
}
}
}
impl fmt::Display for StringifiedAction {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "{}:{}", self.type_, self.command)
}
}
#[derive(Parser, Debug, Clone)]
#[clap(version = env!("CARGO_PKG_VERSION"), author = env!("CARGO_PKG_AUTHORS"))]
pub struct Opts {
#[clap(short, long)]
pub config_file: Option<String>,
#[clap(flatten)]
pub verbose: Verbosity<InfoLevel>,
#[clap(short, long)]
pub seat: Option<String>,
#[clap(short, long, possible_values = ActionType::VARIANTS)]
pub enabled_action_types: Option<Vec<String>>,
#[clap(short, long)]
pub threshold: Option<f64>,
#[clap(long)]
pub three_finger_swipe_left: Option<Vec<StringifiedAction>>,
#[clap(long)]
pub three_finger_swipe_left_up: Option<Vec<StringifiedAction>>,
#[clap(long)]
pub three_finger_swipe_up: Option<Vec<StringifiedAction>>,
#[clap(long)]
pub three_finger_swipe_right_up: Option<Vec<StringifiedAction>>,
#[clap(long)]
pub three_finger_swipe_right: Option<Vec<StringifiedAction>>,
#[clap(long)]
pub three_finger_swipe_right_down: Option<Vec<StringifiedAction>>,
#[clap(long)]
pub three_finger_swipe_down: Option<Vec<StringifiedAction>>,
#[clap(long)]
pub three_finger_swipe_left_down: Option<Vec<StringifiedAction>>,
#[clap(long)]
pub four_finger_swipe_left: Option<Vec<StringifiedAction>>,
#[clap(long)]
pub four_finger_swipe_left_up: Option<Vec<StringifiedAction>>,
#[clap(long)]
pub four_finger_swipe_up: Option<Vec<StringifiedAction>>,
#[clap(long)]
pub four_finger_swipe_right_up: Option<Vec<StringifiedAction>>,
#[clap(long)]
pub four_finger_swipe_right: Option<Vec<StringifiedAction>>,
#[clap(long)]
pub four_finger_swipe_right_down: Option<Vec<StringifiedAction>>,
#[clap(long)]
pub four_finger_swipe_down: Option<Vec<StringifiedAction>>,
#[clap(long)]
pub four_finger_swipe_left_down: Option<Vec<StringifiedAction>>,
#[clap(long)]
pub invert_x: Option<bool>,
#[clap(long)]
pub invert_y: Option<bool>,
}
impl Opts {
#[must_use]
pub fn get_actions_for_event(
&self,
action_event: ActionEvent,
) -> Option<&Vec<StringifiedAction>> {
match action_event {
ActionEvent::ThreeFingerSwipeLeft => self.three_finger_swipe_left.as_ref(),
ActionEvent::ThreeFingerSwipeLeftUp => self.three_finger_swipe_left_up.as_ref(),
ActionEvent::ThreeFingerSwipeUp => self.three_finger_swipe_up.as_ref(),
ActionEvent::ThreeFingerSwipeRightUp => self.three_finger_swipe_right_up.as_ref(),
ActionEvent::ThreeFingerSwipeRight => self.three_finger_swipe_right.as_ref(),
ActionEvent::ThreeFingerSwipeRightDown => self.three_finger_swipe_right_down.as_ref(),
ActionEvent::ThreeFingerSwipeDown => self.three_finger_swipe_down.as_ref(),
ActionEvent::ThreeFingerSwipeLeftDown => self.three_finger_swipe_left_down.as_ref(),
ActionEvent::FourFingerSwipeLeft => self.four_finger_swipe_left.as_ref(),
ActionEvent::FourFingerSwipeLeftUp => self.four_finger_swipe_left_up.as_ref(),
ActionEvent::FourFingerSwipeUp => self.four_finger_swipe_up.as_ref(),
ActionEvent::FourFingerSwipeRightUp => self.four_finger_swipe_right_up.as_ref(),
ActionEvent::FourFingerSwipeRight => self.four_finger_swipe_right.as_ref(),
ActionEvent::FourFingerSwipeRightDown => self.four_finger_swipe_right_down.as_ref(),
ActionEvent::FourFingerSwipeDown => self.four_finger_swipe_down.as_ref(),
ActionEvent::FourFingerSwipeLeftDown => self.four_finger_swipe_left_down.as_ref(),
}
}
}
#[cfg(test)]
mod test {
use super::*;
use crate::settings::{setup_application, Settings};
use crate::test_utils::default_test_settings;
use clap::Parser;
use simplelog::LevelFilter;
use std::env;
use std::fs::{create_dir, File};
use std::io::Write;
use tempfile::Builder;
#[test]
#[should_panic(expected = "The value does not conform to the action string pattern")]
fn test_action_argument_invalid_pattern() {
Opts::try_parse_from(["lillinput", "--three-finger-swipe-left", "invalid"]).unwrap();
}
#[test]
#[should_panic(expected = "The value does not start with a valid action")]
fn test_action_argument_invalid_action_string() {
Opts::try_parse_from(["lillinput", "--three-finger-swipe-left", "invalid:bar"]).unwrap();
}
#[test]
fn test_action_argument_valid_action_string() {
let opts: Opts = Opts::parse_from(["lillinput", "--three-finger-swipe-left", "i3:foo"]);
assert_eq!(
opts.three_finger_swipe_left.unwrap(),
vec![StringifiedAction::from_str("i3:foo").unwrap()]
);
}
#[test]
#[should_panic(expected = "InvalidValue")]
fn test_enabled_action_types_argument_invalid() {
Opts::try_parse_from(["lillinput", "--enabled-action-types", "invalid"]).unwrap();
}
#[test]
fn test_opts_to_settings() {
let opts: Opts = Opts::parse_from([
"lillinput",
"--config-file",
"nonexisting.file",
"--seat",
"some.seat",
"--verbose",
"--verbose",
"--enabled-action-types",
"i3",
"--threshold",
"20",
"--three-finger-swipe-left",
"command:bar",
"--three-finger-swipe-left",
"i3:3left",
"--three-finger-swipe-left-up",
"i3:3left-up",
"--three-finger-swipe-up",
"i3:3up",
"--three-finger-swipe-right-up",
"i3:3right-up",
"--three-finger-swipe-right",
"i3:3right",
"--three-finger-swipe-right-down",
"i3:3right-down",
"--three-finger-swipe-down",
"i3:3down",
"--three-finger-swipe-left-down",
"i3:3left-down",
"--four-finger-swipe-left",
"i3:4left",
"--four-finger-swipe-left-up",
"i3:4left-up",
"--four-finger-swipe-up",
"i3:4up",
"--four-finger-swipe-right-up",
"i3:4right-up",
"--four-finger-swipe-right",
"i3:4right",
"--four-finger-swipe-right-down",
"i3:4right-down",
"--four-finger-swipe-down",
"i3:4down",
"--four-finger-swipe-left-down",
"i3:4left-down",
]);
let converted_settings: Settings = setup_application(opts, false).unwrap();
let mut expected_settings = default_test_settings();
expected_settings.verbose = LevelFilter::Trace;
expected_settings.seat = String::from("some.seat");
expected_settings.enabled_action_types = vec![ActionType::I3.to_string()];
expected_settings.threshold = 20.0;
for (event, command) in vec![
(ActionEvent::ThreeFingerSwipeLeft.to_string(), "3left"),
(ActionEvent::ThreeFingerSwipeLeftUp.to_string(), "3left-up"),
(ActionEvent::ThreeFingerSwipeUp.to_string(), "3up"),
(
ActionEvent::ThreeFingerSwipeRightUp.to_string(),
"3right-up",
),
(ActionEvent::ThreeFingerSwipeRight.to_string(), "3right"),
(
ActionEvent::ThreeFingerSwipeRightDown.to_string(),
"3right-down",
),
(ActionEvent::ThreeFingerSwipeDown.to_string(), "3down"),
(
ActionEvent::ThreeFingerSwipeLeftDown.to_string(),
"3left-down",
),
(ActionEvent::FourFingerSwipeLeft.to_string(), "4left"),
(ActionEvent::FourFingerSwipeLeftUp.to_string(), "4left-up"),
(ActionEvent::FourFingerSwipeUp.to_string(), "4up"),
(ActionEvent::FourFingerSwipeRightUp.to_string(), "4right-up"),
(ActionEvent::FourFingerSwipeRight.to_string(), "4right"),
(
ActionEvent::FourFingerSwipeRightDown.to_string(),
"4right-down",
),
(ActionEvent::FourFingerSwipeDown.to_string(), "4down"),
(
ActionEvent::FourFingerSwipeLeftDown.to_string(),
"4left-down",
),
] {
expected_settings
.actions
.insert(event, vec![StringifiedAction::new("i3", command)]);
}
assert_eq!(converted_settings, expected_settings);
}
#[test]
fn test_config_file() {
let mut file = Builder::new().suffix(".toml").tempfile().unwrap();
let file_path = String::from(file.path().to_str().unwrap());
writeln!(
file,
r#"
verbose = "DEBUG"
seat = "some.seat"
threshold = 42.0
enabled_action_types = ["i3"]
[actions]
three-finger-swipe-right = ["i3:foo"]
three-finger-swipe-left = []
four-finger-swipe-right = ["i3:bar", "command:baz"]
"#
)
.unwrap();
let opts: Opts = Opts::parse_from(["lillinput", "--config-file", &file_path]);
let converted_settings: Settings = setup_application(opts, false).unwrap();
let mut expected_settings = default_test_settings();
expected_settings.verbose = LevelFilter::Debug;
expected_settings.seat = String::from("some.seat");
expected_settings.enabled_action_types = vec![ActionType::I3.to_string()];
expected_settings.threshold = 42.0;
expected_settings.actions.insert(
ActionEvent::ThreeFingerSwipeRight.to_string(),
vec![StringifiedAction::new("i3", "foo")],
);
expected_settings.actions.insert(
ActionEvent::FourFingerSwipeRight.to_string(),
vec![StringifiedAction::new("i3", "bar")],
);
assert_eq!(converted_settings, expected_settings);
}
#[test]
fn test_config_file_from_xdg_config_home() {
let tmp_dir = Builder::new().prefix("lillinput-conf").tempdir().unwrap();
create_dir(tmp_dir.path().join("lillinput")).unwrap();
env::set_var("XDG_CONFIG_HOME", tmp_dir.path());
let config_home_file_path = tmp_dir.path().join("lillinput").join("lillinput.toml");
let mut config_home_file = File::create(config_home_file_path).unwrap();
writeln!(
config_home_file,
r#"
verbose = "DEBUG"
seat = "some.seat"
threshold = 42.0
enabled_action_types = ["i3"]
invert_x = true
[actions]
three-finger-swipe-right = ["i3:foo"]
three-finger-swipe-left = []
four-finger-swipe-right = ["i3:bar", "command:baz"]
"#
)
.unwrap();
let opts: Opts = Opts::parse_from(["lillinput"]);
let converted_settings: Settings = setup_application(opts, false).unwrap();
let mut expected_settings = default_test_settings();
expected_settings.verbose = LevelFilter::Debug;
expected_settings.seat = String::from("some.seat");
expected_settings.enabled_action_types = vec![ActionType::I3.to_string()];
expected_settings.invert_x = true;
expected_settings.threshold = 42.0;
expected_settings.actions.insert(
ActionEvent::ThreeFingerSwipeRight.to_string(),
vec![StringifiedAction::new("i3", "foo")],
);
expected_settings.actions.insert(
ActionEvent::FourFingerSwipeRight.to_string(),
vec![StringifiedAction::new("i3", "bar")],
);
assert_eq!(converted_settings, expected_settings);
}
#[test]
fn test_config_overriding() {
let mut file = Builder::new().suffix(".toml").tempfile().unwrap();
let file_path = String::from(file.path().to_str().unwrap());
writeln!(
file,
r#"
seat = "seat.from.config"
threshold = 42.0
[actions]
three-finger-swipe-right = ["i3:right_from_config"]
three-finger-swipe-left = ["i3:left_from_config"]
"#
)
.unwrap();
let opts: Opts = Opts::parse_from([
"lillinput",
"--config-file",
&file_path,
"--threshold",
"99.9",
"--three-finger-swipe-left",
"i3:left_from_cli",
]);
let converted_settings: Settings = setup_application(opts, false).unwrap();
let mut expected_settings = Settings {
seat: String::from("seat.from.config"),
threshold: 99.9,
..Settings::default()
};
expected_settings.actions.insert(
ActionEvent::ThreeFingerSwipeRight.to_string(),
vec![StringifiedAction::new("i3", "right_from_config")],
);
expected_settings.actions.insert(
ActionEvent::ThreeFingerSwipeLeft.to_string(),
vec![StringifiedAction::new("i3", "left_from_cli")],
);
assert_eq!(converted_settings, expected_settings);
}
}