add initial parse phase for add subcommand (keybind), improve key parsing
This commit is contained in:
		
							parent
							
								
									d8194cf78b
								
							
						
					
					
						commit
						3e6a944df8
					
				| 
						 | 
				
			
			@ -257,6 +257,15 @@ version = "0.6.0"
 | 
			
		|||
source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
			
		||||
checksum = "702fc72eb24e5a1e48ce58027a675bc24edd52096d5397d4aea7c6dd9eca0bd1"
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "clipboard-win"
 | 
			
		||||
version = "5.2.0"
 | 
			
		||||
source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
			
		||||
checksum = "12f9a0700e0127ba15d1d52dd742097f821cd9c65939303a44d970465040a297"
 | 
			
		||||
dependencies = [
 | 
			
		||||
 "error-code",
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "cnx"
 | 
			
		||||
version = "0.3.0"
 | 
			
		||||
| 
						 | 
				
			
			@ -309,6 +318,12 @@ version = "1.10.0"
 | 
			
		|||
source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
			
		||||
checksum = "11157ac094ffbdde99aa67b23417ebdd801842852b500e395a45a9c0aac03e4a"
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "endian-type"
 | 
			
		||||
version = "0.1.2"
 | 
			
		||||
source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
			
		||||
checksum = "c34f04666d835ff5d62e058c3995147c06f42fe86ff053337632bca83e42702d"
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "env_logger"
 | 
			
		||||
version = "0.10.2"
 | 
			
		||||
| 
						 | 
				
			
			@ -338,6 +353,23 @@ dependencies = [
 | 
			
		|||
 "windows-sys 0.52.0",
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "error-code"
 | 
			
		||||
version = "3.2.0"
 | 
			
		||||
source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
			
		||||
checksum = "a0474425d51df81997e2f90a21591180b38eccf27292d755f3e30750225c175b"
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "fd-lock"
 | 
			
		||||
version = "4.0.2"
 | 
			
		||||
source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
			
		||||
checksum = "7e5768da2206272c81ef0b5e951a41862938a6070da63bcea197899942d3b947"
 | 
			
		||||
dependencies = [
 | 
			
		||||
 "cfg-if",
 | 
			
		||||
 "rustix",
 | 
			
		||||
 "windows-sys 0.52.0",
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "futures"
 | 
			
		||||
version = "0.3.30"
 | 
			
		||||
| 
						 | 
				
			
			@ -553,6 +585,7 @@ dependencies = [
 | 
			
		|||
 "paste",
 | 
			
		||||
 "pretty_assertions",
 | 
			
		||||
 "pretty_env_logger",
 | 
			
		||||
 "rustyline",
 | 
			
		||||
 "serde",
 | 
			
		||||
 "serde_json",
 | 
			
		||||
 "strum",
 | 
			
		||||
| 
						 | 
				
			
			@ -685,6 +718,26 @@ dependencies = [
 | 
			
		|||
 "windows-sys 0.48.0",
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "nibble_vec"
 | 
			
		||||
version = "0.1.0"
 | 
			
		||||
source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
			
		||||
checksum = "77a5d83df9f36fe23f0c3648c6bbb8b0298bb5f1939c8f2704431371f4b84d43"
 | 
			
		||||
dependencies = [
 | 
			
		||||
 "smallvec",
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "nix"
 | 
			
		||||
version = "0.27.1"
 | 
			
		||||
source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
			
		||||
checksum = "2eb04e9c688eff1c89d72b407f168cf79bb9e867a9d3323ed6c01519eb9cc053"
 | 
			
		||||
dependencies = [
 | 
			
		||||
 "bitflags 2.4.2",
 | 
			
		||||
 "cfg-if",
 | 
			
		||||
 "libc",
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "num-traits"
 | 
			
		||||
version = "0.2.18"
 | 
			
		||||
| 
						 | 
				
			
			@ -877,6 +930,16 @@ dependencies = [
 | 
			
		|||
 "proc-macro2",
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "radix_trie"
 | 
			
		||||
version = "0.2.1"
 | 
			
		||||
source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
			
		||||
checksum = "c069c179fcdc6a2fe24d8d18305cf085fdbd4f922c041943e203685d6a1c58fd"
 | 
			
		||||
dependencies = [
 | 
			
		||||
 "endian-type",
 | 
			
		||||
 "nibble_vec",
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "regex"
 | 
			
		||||
version = "1.10.3"
 | 
			
		||||
| 
						 | 
				
			
			@ -931,6 +994,28 @@ version = "1.0.14"
 | 
			
		|||
source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
			
		||||
checksum = "7ffc183a10b4478d04cbbbfc96d0873219d962dd5accaff2ffbd4ceb7df837f4"
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "rustyline"
 | 
			
		||||
version = "13.0.0"
 | 
			
		||||
source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
			
		||||
checksum = "02a2d683a4ac90aeef5b1013933f6d977bd37d51ff3f4dad829d4931a7e6be86"
 | 
			
		||||
dependencies = [
 | 
			
		||||
 "bitflags 2.4.2",
 | 
			
		||||
 "cfg-if",
 | 
			
		||||
 "clipboard-win",
 | 
			
		||||
 "fd-lock",
 | 
			
		||||
 "home",
 | 
			
		||||
 "libc",
 | 
			
		||||
 "log",
 | 
			
		||||
 "memchr",
 | 
			
		||||
 "nix",
 | 
			
		||||
 "radix_trie",
 | 
			
		||||
 "unicode-segmentation",
 | 
			
		||||
 "unicode-width",
 | 
			
		||||
 "utf8parse",
 | 
			
		||||
 "winapi",
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "ryu"
 | 
			
		||||
version = "1.0.17"
 | 
			
		||||
| 
						 | 
				
			
			@ -1189,6 +1274,18 @@ version = "1.0.12"
 | 
			
		|||
source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
			
		||||
checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b"
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "unicode-segmentation"
 | 
			
		||||
version = "1.11.0"
 | 
			
		||||
source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
			
		||||
checksum = "d4c87d22b6e3f4a18d4d40ef354e97c90fcb14dd91d7dc0aa9d8a1172ebf7202"
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "unicode-width"
 | 
			
		||||
version = "0.1.11"
 | 
			
		||||
source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
			
		||||
checksum = "e51733f11c9c4f72aa0c160008246859e340b00807569a0da0e7a1079b27ba85"
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "utf8parse"
 | 
			
		||||
version = "0.2.1"
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -18,6 +18,7 @@ which = "6.0.0"
 | 
			
		|||
log = "0.4"
 | 
			
		||||
pretty_env_logger = "0.5"
 | 
			
		||||
cnx = { git = "https://github.com/mjkillough/cnx.git", rev = "7845d99baa296901171c083db61588a62f9a8b34" }
 | 
			
		||||
rustyline = "13.0.0"
 | 
			
		||||
 | 
			
		||||
[dev-dependencies]
 | 
			
		||||
pretty_assertions = "1.4.0"
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -0,0 +1,81 @@
 | 
			
		|||
use clap::Subcommand;
 | 
			
		||||
use rustyline::{error::ReadlineError, DefaultEditor};
 | 
			
		||||
use thiserror::Error;
 | 
			
		||||
 | 
			
		||||
use crate::hlwm::{
 | 
			
		||||
    command::{CommandParseError, HlwmCommand},
 | 
			
		||||
    key::Key,
 | 
			
		||||
    parser::{FromCommandArgs, ParseError},
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
#[derive(Subcommand, Debug, Clone)]
 | 
			
		||||
pub enum Add {
 | 
			
		||||
    /// Add a new keybind to the existing hlctl config
 | 
			
		||||
    #[clap(arg_required_else_help = true)]
 | 
			
		||||
    Keybind {
 | 
			
		||||
        #[arg(short, long)]
 | 
			
		||||
        interactive: bool,
 | 
			
		||||
    },
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#[derive(Debug, Clone, Error)]
 | 
			
		||||
pub enum Error {
 | 
			
		||||
    #[error("readline error: [{0}]")]
 | 
			
		||||
    ReadlineError(String),
 | 
			
		||||
    #[error("parse error: [{0}]")]
 | 
			
		||||
    KeyParseError(#[from] ParseError),
 | 
			
		||||
    #[error("could not parse command: [{0}]")]
 | 
			
		||||
    CommandParseError(#[from] CommandParseError),
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
impl From<ReadlineError> for Error {
 | 
			
		||||
    fn from(value: ReadlineError) -> Self {
 | 
			
		||||
        Self::ReadlineError(value.to_string())
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
impl Add {
 | 
			
		||||
    pub fn run(self) -> Result<(), Error> {
 | 
			
		||||
        match self {
 | 
			
		||||
            Add::Keybind { interactive } => {
 | 
			
		||||
                if interactive {
 | 
			
		||||
                    keybind_interactive()
 | 
			
		||||
                } else {
 | 
			
		||||
                    todo!()
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
fn keybind_interactive() -> Result<(), Error> {
 | 
			
		||||
    let mut read = DefaultEditor::new()?;
 | 
			
		||||
    let keys = read
 | 
			
		||||
        .readline("Keys (Separated by spaces, tabs, +, or -): ")?
 | 
			
		||||
        .trim()
 | 
			
		||||
        .split([' ', '\t', '+', '-'])
 | 
			
		||||
        .map(|l| l.trim())
 | 
			
		||||
        .filter(|l| !l.is_empty())
 | 
			
		||||
        .map(|k| Key::from_str_case_insensitive(k))
 | 
			
		||||
        .collect::<Result<Vec<_>, _>>()?;
 | 
			
		||||
 | 
			
		||||
    let command = {
 | 
			
		||||
        let read = read.readline("Command (Arguments separated by tabs or spaces): ")?;
 | 
			
		||||
        let mut args = read
 | 
			
		||||
            .trim()
 | 
			
		||||
            .split([' ', '\t'])
 | 
			
		||||
            .map(|a| a.trim())
 | 
			
		||||
            .filter(|a| !a.is_empty())
 | 
			
		||||
            .map(|a| a.to_string());
 | 
			
		||||
        match args.next() {
 | 
			
		||||
            Some(cmd) => HlwmCommand::from_command_args(&cmd, args)?,
 | 
			
		||||
            None => {
 | 
			
		||||
                println!("No command provided");
 | 
			
		||||
                std::process::exit(1);
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    println!("{keys:?} -> {command:?}");
 | 
			
		||||
    Ok(())
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -962,6 +962,9 @@ mod test {
 | 
			
		|||
            value: Attribute::Color(Color::X11(X11Color::NavajoWhite3)),
 | 
			
		||||
        });
 | 
			
		||||
        let cfg_serialized = cfg.serialize().unwrap();
 | 
			
		||||
        println!("--> Config <--");
 | 
			
		||||
        println!("{cfg_serialized}");
 | 
			
		||||
        println!("--> Config <--");
 | 
			
		||||
        let parsed_cfg = Config::deserialize(&cfg_serialized).unwrap();
 | 
			
		||||
 | 
			
		||||
        assert_eq!(cfg, parsed_cfg,)
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -277,7 +277,7 @@ impl Default for HlwmCommand {
 | 
			
		|||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#[derive(Debug, Error)]
 | 
			
		||||
#[derive(Debug, Clone, Error)]
 | 
			
		||||
pub enum CommandParseError {
 | 
			
		||||
    #[error("invalid argument count [{0}] at [{1}]")]
 | 
			
		||||
    InvalidArgumentCount(usize, String),
 | 
			
		||||
| 
						 | 
				
			
			@ -286,11 +286,17 @@ pub enum CommandParseError {
 | 
			
		|||
    #[error("command execution error: [{0}]")]
 | 
			
		||||
    CommandError(#[from] CommandError),
 | 
			
		||||
    #[error("string utf8 error")]
 | 
			
		||||
    StringUtf8Error(#[from] FromUtf8Error),
 | 
			
		||||
    StringUtf8Error(String),
 | 
			
		||||
    #[error("parsing error: [{0}]")]
 | 
			
		||||
    StringParseError(#[from] ParseError),
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
impl From<FromUtf8Error> for CommandParseError {
 | 
			
		||||
    fn from(value: FromUtf8Error) -> Self {
 | 
			
		||||
        Self::StringUtf8Error(value.to_string())
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
impl serde::de::Expected for CommandParseError {
 | 
			
		||||
    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
 | 
			
		||||
        write!(f, "expected valid command string. Got error: {self}")
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
							
								
								
									
										185
									
								
								src/hlwm/key.rs
								
								
								
								
							
							
						
						
									
										185
									
								
								src/hlwm/key.rs
								
								
								
								
							| 
						 | 
				
			
			@ -3,7 +3,7 @@ use std::{borrow::BorrowMut, fmt::Display, str::FromStr};
 | 
			
		|||
use serde::{Deserialize, Serialize};
 | 
			
		||||
use strum::IntoEnumIterator;
 | 
			
		||||
 | 
			
		||||
use crate::split;
 | 
			
		||||
use crate::{logerr::UnwrapLog, split};
 | 
			
		||||
 | 
			
		||||
use super::{
 | 
			
		||||
    command::HlwmCommand,
 | 
			
		||||
| 
						 | 
				
			
			@ -43,6 +43,21 @@ pub enum Key {
 | 
			
		|||
    Mouse(MouseButton),
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
fn title_case(s: &str) -> String {
 | 
			
		||||
    let mut chars = s.chars();
 | 
			
		||||
    let mut out_chars = Vec::with_capacity(s.len());
 | 
			
		||||
 | 
			
		||||
    if let Some(first) = chars.next() {
 | 
			
		||||
        out_chars.push(first.to_ascii_uppercase());
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    while let Some(c) = chars.next() {
 | 
			
		||||
        out_chars.push(c.to_ascii_lowercase());
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    out_chars.into_iter().collect()
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
impl Key {
 | 
			
		||||
    pub fn is_standard(&self) -> bool {
 | 
			
		||||
        match self {
 | 
			
		||||
| 
						 | 
				
			
			@ -68,6 +83,57 @@ impl Key {
 | 
			
		|||
            .map(Key::from_str)
 | 
			
		||||
            .collect::<Result<Vec<_>, _>>()
 | 
			
		||||
    }
 | 
			
		||||
    pub fn from_str_case_insensitive(s: &str) -> Result<Self, ParseError> {
 | 
			
		||||
        Key::from_str(&title_case(s))
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// Returns the name for this key as it should appear in the config
 | 
			
		||||
    pub fn config_name(&self) -> String {
 | 
			
		||||
        self.aliases()
 | 
			
		||||
            .first()
 | 
			
		||||
            .expect_log("all keys should have at least one alias")
 | 
			
		||||
            .to_string()
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// Returns a list of aliases for the key
 | 
			
		||||
    pub fn aliases(&self) -> Vec<String> {
 | 
			
		||||
        match self {
 | 
			
		||||
            Key::Mod1Alt => vec!["Alt".into(), "Mod1".into()],
 | 
			
		||||
            Key::Mod4Super => vec!["Super".into(), "Mod4".into()],
 | 
			
		||||
            Key::Return => vec!["Return".into(), "Enter".into()],
 | 
			
		||||
            Key::Shift => vec!["Shift".into()],
 | 
			
		||||
            Key::Tab => vec!["Tab".into()],
 | 
			
		||||
            Key::Left => vec!["Left".into()],
 | 
			
		||||
            Key::Right => vec!["Right".into()],
 | 
			
		||||
            Key::Up => vec!["Up".into()],
 | 
			
		||||
            Key::Down => vec!["Down".into()],
 | 
			
		||||
            Key::Space => vec!["Space".into()],
 | 
			
		||||
            Key::Control => vec!["Ctrl".into(), "Ctl".into(), "Control".into()],
 | 
			
		||||
            Key::Backtick => vec!["Backtick".into(), "Grave".into()],
 | 
			
		||||
            Key::F1 => vec!["F1".into()],
 | 
			
		||||
            Key::F2 => vec!["F2".into()],
 | 
			
		||||
            Key::F3 => vec!["F3".into()],
 | 
			
		||||
            Key::F4 => vec!["F4".into()],
 | 
			
		||||
            Key::F5 => vec!["F5".into()],
 | 
			
		||||
            Key::F6 => vec!["F6".into()],
 | 
			
		||||
            Key::F7 => vec!["F7".into()],
 | 
			
		||||
            Key::F8 => vec!["F8".into()],
 | 
			
		||||
            Key::F9 => vec!["F9".into()],
 | 
			
		||||
            Key::F10 => vec!["F10".into()],
 | 
			
		||||
            Key::F11 => vec!["F11".into()],
 | 
			
		||||
            Key::F12 => vec!["F12".into()],
 | 
			
		||||
            Key::Home => vec!["Home".into()],
 | 
			
		||||
            Key::Delete => vec!["Delete".into(), "Del".into()],
 | 
			
		||||
            Key::Char(c) => vec![c.to_ascii_lowercase().to_string()],
 | 
			
		||||
            Key::Mouse(m) => vec![m.to_string()],
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
impl Display for Key {
 | 
			
		||||
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
 | 
			
		||||
        f.write_str(&self.aliases().first().unwrap())
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
impl From<MouseButton> for Key {
 | 
			
		||||
| 
						 | 
				
			
			@ -87,7 +153,7 @@ impl Serialize for Key {
 | 
			
		|||
    where
 | 
			
		||||
        S: serde::Serializer,
 | 
			
		||||
    {
 | 
			
		||||
        serializer.serialize_str(&self.to_string())
 | 
			
		||||
        serializer.serialize_str(&self.config_name())
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -103,7 +169,7 @@ impl<'de> Deserialize<'de> for Key {
 | 
			
		|||
            }
 | 
			
		||||
        }
 | 
			
		||||
        let str_val: String = Deserialize::deserialize(deserializer)?;
 | 
			
		||||
        Ok(Self::from_str(&str_val).map_err(|_| {
 | 
			
		||||
        Ok(Self::from_str_case_insensitive(&str_val).map_err(|_| {
 | 
			
		||||
            serde::de::Error::invalid_value(serde::de::Unexpected::Str(&str_val), &ExpectedKey)
 | 
			
		||||
        })?)
 | 
			
		||||
    }
 | 
			
		||||
| 
						 | 
				
			
			@ -118,14 +184,16 @@ impl FromStr for Key {
 | 
			
		|||
                Ok(Self::Mouse(MouseButton::from_str(s)?))
 | 
			
		||||
            }
 | 
			
		||||
            _ => {
 | 
			
		||||
                if let Some(key) = Self::iter().into_iter().find(|key| key.to_string() == s) {
 | 
			
		||||
                    match key {
 | 
			
		||||
                        Key::Char(_) | Key::Mouse(_) => (),
 | 
			
		||||
                        _ => return Ok(key),
 | 
			
		||||
                    }
 | 
			
		||||
                if let Some(key) = Self::iter().into_iter().find(|key| match key {
 | 
			
		||||
                    Key::Char(_) | Key::Mouse(_) => false,
 | 
			
		||||
                    _ => key.aliases().contains(&s.to_string()),
 | 
			
		||||
                }) {
 | 
			
		||||
                    return Ok(key);
 | 
			
		||||
                }
 | 
			
		||||
                if s.len() == 1 {
 | 
			
		||||
                    Ok(Self::Char(s.chars().next().unwrap()))
 | 
			
		||||
                    Ok(Self::Char(
 | 
			
		||||
                        s.chars().next().unwrap_log().to_ascii_lowercase(),
 | 
			
		||||
                    ))
 | 
			
		||||
                } else {
 | 
			
		||||
                    Err(ParseError::InvalidValue {
 | 
			
		||||
                        value: s.to_string(),
 | 
			
		||||
| 
						 | 
				
			
			@ -179,42 +247,6 @@ impl Display for MouseButton {
 | 
			
		|||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
impl Display for Key {
 | 
			
		||||
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
 | 
			
		||||
        let data = match self {
 | 
			
		||||
            Key::Return => "Return".to_string(),
 | 
			
		||||
            Key::Shift => "Shift".to_string(),
 | 
			
		||||
            Key::Tab => "Tab".to_string(),
 | 
			
		||||
            Key::Left => "Left".to_string(),
 | 
			
		||||
            Key::Right => "Right".to_string(),
 | 
			
		||||
            Key::Up => "Up".to_string(),
 | 
			
		||||
            Key::Down => "Down".to_string(),
 | 
			
		||||
            Key::Space => "space".to_string(),
 | 
			
		||||
            Key::Control => "Control".to_string(),
 | 
			
		||||
            Key::Backtick => "grave".to_string(),
 | 
			
		||||
            Key::Char(c) => c.to_string(),
 | 
			
		||||
            Key::Mouse(m) => m.to_string(),
 | 
			
		||||
            Key::F1 => "F1".to_string(),
 | 
			
		||||
            Key::F2 => "F2".to_string(),
 | 
			
		||||
            Key::F3 => "F3".to_string(),
 | 
			
		||||
            Key::F4 => "F4".to_string(),
 | 
			
		||||
            Key::F5 => "F5".to_string(),
 | 
			
		||||
            Key::F6 => "F6".to_string(),
 | 
			
		||||
            Key::F7 => "F7".to_string(),
 | 
			
		||||
            Key::F8 => "F8".to_string(),
 | 
			
		||||
            Key::F9 => "F9".to_string(),
 | 
			
		||||
            Key::F10 => "F10".to_string(),
 | 
			
		||||
            Key::F11 => "F11".to_string(),
 | 
			
		||||
            Key::F12 => "F12".to_string(),
 | 
			
		||||
            Key::Home => "Home".to_string(),
 | 
			
		||||
            Key::Delete => "Delete".to_string(),
 | 
			
		||||
            Key::Mod1Alt => "Mod1".to_string(),
 | 
			
		||||
            Key::Mod4Super => "Mod4".to_string(),
 | 
			
		||||
        };
 | 
			
		||||
        f.write_str(&data)
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#[derive(Debug, Clone, strum::Display, strum::EnumIter, PartialEq)]
 | 
			
		||||
#[strum(serialize_all = "snake_case")]
 | 
			
		||||
pub enum MousebindAction {
 | 
			
		||||
| 
						 | 
				
			
			@ -456,9 +488,10 @@ mod test {
 | 
			
		|||
    use serde::{Deserialize, Serialize};
 | 
			
		||||
    use strum::IntoEnumIterator;
 | 
			
		||||
 | 
			
		||||
    use crate::hlwm::command::HlwmCommand;
 | 
			
		||||
    use crate::{hlwm::command::HlwmCommand, logerr::UnwrapLog};
 | 
			
		||||
 | 
			
		||||
    use super::{Key, MousebindAction};
 | 
			
		||||
    use pretty_assertions::assert_eq;
 | 
			
		||||
 | 
			
		||||
    #[derive(Debug, Serialize, Deserialize)]
 | 
			
		||||
    pub struct KeyWrapper {
 | 
			
		||||
| 
						 | 
				
			
			@ -468,8 +501,8 @@ mod test {
 | 
			
		|||
    #[test]
 | 
			
		||||
    fn key_serialize_deserialize() {
 | 
			
		||||
        for (original_key, wrapper) in Key::iter().map(|key| (key, KeyWrapper { key })) {
 | 
			
		||||
            let wrapper_str = toml::to_string_pretty(&wrapper).unwrap();
 | 
			
		||||
            let parsed: KeyWrapper = toml::from_str(&wrapper_str).unwrap();
 | 
			
		||||
            let wrapper_str = toml::to_string_pretty(&wrapper).expect_log("serialize");
 | 
			
		||||
            let parsed: KeyWrapper = toml::from_str(&wrapper_str).expect_log("deserialize");
 | 
			
		||||
            assert_eq!(original_key, parsed.key);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
| 
						 | 
				
			
			@ -494,4 +527,60 @@ mod test {
 | 
			
		|||
            assert_eq!(original_key, parsed.action);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fn case_random<S: Into<String>>(s: S) -> Vec<String> {
 | 
			
		||||
        let s = s.into();
 | 
			
		||||
        let counts: usize = s.chars().map(|c| c.is_ascii_alphabetic() as usize).sum();
 | 
			
		||||
        if counts < 2 {
 | 
			
		||||
            return vec![s.to_string()];
 | 
			
		||||
        }
 | 
			
		||||
        let mut variants: Vec<String> = Vec::with_capacity(counts + 2);
 | 
			
		||||
        let mut working = Vec::new();
 | 
			
		||||
 | 
			
		||||
        for idx in 0..counts {
 | 
			
		||||
            let mut c_idx = 0;
 | 
			
		||||
            for c in s.chars() {
 | 
			
		||||
                if !c.is_ascii_alphabetic() {
 | 
			
		||||
                    working.push(c);
 | 
			
		||||
                    continue;
 | 
			
		||||
                }
 | 
			
		||||
                if c_idx == idx {
 | 
			
		||||
                    working.push(c.to_ascii_uppercase());
 | 
			
		||||
                } else {
 | 
			
		||||
                    working.push(c.to_ascii_lowercase());
 | 
			
		||||
                }
 | 
			
		||||
                c_idx += 1;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            variants.push(working.into_iter().collect());
 | 
			
		||||
            working = Vec::new();
 | 
			
		||||
        }
 | 
			
		||||
        variants.push(s.to_ascii_lowercase());
 | 
			
		||||
        variants.push(s.to_ascii_uppercase());
 | 
			
		||||
 | 
			
		||||
        variants
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    #[test]
 | 
			
		||||
    fn key_from_case_insensitive() {
 | 
			
		||||
        let test_cases = Key::iter().map(|key| {
 | 
			
		||||
            let strings = key
 | 
			
		||||
                .aliases()
 | 
			
		||||
                .into_iter()
 | 
			
		||||
                .map(|a| case_random(a))
 | 
			
		||||
                .flatten()
 | 
			
		||||
                .collect::<Vec<_>>();
 | 
			
		||||
            (strings, key)
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        for (strings, key) in test_cases {
 | 
			
		||||
            for case in strings {
 | 
			
		||||
                println!("comparing str: [{case}] with key: [{key}]");
 | 
			
		||||
                let parsed = Key::from_str_case_insensitive(&case).expect(&format!(
 | 
			
		||||
                    "key [{key}] parse from insensitive string [{case}] failed"
 | 
			
		||||
                ));
 | 
			
		||||
                assert_eq!(key, parsed);
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -6,6 +6,8 @@ pub trait UnwrapLog {
 | 
			
		|||
    type Target;
 | 
			
		||||
 | 
			
		||||
    fn unwrap_or_log(self, default: Self::Target) -> Self::Target;
 | 
			
		||||
    fn unwrap_log(self) -> Self::Target;
 | 
			
		||||
    fn expect_log(self, expect: &str) -> Self::Target;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
impl<T, E> UnwrapLog for Result<T, E>
 | 
			
		||||
| 
						 | 
				
			
			@ -28,6 +30,26 @@ where
 | 
			
		|||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fn unwrap_log(self) -> Self::Target {
 | 
			
		||||
        match self {
 | 
			
		||||
            Ok(target) => target,
 | 
			
		||||
            Err(err) => {
 | 
			
		||||
                error!("called `Result::unwrap_log()` on an `Err` value: {err}");
 | 
			
		||||
                panic!("called `Result::unwrap_log()` on an `Err` value: {err}");
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fn expect_log(self, expect: &str) -> Self::Target {
 | 
			
		||||
        match self {
 | 
			
		||||
            Ok(target) => target,
 | 
			
		||||
            Err(err) => {
 | 
			
		||||
                error!("[{expect}] called `Result::expect_log()` on an `Err` value: {err}");
 | 
			
		||||
                panic!("[{expect}] called `Result::expect_log()` on an `Err` value: {err}");
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
impl<T> UnwrapLog for Option<T>
 | 
			
		||||
| 
						 | 
				
			
			@ -49,4 +71,24 @@ where
 | 
			
		|||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fn unwrap_log(self) -> Self::Target {
 | 
			
		||||
        match self {
 | 
			
		||||
            Some(target) => target,
 | 
			
		||||
            None => {
 | 
			
		||||
                error!("called `Option::unwrap_log()` on a `None` value");
 | 
			
		||||
                panic!("called `Option::unwrap_log()` on a `None` value");
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fn expect_log(self, expect: &str) -> Self::Target {
 | 
			
		||||
        match self {
 | 
			
		||||
            Some(target) => target,
 | 
			
		||||
            None => {
 | 
			
		||||
                error!("[{expect}] called `Option::expect_log()` on an `None` value");
 | 
			
		||||
                panic!("[{expect}] called `Option::expect_log()` on an `None` value");
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
							
								
								
									
										17
									
								
								src/main.rs
								
								
								
								
							
							
						
						
									
										17
									
								
								src/main.rs
								
								
								
								
							| 
						 | 
				
			
			@ -1,9 +1,11 @@
 | 
			
		|||
use std::path::{Path, PathBuf};
 | 
			
		||||
 | 
			
		||||
use add::Add;
 | 
			
		||||
use clap::{Parser, Subcommand};
 | 
			
		||||
use config::Config;
 | 
			
		||||
use log::{error, info};
 | 
			
		||||
use log::{error, info, LevelFilter};
 | 
			
		||||
 | 
			
		||||
mod add;
 | 
			
		||||
pub mod cmd;
 | 
			
		||||
mod config;
 | 
			
		||||
pub mod environ;
 | 
			
		||||
| 
						 | 
				
			
			@ -40,10 +42,16 @@ enum HlctlCommand {
 | 
			
		|||
    },
 | 
			
		||||
    /// Start the top panel
 | 
			
		||||
    Panel,
 | 
			
		||||
    /// Add an element to the existing hlctl config
 | 
			
		||||
    #[command(subcommand)]
 | 
			
		||||
    Add(Add),
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
fn main() {
 | 
			
		||||
    pretty_env_logger::init();
 | 
			
		||||
    pretty_env_logger::formatted_builder()
 | 
			
		||||
        .filter_module("rustyline", LevelFilter::Error)
 | 
			
		||||
        .parse_default_env()
 | 
			
		||||
        .init();
 | 
			
		||||
 | 
			
		||||
    let args = Args::parse();
 | 
			
		||||
    match args.command {
 | 
			
		||||
| 
						 | 
				
			
			@ -55,6 +63,11 @@ fn main() {
 | 
			
		|||
                error!("panel: {err}");
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
        HlctlCommand::Add(add) => {
 | 
			
		||||
            if let Err(err) = add.run() {
 | 
			
		||||
                error!("add: {err}");
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
		Loading…
	
		Reference in New Issue