diff --git a/Cargo.lock b/Cargo.lock index 3429f36..785e645 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,6 +2,25 @@ # It is not intended for manual editing. version = 3 +[[package]] +name = "ahash" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c99f64d1e06488f620f932677e24bc6e2897582980441ae90a671415bd7ec2f" +dependencies = [ + "cfg-if", + "const-random", + "getrandom", + "once_cell", + "version_check", +] + +[[package]] +name = "autocfg" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" + [[package]] name = "bitflags" version = "1.3.2" @@ -14,13 +33,20 @@ version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0d8c1fef690941d3e7788d328517591fecc684c084084702d6ff1641e993699a" +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + [[package]] name = "clafrica" -version = "0.3.0" +version = "0.3.1" dependencies = [ "clafrica-lib", "enigo", "rdev", + "rhai", "rstk", "serde", "toml", @@ -45,6 +71,28 @@ dependencies = [ "objc", ] +[[package]] +name = "const-random" +version = "0.1.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "368a7a772ead6ce7e1de82bfb04c485f3db8ec744f72925af5735e29a22cc18e" +dependencies = [ + "const-random-macro", + "proc-macro-hack", +] + +[[package]] +name = "const-random-macro" +version = "0.1.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d7d6ab3c3a2282db210df5f02c4dab6e0a7057af0fb7ebd4070f30fe05c0ddb" +dependencies = [ + "getrandom", + "once_cell", + "proc-macro-hack", + "tiny-keccak", +] + [[package]] name = "core-foundation" version = "0.7.0" @@ -125,6 +173,12 @@ dependencies = [ "libc", ] +[[package]] +name = "crunchy" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a81dae078cea95a014a339291cec439d2f232ebe854a9d672b796c6afafa9b7" + [[package]] name = "enigo" version = "0.1.2" @@ -159,6 +213,17 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" +[[package]] +name = "getrandom" +version = "0.2.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be4136b2a15dd319360be1c07d9933517ccf0be8f16bf62a3bee4f0d618df427" +dependencies = [ + "cfg-if", + "libc", + "wasi", +] + [[package]] name = "hashbrown" version = "0.14.0" @@ -175,6 +240,15 @@ dependencies = [ "hashbrown", ] +[[package]] +name = "instant" +version = "0.1.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a5bbe824c507c5da5956355e86a746d82e0e1464f65d862cc5e71da70e94b2c" +dependencies = [ + "cfg-if", +] + [[package]] name = "lazy_static" version = "1.4.0" @@ -202,6 +276,15 @@ version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2dffe52ecf27772e601905b7522cb4ef790d2cc203488bbd0e2fe85fcb74566d" +[[package]] +name = "num-traits" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f30b0abd723be7e2ffca1272140fac1a2f084c77ec3e123c192b66af1ee9e6c2" +dependencies = [ + "autocfg", +] + [[package]] name = "objc" version = "0.2.7" @@ -223,6 +306,12 @@ version = "0.3.27" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "26072860ba924cbfa98ea39c8c19b4dd6a4a25423dbdf219c1eca91aa0cf6964" +[[package]] +name = "proc-macro-hack" +version = "0.5.20+deprecated" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc375e1527247fe1a97d8b7156678dfe7c1af2fc075c9a4db3690ecd2a148068" + [[package]] name = "proc-macro2" version = "1.0.66" @@ -234,9 +323,9 @@ dependencies = [ [[package]] name = "quote" -version = "1.0.32" +version = "1.0.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "50f3b39ccfb720540debaa0164757101c08ecb8d326b15358ce76a62c7e85965" +checksum = "5267fca4496028628a95160fc423a33e8b2e6af8a5302579e322e4b520293cae" dependencies = [ "proc-macro2", ] @@ -257,6 +346,32 @@ dependencies = [ "x11", ] +[[package]] +name = "rhai" +version = "1.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c2a11a646ef5d4e4a9d5cf80c7e4ecb20f9b1954292d5c5e6d6cbc8d33728ec" +dependencies = [ + "ahash", + "bitflags", + "instant", + "num-traits", + "rhai_codegen", + "smallvec", + "smartstring", +] + +[[package]] +name = "rhai_codegen" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db74e3fdd29d969a0ec1f8e79171a6f0f71d0429293656901db382d248c4c021" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + [[package]] name = "rstk" version = "0.1.0" @@ -283,7 +398,7 @@ checksum = "aafe972d60b0b9bee71a91b92fee2d4fb3c9d7e8f6b179aa99f27203d99a4816" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.29", ] [[package]] @@ -295,17 +410,60 @@ dependencies = [ "serde", ] +[[package]] +name = "smallvec" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62bb4feee49fdd9f707ef802e22365a35de4b7b299de4763d44bfea899442ff9" + +[[package]] +name = "smartstring" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3fb72c633efbaa2dd666986505016c32c3044395ceaf881518399d2f4127ee29" +dependencies = [ + "autocfg", + "static_assertions", + "version_check", +] + +[[package]] +name = "static_assertions" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" + [[package]] name = "syn" -version = "2.0.28" +version = "1.0.109" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "04361975b3f5e348b2189d8dc55bc942f278b2d482a6a0365de5bdd62d351567" +checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" dependencies = [ "proc-macro2", "quote", "unicode-ident", ] +[[package]] +name = "syn" +version = "2.0.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c324c494eba9d92503e6f1ef2e6df781e78f6a7705a0202d9801b198807d518a" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "tiny-keccak" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c9d3793400a45f954c52e73d068316d76b6f4e36977e3fcebb13a2721e80237" +dependencies = [ + "crunchy", +] + [[package]] name = "toml" version = "0.7.6" @@ -346,6 +504,18 @@ version = "1.0.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "301abaae475aa91687eb82514b328ab47a211a533026cb25fc3e519b86adfc3c" +[[package]] +name = "version_check" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" + +[[package]] +name = "wasi" +version = "0.11.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" + [[package]] name = "winapi" version = "0.3.9" @@ -379,9 +549,9 @@ dependencies = [ [[package]] name = "windows-targets" -version = "0.48.1" +version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05d4b17490f70499f20b9e791dcf6a299785ce8af4d709018206dc5b4953e95f" +checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" dependencies = [ "windows_aarch64_gnullvm", "windows_aarch64_msvc", @@ -394,51 +564,51 @@ dependencies = [ [[package]] name = "windows_aarch64_gnullvm" -version = "0.48.0" +version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "91ae572e1b79dba883e0d315474df7305d12f569b400fcf90581b06062f7e1bc" +checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" [[package]] name = "windows_aarch64_msvc" -version = "0.48.0" +version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b2ef27e0d7bdfcfc7b868b317c1d32c641a6fe4629c171b8928c7b08d98d7cf3" +checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" [[package]] name = "windows_i686_gnu" -version = "0.48.0" +version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "622a1962a7db830d6fd0a69683c80a18fda201879f0f447f065a3b7467daa241" +checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" [[package]] name = "windows_i686_msvc" -version = "0.48.0" +version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4542c6e364ce21bf45d69fdd2a8e455fa38d316158cfd43b3ac1c5b1b19f8e00" +checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" [[package]] name = "windows_x86_64_gnu" -version = "0.48.0" +version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ca2b8a661f7628cbd23440e50b05d705db3686f894fc9580820623656af974b1" +checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" [[package]] name = "windows_x86_64_gnullvm" -version = "0.48.0" +version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7896dbc1f41e08872e9d5e8f8baa8fdd2677f29468c4e156210174edc7f7b953" +checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" [[package]] name = "windows_x86_64_msvc" -version = "0.48.0" +version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a515f5799fe4961cb532f983ce2b23082366b898e52ffbce459c86f67c8378a" +checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" [[package]] name = "winnow" -version = "0.5.10" +version = "0.5.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5504cc7644f4b593cbc05c4a55bf9bd4e94b867c3c0bd440934174d50482427d" +checksum = "d09770118a7eb1ccaf4a594a221334119a44a814fcb0d31c5b85e83e97227a97" dependencies = [ "memchr", ] diff --git a/Cargo.toml b/Cargo.toml index 036bec4..740361e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -2,4 +2,4 @@ members = [ "clafrica-lib", "clafrica" -] \ No newline at end of file +] diff --git a/clafrica/Cargo.toml b/clafrica/Cargo.toml index 4f7fa01..532a17f 100644 --- a/clafrica/Cargo.toml +++ b/clafrica/Cargo.toml @@ -15,6 +15,7 @@ authors = ["Fomegne Brady "] clafrica-lib = { version = "0.3.0", path = "../clafrica-lib" } enigo = "0.1.2" rdev = "0.5.2" +rhai = "1.15.1" serde = { version = "1.0.163", features = ["serde_derive"] } toml = "0.7.3" diff --git a/clafrica/data/bad_script.toml b/clafrica/data/bad_script.toml new file mode 100644 index 0000000..580907b --- /dev/null +++ b/clafrica/data/bad_script.toml @@ -0,0 +1,4 @@ +# script file not found + +[translators] +not_found = "not_found" diff --git a/clafrica/data/bad_script2.toml b/clafrica/data/bad_script2.toml new file mode 100644 index 0000000..417c126 --- /dev/null +++ b/clafrica/data/bad_script2.toml @@ -0,0 +1,4 @@ +# script parsing error + +[translators] +invalid = "./scripts/invalid.rhai" diff --git a/clafrica/data/blank_sample.toml b/clafrica/data/blank_sample.toml index bef57d7..e69de29 100644 --- a/clafrica/data/blank_sample.toml +++ b/clafrica/data/blank_sample.toml @@ -1 +0,0 @@ -[data] \ No newline at end of file diff --git a/clafrica/data/config_sample.toml b/clafrica/data/config_sample.toml index 166c683..1f4c736 100644 --- a/clafrica/data/config_sample.toml +++ b/clafrica/data/config_sample.toml @@ -6,3 +6,10 @@ auto_capitalize = false [data] sample = { path = "./data_sample.toml" } + +[translators] +datetime = { path = "./scripts/datetime.toml" } + +[translation] +mydict = { path = "./dictionary.toml" } + diff --git a/clafrica/data/dictionary.toml b/clafrica/data/dictionary.toml new file mode 100644 index 0000000..2197711 --- /dev/null +++ b/clafrica/data/dictionary.toml @@ -0,0 +1,3 @@ +[translation] +halo = "hello" +hi = { value = "hello", alias = ["hey"] } diff --git a/clafrica/data/invalid.toml b/clafrica/data/invalid.toml new file mode 100644 index 0000000..beaae16 --- /dev/null +++ b/clafrica/data/invalid.toml @@ -0,0 +1,2 @@ +# parsing error +1a = à diff --git a/clafrica/data/scripts/datetime.toml b/clafrica/data/scripts/datetime.toml new file mode 100644 index 0000000..63f7307 --- /dev/null +++ b/clafrica/data/scripts/datetime.toml @@ -0,0 +1,4 @@ +[translators] +date = "./datetime/date.rhai" +time = { value = "./datetime/time.rhai", alias = ["clock"] } + diff --git a/clafrica/data/scripts/datetime/core.rhai b/clafrica/data/scripts/datetime/core.rhai new file mode 100644 index 0000000..c9379ce --- /dev/null +++ b/clafrica/data/scripts/datetime/core.rhai @@ -0,0 +1,9 @@ +fn parse_cmd(input) { + let data = input.split('_'); + + if data.len() == 3 { + return data[1]; + } + + return ""; +} diff --git a/clafrica/data/scripts/datetime/date.rhai b/clafrica/data/scripts/datetime/date.rhai new file mode 100644 index 0000000..c3e425b --- /dev/null +++ b/clafrica/data/scripts/datetime/date.rhai @@ -0,0 +1,38 @@ +import `${DIR}/core` as core; + +const MONTHS = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"]; + +fn parse_date(input) { + let data = input.split('/'); + + if data.len() == 3 { + let day = parse_int(data[0]); + let month = parse_int(data[1]); + let year = parse_int(data[2]); + + if day in 1..31 && month in 1..12 && year in 1..2100 { + return [day, month, year]; + } + } + + return []; +} + +// Main function +fn translate(input) { + let data = core::parse_cmd(input); + let date = parse_date(data); + + if !date.is_empty() { + return ["", `${date[0]} ${global::MONTHS[date[1]]} ${date[2]}`, true]; + } + + return ["", "", false]; +} + +// Test +// print(translate("")); +// print(translate("01/02/2002")); +// print(translate("__")); +// print(translate("_04/06/2010_")); +// print(translate("_04/06/3000_")); diff --git a/clafrica/data/scripts/datetime/time.rhai b/clafrica/data/scripts/datetime/time.rhai new file mode 100644 index 0000000..4e66695 --- /dev/null +++ b/clafrica/data/scripts/datetime/time.rhai @@ -0,0 +1,3 @@ +fn translate(input) { + return ["", "", false]; +} diff --git a/clafrica/data/scripts/hi.rhai b/clafrica/data/scripts/hi.rhai new file mode 100644 index 0000000..db0f565 --- /dev/null +++ b/clafrica/data/scripts/hi.rhai @@ -0,0 +1,7 @@ +fn translate(input) { + if (input == "hi") { + return ["", "hello", true]; + } + + return ["", "", false]; +} diff --git a/clafrica/data/scripts/invalid.rhai b/clafrica/data/scripts/invalid.rhai new file mode 100644 index 0000000..e4bedd7 --- /dev/null +++ b/clafrica/data/scripts/invalid.rhai @@ -0,0 +1,4 @@ +# syntax error + +def hello(): + print("hi") diff --git a/clafrica/data/test.toml b/clafrica/data/test.toml index 379894d..67ba68d 100644 --- a/clafrica/data/test.toml +++ b/clafrica/data/test.toml @@ -5,16 +5,22 @@ name = "Test code" description = "Test code for testing purpose." [core] -buffer_size = 128 +buffer_size = 64 auto_capitalize = true [data] -af = "ɑ" -Af = "α" -u5 = { value = "û", alias = ["5u"]} -aff = "ɑɑ" -aff3 = "ɑ̄ɑ̄" -cc = { value = "ç", alias = ["ccced"]} +af = { value = "ɑ", alias = ["qf"] } +Af = { value = "α", alias = ["Qf"] } +u5 = { value = "û", alias = ["5u", "(u", "u("] } +aff = { value = "ɑɑ", alias = ["qff"] } +aff3 = { value = "ɑ̄ɑ̄", alias = ["qff\""] } +c_ = { value = "ç", alias = ["c_ced", "c8", "c8ced"]} uu = "ʉ" -uu3 = "ʉ̄" -uuaf3 = "ʉ̄ɑ̄" +uu3 = { value = "ʉ̄", alias = ["uu\""] } +uuaf3 = { value = "ʉ̄ɑ̄", alias = ["uuqf\""] } + +[translators] +hi = "./scripts/hi.rhai" + +[translation] +hello = "hi" diff --git a/clafrica/src/api.rs b/clafrica/src/api.rs index b37207b..dcb5422 100644 --- a/clafrica/src/api.rs +++ b/clafrica/src/api.rs @@ -1,7 +1,9 @@ pub trait Frontend { fn update_screen(&mut self, _screen: (u64, u64)) {} fn update_position(&mut self, _position: (f64, f64)) {} - fn update_text(&mut self, _text: Vec) {} + fn update_text(&mut self, _text: &str) {} + fn add_predicate(&mut self, _remaining_code: &str, _text: &str) {} + fn clear_predicates(&mut self) {} } pub struct None; @@ -11,8 +13,11 @@ impl Frontend for None {} pub struct Console; impl Frontend for Console { - fn update_text(&mut self, text: Vec) { - println!("{:?}", text); + fn update_text(&mut self, text: &str) { + println!("text: {:?}", text); + } + fn add_predicate(&mut self, remaining_code: &str, text: &str) { + println!("predicate: {} ~{}", text, remaining_code); } } @@ -22,6 +27,8 @@ fn test_console() { let mut none = None; console.update_screen((0, 0)); console.update_position((0.0, 0.0)); - console.update_text(vec!['h', 'e', 'l', 'l', 'o']); - none.update_text(vec!['h', 'e', 'l', 'l', 'o']); + console.update_text("hello"); + console.add_predicate("_", "10/12/2003"); + console.clear_predicates(); + none.update_text("hello"); } diff --git a/clafrica/src/config.rs b/clafrica/src/config.rs index 4ec98de..8b14c6f 100644 --- a/clafrica/src/config.rs +++ b/clafrica/src/config.rs @@ -1,3 +1,4 @@ +use rhai::{Engine, AST}; use serde::Deserialize; use std::{collections::HashMap, error, fs, path::Path}; use toml::{self}; @@ -5,7 +6,9 @@ use toml::{self}; #[derive(Deserialize, Debug, Clone)] pub struct Config { pub core: Option, - data: HashMap, + data: Option>, + translators: Option>, + translation: Option>, } #[derive(Deserialize, Debug, Clone)] @@ -35,22 +38,24 @@ struct DetailedData { impl Config { pub fn from_file(filepath: &Path) -> Result> { - let content = fs::read_to_string(filepath)?; - let mut config: Self = toml::from_str(&content)?; + let content = fs::read_to_string(filepath) + .map_err(|err| format!("Couldn't open file `{filepath:?}`.\nCaused by:\n\t{err}."))?; + let mut config: Self = toml::from_str(&content).map_err(|err| { + format!("Failed to parse configuration file `{filepath:?}`.\nCaused by:\n\t{err}") + })?; let config_path = filepath.parent().unwrap(); + // Data let mut data = HashMap::new(); - config - .data - .iter() - .try_for_each(|(key, value)| -> Result<(), Box> { + config.data.unwrap_or_default().iter().try_for_each( + |(key, value)| -> Result<(), Box> { match value { Data::File(DataFile { path }) => { let filepath = config_path.join(path); let conf = Config::from_file(&filepath)?; - data.extend(conf.data); + data.extend(conf.data.unwrap_or_default()); } Data::Simple(_) => { data.insert(key.to_owned(), value.clone()); @@ -62,21 +67,84 @@ impl Config { } }; Ok(()) - })?; + }, + )?; + config.data = Some(data); + + // Translators + let mut translators = HashMap::new(); + + config.translators.unwrap_or_default().iter().try_for_each( + |(key, value)| -> Result<(), Box> { + match value { + Data::File(DataFile { path }) => { + let filepath = config_path.join(path); + let conf = Config::from_file(&filepath)?; + translators.extend(conf.translators.unwrap_or_default()); + } + Data::Simple(v) => { + let filepath = config_path.join(v.clone()).to_str().unwrap().to_string(); + translators.insert(key.to_owned(), Data::Simple(filepath)); + } + Data::Detailed(DetailedData { value, alias }) => { + let filepath = config_path + .join(value.clone()) + .to_str() + .unwrap() + .to_string(); + alias.iter().chain([key.to_owned()].iter()).for_each(|e| { + translators.insert(e.to_owned(), Data::Simple(filepath.clone())); + }); + } + }; + Ok(()) + }, + )?; + config.translators = Some(translators); + + // Translation + let mut translation = HashMap::new(); + + config.translation.unwrap_or_default().iter().try_for_each( + |(key, value)| -> Result<(), Box> { + match value { + Data::File(DataFile { path }) => { + let filepath = config_path.join(path); + let conf = Config::from_file(&filepath)?; + translation.extend(conf.translation.unwrap_or_default()); + } + Data::Simple(_) => { + translation.insert(key.to_owned(), value.clone()); + } + Data::Detailed(DetailedData { value, alias }) => { + alias.iter().chain([key.to_owned()].iter()).for_each(|e| { + translation.insert(e.to_owned(), Data::Simple(value.to_owned())); + }); + } + }; + Ok(()) + }, + )?; - config.data = data; + config.translation = Some(translation); Ok(config) } pub fn extract_data(&self) -> HashMap { - let data = self.data.iter().filter_map(|(k, v)| { - let v = match v { - Data::Simple(value) => Some(value), - _ => None, - }; - v.map(|v| (k.to_owned(), v.to_owned())) - }); + let empty = HashMap::default(); + let data = self + .data + .as_ref() + .unwrap_or(&empty) + .iter() + .filter_map(|(k, v)| { + let v = match v { + Data::Simple(value) => Some(value), + _ => None, + }; + v.map(|v| (k.to_owned(), v.to_owned())) + }); if self .core @@ -86,25 +154,68 @@ impl Config { { data.clone() .chain(data.clone().filter_map(|(k, v)| { - if k.chars().next()?.is_lowercase() { - Some((k[0..1].to_uppercase() + &k[1..], v.to_uppercase())) - } else { - None - } + k.chars() + .next()? + .is_lowercase() + .then(|| (k[0..1].to_uppercase() + &k[1..], v.to_uppercase())) })) // We overwrite the auto capitalization - .chain(data.filter_map(|(k, v)| { - if k.chars().next()?.is_uppercase() { - Some((k, v)) - } else { - None - } - })) + .chain(data.filter_map(|(k, v)| k.chars().next()?.is_uppercase().then_some((k, v)))) .collect() } else { data.collect() } } + + pub fn extract_translators(&self) -> Result, Box> { + let empty = HashMap::default(); + let mut engine = Engine::new(); + + // allow nesting up to 50 layers of expressions/statements + // at global level, but only 10 inside function + engine.set_max_expr_depths(25, 25); + + self.translators + .as_ref() + .unwrap_or(&empty) + .iter() + .filter_map(|(name, filename)| { + let filename = match filename { + Data::Simple(filename) => Some(filename), + _ => None, + }; + + filename.map(|filename| { + let parent = Path::new(&filename).parent().unwrap().to_str().unwrap(); + let header = format!(r#"const DIR = {parent:?};"#); + let ast = engine.compile_file(filename.into()).map_err(|err| { + format!("Failed to parse script file `{filename}`.\nCaused by:\n\t{err}.") + })?; + let ast = engine.compile(header).unwrap().merge(&ast); + + Ok((name.to_owned(), ast)) + }) + }) + .collect() + } + + pub fn extract_translation(&self) -> HashMap { + let empty = HashMap::new(); + + self.translation + .as_ref() + .unwrap_or(&empty) + .iter() + .filter_map(|(k, v)| { + let v = match v { + Data::Simple(v) => Some(v), + _ => None, + }; + + v.map(|v| (k.to_owned(), v.to_owned())) + }) + .collect() + } } #[cfg(test)] @@ -115,17 +226,60 @@ mod tests { use std::path::Path; let conf = Config::from_file(Path::new("./data/config_sample.toml")).unwrap(); - assert_eq!(conf.core.clone().unwrap().buffer_size, 12); - assert!(!conf.core.clone().unwrap().auto_capitalize); + assert_eq!(conf.core.as_ref().unwrap().buffer_size, 12); + assert!(!conf.core.as_ref().unwrap().auto_capitalize); let data = conf.extract_data(); assert_eq!(data.keys().len(), 19); - let conf = Config::from_file(Path::new("./not_found")); + // parsing error + let conf = Config::from_file(Path::new("./data/invalid.toml")); + assert!(conf.is_err()); + + // config file not found + let conf = Config::from_file(Path::new("./data/not_found")); assert!(conf.is_err()); + // data and and core not provided let conf = Config::from_file(Path::new("./data/blank_sample.toml")).unwrap(); let data = conf.extract_data(); assert_eq!(data.keys().len(), 0); } + + #[test] + fn from_file_with_translators() { + use crate::config::Config; + use std::path::Path; + + let conf = Config::from_file(Path::new("./data/config_sample.toml")).unwrap(); + let translators = conf.extract_translators().unwrap(); + assert_eq!(translators.keys().len(), 3); + + // translators not provided + let conf = Config::from_file(Path::new("./data/blank_sample.toml")).unwrap(); + let translators = conf.extract_translators().unwrap(); + assert_eq!(translators.keys().len(), 0); + + // scripts parsing error + let conf = Config::from_file(Path::new("./data/bad_script2.toml")).unwrap(); + assert!(conf.extract_translators().is_err()); + + // script file not found + let conf = Config::from_file(Path::new("./data/bad_script.toml")).unwrap(); + assert!(conf.extract_translators().is_err()); + } + + #[test] + fn from_file_with_translation() { + use crate::config::Config; + use std::path::Path; + + let conf = Config::from_file(Path::new("./data/config_sample.toml")).unwrap(); + let translation = conf.extract_translation(); + assert_eq!(translation.keys().len(), 3); + + let conf = Config::from_file(Path::new("./data/blank_sample.toml")).unwrap(); + let translation = conf.extract_translation(); + assert_eq!(translation.keys().len(), 0); + } } diff --git a/clafrica/src/lib.rs b/clafrica/src/lib.rs index b05db18..aa01048 100644 --- a/clafrica/src/lib.rs +++ b/clafrica/src/lib.rs @@ -1,17 +1,23 @@ pub mod api; pub mod config; +pub mod processor; +pub mod translator; use crate::api::Frontend; -use clafrica_lib::{text_buffer, utils}; -use enigo::{Enigo, Key, KeyboardControllable}; +use crate::processor::Processor; +use crate::translator::Translator; +use clafrica_lib::utils; use rdev::{self, EventType, Key as E_Key}; -use std::{io, sync::mpsc, thread}; +use std::{error, sync::mpsc, thread}; pub mod prelude { pub use crate::config::Config; } -pub fn run(config: config::Config, mut frontend: impl Frontend) -> Result<(), io::Error> { +pub fn run( + config: config::Config, + mut frontend: impl Frontend, +) -> Result<(), Box> { let map = utils::build_map( config .extract_data() @@ -19,9 +25,11 @@ pub fn run(config: config::Config, mut frontend: impl Frontend) -> Result<(), io .map(|(k, v)| [k.as_str(), v.as_str()]) .collect(), ); - let mut cursor = text_buffer::Cursor::new(map, config.core.map(|e| e.buffer_size).unwrap_or(8)); - - let mut keyboard = Enigo::new(); + let mut processor = Processor::new( + map, + config.core.as_ref().map(|e| e.buffer_size).unwrap_or(8), + ); + let translator = Translator::new(config.extract_translation(), config.extract_translators()?); frontend.update_screen(rdev::display_size().unwrap()); @@ -59,79 +67,31 @@ pub fn run(config: config::Config, mut frontend: impl Frontend) -> Result<(), io }); for event in rx.iter() { - let character = event.name.and_then(|s| s.chars().next()); - let is_valid = character - .map(|c| c.is_alphanumeric() || c.is_ascii_punctuation()) - .unwrap_or_default(); - - match event.event_type { - EventType::KeyPress(E_Key::Backspace) => { - if let Some(out) = cursor.undo() { - rdev::simulate(&EventType::KeyPress(E_Key::Pause)) - .expect("We could pause the listeners"); - keyboard.key_up(Key::Backspace); - - let i = out.chars().count(); - (1..i).for_each(|_| keyboard.key_click(Key::Backspace)); - - rdev::simulate(&EventType::KeyRelease(E_Key::Pause)) - .expect("We could resume the listeners"); - - // Clear the remaining code - while let (None, 1.., ..) = cursor.state() { - cursor.undo(); - } - - if let (Some(_in), ..) = cursor.state() { - keyboard.key_sequence(&_in); - } - } - - frontend.update_text(cursor.to_sequence()); - } - EventType::KeyPress( - E_Key::Unknown(_) | E_Key::ShiftLeft | E_Key::ShiftRight | E_Key::CapsLock, - ) => { - // println!("[ignore] {:?}", event.event_type) - } - EventType::ButtonPress(_) | EventType::KeyPress(_) if !is_valid => { - cursor.clear(); - frontend.update_text(cursor.to_sequence()); - } - EventType::KeyPress(_) => { - let character = character.unwrap(); - - let mut prev_cursor = cursor.clone(); - - if let Some(_in) = cursor.hit(character) { - rdev::simulate(&EventType::KeyPress(E_Key::Pause)) - .expect("We could pause the listeners"); - - keyboard.key_click(Key::Backspace); - - // Remove the remaining code - while let (None, 1.., ..) = prev_cursor.state() { - prev_cursor.undo(); - keyboard.key_click(Key::Backspace); - } - - if let (Some(out), ..) = prev_cursor.state() { - (0..out.chars().count()).for_each(|_| keyboard.key_click(Key::Backspace)) - } - - keyboard.key_sequence(&_in); - - rdev::simulate(&EventType::KeyRelease(E_Key::Pause)) - .expect("We could resume the listeners"); + if let EventType::MouseMove { x, y } = &event.event_type { + frontend.update_position((*x, *y)); + } else { + let (changed, committed) = processor.process(event); + + if changed { + let input = processor.get_input(); + + frontend.clear_predicates(); + + if !committed { + translator.translate(&input).iter().for_each( + |(remaining_code, text, translated)| { + if *translated { + processor.commit(&input, text); + } else if !remaining_code.is_empty() { + frontend.add_predicate(remaining_code, text); + } + }, + ); }; - frontend.update_text(cursor.to_sequence()); - } - EventType::MouseMove { x, y } => { - frontend.update_position((x, y)); + frontend.update_text(&input); } - _ => (), - }; + } } Ok(()) @@ -191,12 +151,7 @@ mod tests { input_field.height(12); input_field.pack().layout(); root.geometry(200, 200, 0, 0); - rstk::tell_wish( - r#" - bind . { destroy . }; - chan configure stdout -encoding utf-8; - "#, - ); + rstk::tell_wish("chan configure stdout -encoding utf-8;"); thread::sleep(Duration::from_secs(1)); input_field } @@ -224,13 +179,13 @@ mod tests { input!(KeyB KeyB KeyB Escape, typing_speed_ms); input!(KeyU Backspace KeyU KeyU Backspace KeyU, typing_speed_ms); input!( - KeyC KeyC KeyC KeyE KeyD + KeyC Num8 KeyC KeyE KeyD KeyU KeyU - KeyA KeyF CapsLock Num3 CapsLock, typing_speed_ms); + KeyA KeyF Num3, typing_speed_ms); input!( KeyA KeyF KeyA KeyF - KeyA KeyF KeyF CapsLock Num3 CapsLock, typing_speed_ms); - input!(KeyU KeyU CapsLock Num3 CapsLock, typing_speed_ms); + KeyA KeyF KeyF Num3, typing_speed_ms); + input!(KeyU KeyU Num3, typing_speed_ms); output!(textfield, format!("{LIMIT}uçʉ̄ɑ̄ɑɑɑ̄ɑ̄ʉ̄")); // We verify that the undo (backspace) works as expected @@ -241,10 +196,10 @@ mod tests { // We verify that the pause/resume works as expected input!(ControlLeft ControlLeft, typing_speed_ms); - input!(KeyA KeyF, typing_speed_ms); + input!(KeyU KeyU, typing_speed_ms); input!(ControlLeft ControlRight, typing_speed_ms); input!(KeyA KeyF, typing_speed_ms); - output!(textfield, format!("{LIMIT}afɑ")); + output!(textfield, format!("{LIMIT}uuɑ")); (0..3).for_each(|_| { input!(Backspace, typing_speed_ms); @@ -252,10 +207,19 @@ mod tests { // We verify the auto capitalization works as expected input!(CapsLock KeyA CapsLock KeyF, typing_speed_ms); - input!(CapsLock KeyU CapsLock, typing_speed_ms); - input!(CapsLock Num5 CapsLock, typing_speed_ms); - input!(CapsLock Num5 CapsLock KeyU, typing_speed_ms); - output!(textfield, format!("{LIMIT}αÛû")); + input!(CapsLock KeyA CapsLock KeyF KeyF, typing_speed_ms); + input!(KeyA KeyF KeyF, typing_speed_ms); + output!(textfield, format!("{LIMIT}αⱭⱭɑɑ")); + + (0..5).for_each(|_| { + input!(Backspace, typing_speed_ms); + }); + + // We verify that the translation work as expected + input!(KeyH KeyE KeyL KeyL KeyO, typing_speed_ms); + input!(Escape KeyH KeyI, typing_speed_ms); + + output!(textfield, format!("{LIMIT}hihello")); rstk::end_wish(); } diff --git a/clafrica/src/processor.rs b/clafrica/src/processor.rs new file mode 100644 index 0000000..5948bb9 --- /dev/null +++ b/clafrica/src/processor.rs @@ -0,0 +1,116 @@ +use clafrica_lib::text_buffer::{Cursor, Node}; +use enigo::{Enigo, Key, KeyboardControllable}; +use rdev::{self, Event, EventType, Key as E_Key}; + +pub struct Processor { + keyboard: Enigo, + cursor: Cursor, +} + +impl Processor { + pub fn new(map: Node, buffer_size: usize) -> Self { + let cursor = Cursor::new(map, buffer_size); + + Self { + keyboard: Enigo::new(), + cursor, + } + } + + pub fn process(&mut self, event: Event) -> (bool, bool) { + let character = event.name.and_then(|s| s.chars().next()); + let is_valid = character + .map(|c| c.is_alphanumeric() || c.is_ascii_punctuation()) + .unwrap_or_default(); + let (mut changed, mut committed) = (false, false); + + match event.event_type { + EventType::KeyPress(E_Key::Backspace) => { + if let Some(out) = self.cursor.undo() { + self.pause(); + self.keyboard.key_up(Key::Backspace); + + let i = out.chars().count(); + (1..i).for_each(|_| self.keyboard.key_click(Key::Backspace)); + + // Clear the remaining code + while let (None, 1.., ..) = self.cursor.state() { + self.cursor.undo(); + } + + if let (Some(_in), ..) = self.cursor.state() { + self.keyboard.key_sequence(&_in); + } + + self.resume(); + committed = true; + } + + changed = true; + } + EventType::KeyPress( + E_Key::Unknown(_) | E_Key::ShiftLeft | E_Key::ShiftRight | E_Key::CapsLock, + ) => { + // println!("[ignore] {:?}", event.event_type) + } + EventType::ButtonPress(_) | EventType::KeyPress(_) if !is_valid => { + self.cursor.clear(); + changed = true; + } + EventType::KeyPress(_) => { + let character = character.unwrap(); + let mut prev_cursor = self.cursor.clone(); + + if let Some(_in) = self.cursor.hit(character) { + self.pause(); + self.keyboard.key_click(Key::Backspace); + + // Remove the remaining code + while let (None, 1.., ..) = prev_cursor.state() { + prev_cursor.undo(); + self.keyboard.key_click(Key::Backspace); + } + + if let (Some(out), ..) = prev_cursor.state() { + (0..out.chars().count()) + .for_each(|_| self.keyboard.key_click(Key::Backspace)) + } + + self.keyboard.key_sequence(&_in); + self.resume(); + committed = true; + }; + + changed = true; + } + _ => (), + }; + + (changed, committed) + } + + pub fn commit(&mut self, code: &str, text: &str) { + self.pause(); + (0..code.len()).for_each(|_| self.keyboard.key_click(Key::Backspace)); + self.keyboard.key_sequence(text); + self.resume(); + } + + fn pause(&mut self) { + rdev::simulate(&EventType::KeyPress(E_Key::Pause)) + .expect("We couldn't pause the listeners"); + } + + fn resume(&mut self) { + rdev::simulate(&EventType::KeyRelease(E_Key::Pause)) + .expect("We couldn't resume the listeners"); + } + + pub fn get_input(&self) -> String { + self.cursor + .to_sequence() + .into_iter() + .filter(|c| *c != '\0') + .collect::() + } +} diff --git a/clafrica/src/translator.rs b/clafrica/src/translator.rs new file mode 100644 index 0000000..b590e93 --- /dev/null +++ b/clafrica/src/translator.rs @@ -0,0 +1,47 @@ +use rhai::{Array, Engine, Scope, AST}; +use std::collections::HashMap; + +pub struct Translator { + dictionary: HashMap, + translators: HashMap, +} + +impl Translator { + pub fn new(dictionary: HashMap, translators: HashMap) -> Self { + Self { + dictionary, + translators, + } + } + + pub fn translate(&self, input: &str) -> Vec<(String, String, bool)> { + let mut scope = Scope::new(); + let engine = Engine::new(); + + self.dictionary + .iter() + .filter_map(|(k, v)| { + if k == input { + Some((k.to_owned(), v.to_owned(), true)) + } else if input.len() > 2 && k.starts_with(input) { + Some((k.chars().skip(input.len()).collect(), v.to_owned(), false)) + } else { + None + } + }) + .chain(self.translators.iter().filter_map(|(_name, translator)| { + let data = engine + .call_fn::(&mut scope, translator, "translate", (input.to_owned(),)) + .unwrap_or_default(); + + (data.len() == 3).then(|| { + let remaining_code = data[0].clone().into_string().unwrap(); + let text = data[1].clone().into_string().unwrap(); + let translated = data[2].clone().as_bool().unwrap(); + + (remaining_code, text, translated) + }) + })) + .collect() + } +}