diff --git a/.gitignore b/.gitignore index 36a613b..09b8950 100644 --- a/.gitignore +++ b/.gitignore @@ -41,4 +41,5 @@ testem.log .DS_Store Thumbs.db -TextTestExe/ \ No newline at end of file +TextTestExe/ +data/ \ No newline at end of file diff --git a/README.md b/README.md index f24f421..2a6cabf 100644 --- a/README.md +++ b/README.md @@ -17,4 +17,4 @@ Keyboard emulation on most systems requires workarounds for two issues: ## Logging and Statistics For logging, either use [TextTestExe](https://depts.washington.edu/acelab/proj/texttest/) (local program) or [TextTestPP](https://drustz.com/TextTestPP/) (online). -The theoretical framework is inspired by https://www.yorku.ca/mack/bit95.html +The theoretical framework is inspired by https://www.yorku.ca/mack/bit95.html. diff --git a/package-lock.json b/package-lock.json index a57458b..061b390 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "keeb", - "version": "0.1.0", + "version": "1.0.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "keeb", - "version": "0.1.0", + "version": "1.0.1", "dependencies": { "@angular/animations": "^18.2.14", "@angular/common": "^18.2.14", @@ -17,6 +17,7 @@ "@angular/platform-browser-dynamic": "^18.2.14", "@angular/router": "^18.2.14", "@tauri-apps/api": "^2", + "@tauri-apps/plugin-cli": "^2.4.1", "@tauri-apps/plugin-opener": "^2", "rxjs": "~7.8.0", "tslib": "^2.3.0", @@ -4581,6 +4582,15 @@ "node": ">= 10" } }, + "node_modules/@tauri-apps/plugin-cli": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/@tauri-apps/plugin-cli/-/plugin-cli-2.4.1.tgz", + "integrity": "sha512-8JXofQFI5cmiGolh1PlU4hzE2YJgrgB1lyaztyBYiiMCy13luVxBXaXChYPeqMkUo46J1UadxvYdjRjj0E8zaw==", + "license": "MIT OR Apache-2.0", + "dependencies": { + "@tauri-apps/api": "^2.8.0" + } + }, "node_modules/@tauri-apps/plugin-opener": { "version": "2.5.0", "resolved": "https://registry.npmjs.org/@tauri-apps/plugin-opener/-/plugin-opener-2.5.0.tgz", @@ -13399,6 +13409,7 @@ "integrity": "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "esbuild": "^0.21.3", "postcss": "^8.4.43", @@ -14047,6 +14058,7 @@ "integrity": "sha512-QcQ72gh8a+7JO63TAx/6XZf/CWhgMzu5m0QirvPfGvptOusAxG12w2+aua1Jkjr7hzaWDnJ2n6JFeexMHI+Zjg==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@types/bonjour": "^3.5.13", "@types/connect-history-api-fallback": "^1.5.4", diff --git a/package.json b/package.json index cd4f068..118c514 100644 --- a/package.json +++ b/package.json @@ -18,16 +18,18 @@ "@angular/platform-browser": "^18.2.14", "@angular/platform-browser-dynamic": "^18.2.14", "@angular/router": "^18.2.14", + "@tauri-apps/api": "^2", + "@tauri-apps/plugin-cli": "^2.4.1", + "@tauri-apps/plugin-opener": "^2", "rxjs": "~7.8.0", "tslib": "^2.3.0", - "zone.js": "~0.14.2", - "@tauri-apps/api": "^2", - "@tauri-apps/plugin-opener": "^2" + "zone.js": "~0.14.2" }, "devDependencies": { "@angular-devkit/build-angular": "^18.2.21", "@angular/cli": "^18.2.21", "@angular/compiler-cli": "^18.2.14", + "@tauri-apps/cli": "^2", "@types/jasmine": "~5.1.0", "jasmine-core": "~5.1.0", "karma": "~6.4.0", @@ -35,7 +37,6 @@ "karma-coverage": "~2.2.0", "karma-jasmine": "~5.1.0", "karma-jasmine-html-reporter": "~2.1.0", - "typescript": "~5.4.0", - "@tauri-apps/cli": "^2" + "typescript": "~5.4.0" } } diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index f88d4f4..71502a1 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -41,6 +41,56 @@ dependencies = [ "libc", ] +[[package]] +name = "anstream" +version = "0.6.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43d5b281e737544384e969a5ccad3f1cdd24b48086a0fc1b2a5262a26b8f4f4a" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "is_terminal_polyfill", + "utf8parse", +] + +[[package]] +name = "anstyle" +version = "1.0.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5192cca8006f1fd4f7237516f40fa183bb07f8fbdfedaa0036de5ea9b0b45e78" + +[[package]] +name = "anstyle-parse" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e7644824f0aa2c7b9384579234ef10eb7efb6a0deb83f9630a49594dd9c15c2" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "anstyle-wincon" +version = "3.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d" +dependencies = [ + "anstyle", + "once_cell_polyfill", + "windows-sys 0.61.2", +] + [[package]] name = "anyhow" version = "1.0.100" @@ -456,6 +506,39 @@ dependencies = [ "windows-link 0.2.1", ] +[[package]] +name = "clap" +version = "4.5.54" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6e6ff9dcd79cff5cd969a17a545d79e84ab086e444102a591e288a8aa3ce394" +dependencies = [ + "clap_builder", +] + +[[package]] +name = "clap_builder" +version = "4.5.54" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa42cf4d2b7a41bc8f663a7cab4031ebafa1bf3875705bfaf8466dc60ab52c00" +dependencies = [ + "anstream", + "anstyle", + "clap_lex", + "strsim", +] + +[[package]] +name = "clap_lex" +version = "0.7.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3e64b0cc0439b12df2fa678eae89a1c56a529fd067a9115f7827f1fffd22b32" + +[[package]] +name = "colorchoice" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75" + [[package]] name = "combine" version = "4.6.7" @@ -1750,6 +1833,12 @@ dependencies = [ "once_cell", ] +[[package]] +name = "is_terminal_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" + [[package]] name = "itoa" version = "1.0.15" @@ -1843,6 +1932,7 @@ dependencies = [ "serde_json", "tauri", "tauri-build", + "tauri-plugin-cli", "tauri-plugin-opener", "windows 0.62.2", "x11", @@ -2434,6 +2524,12 @@ version = "1.21.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" +[[package]] +name = "once_cell_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" + [[package]] name = "open" version = "5.3.2" @@ -3754,6 +3850,21 @@ dependencies = [ "walkdir", ] +[[package]] +name = "tauri-plugin-cli" +version = "2.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28e78fb2c09a81546bcd376d34db4bda5769270d00990daa9f0d6e7ac1107e25" +dependencies = [ + "clap", + "log", + "serde", + "serde_json", + "tauri", + "tauri-plugin", + "thiserror 2.0.17", +] + [[package]] name = "tauri-plugin-opener" version = "2.5.0" @@ -4320,6 +4431,12 @@ version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" +[[package]] +name = "utf8parse" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" + [[package]] name = "uuid" version = "1.18.1" diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index ad71753..3308f07 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -34,6 +34,9 @@ windows = { version = "0.62.2", features = [ [target.'cfg(target_os = "linux")'.dependencies] x11 = { version = "2.21.0", features = ["xlib"] } +[target.'cfg(not(any(target_os = "android", target_os = "ios")))'.dependencies] +tauri-plugin-cli = "2" + [profile.dev] incremental = true # Compile your binary in smaller steps. diff --git a/src-tauri/capabilities/desktop.json b/src-tauri/capabilities/desktop.json new file mode 100644 index 0000000..a33e9fd --- /dev/null +++ b/src-tauri/capabilities/desktop.json @@ -0,0 +1,14 @@ +{ + "identifier": "desktop-capability", + "platforms": [ + "macOS", + "windows", + "linux" + ], + "windows": [ + "main" + ], + "permissions": [ + "cli:default" + ] +} \ No newline at end of file diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 446a1c4..8c286b6 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -1,7 +1,7 @@ use enigo::{Direction, Enigo, Key, Keyboard, Settings}; -use tauri::Manager; #[cfg(target_os = "linux")] use std::ptr; +use tauri::Manager; // Learn more about Tauri commands at https://tauri.app/develop/calling-rust/ #[tauri::command] @@ -15,29 +15,39 @@ fn send_key(key: String) -> Result<(), String> { if key.len() == 1 { let ch = key.chars().next().unwrap(); - + // Check if uppercase letter if ch.is_uppercase() && ch.is_alphabetic() { // Send Shift + lowercase letter - enigo.key(Key::Shift, Direction::Press) + enigo + .key(Key::Shift, Direction::Press) .map_err(|e| format!("Shift press error: {}", e))?; - enigo.key(Key::Unicode(ch.to_lowercase().next().unwrap()), Direction::Click) + enigo + .key( + Key::Unicode(ch.to_lowercase().next().unwrap()), + Direction::Click, + ) .map_err(|e| format!("Key error: {}", e))?; - enigo.key(Key::Shift, Direction::Release) + enigo + .key(Key::Shift, Direction::Release) .map_err(|e| format!("Shift release error: {}", e))?; } else { // Send character as-is (lowercase or non-letter) - enigo.key(Key::Unicode(ch), Direction::Click) + enigo + .key(Key::Unicode(ch), Direction::Click) .map_err(|e| format!("Key error: {}", e))?; } } else { // Special keys match key.as_str() { - "Enter" => enigo.key(Key::Return, Direction::Click) + "Enter" => enigo + .key(Key::Return, Direction::Click) .map_err(|e| format!("Key error: {}", e))?, - "Space" => enigo.key(Key::Space, Direction::Click) + "Space" => enigo + .key(Key::Space, Direction::Click) .map_err(|e| format!("Key error: {}", e))?, - "Backspace" => enigo.key(Key::Backspace, Direction::Click) + "Backspace" => enigo + .key(Key::Backspace, Direction::Click) .map_err(|e| format!("Key error: {}", e))?, _ => return Err("Unknown key".to_string()), } @@ -48,26 +58,30 @@ fn send_key(key: String) -> Result<(), String> { #[cfg_attr(mobile, tauri::mobile_entry_point)] pub fn run() { tauri::Builder::default() + .plugin(tauri_plugin_cli::init()) .setup(|app| { let window = app.get_webview_window("main").unwrap(); - + #[cfg(target_os = "windows")] { - use windows::Win32::UI::WindowsAndMessaging::{SetWindowPos, HWND_TOPMOST, SWP_NOACTIVATE, GetWindowLongPtrW, SetWindowLongPtrW, GWL_EXSTYLE, WS_EX_NOACTIVATE}; - use windows::Win32::Foundation::HWND; use raw_window_handle::{HasRawWindowHandle, RawWindowHandle}; - + use windows::Win32::Foundation::HWND; + use windows::Win32::UI::WindowsAndMessaging::{ + GetWindowLongPtrW, SetWindowLongPtrW, SetWindowPos, GWL_EXSTYLE, HWND_TOPMOST, + SWP_NOACTIVATE, WS_EX_NOACTIVATE, + }; + unsafe { if let Ok(RawWindowHandle::Win32(handle)) = window.raw_window_handle() { let hwnd = HWND(handle.hwnd.get() as *mut core::ffi::c_void); - + // Get current extended window style let ex_style = GetWindowLongPtrW(hwnd, GWL_EXSTYLE) as u32; - + // Add WS_EX_NOACTIVATE flag let new_style = ex_style | WS_EX_NOACTIVATE.0; SetWindowLongPtrW(hwnd, GWL_EXSTYLE, new_style as isize); - + // Position window as topmost without activating /* SetWindowPos( @@ -79,12 +93,12 @@ pub fn run() { } } } - + #[cfg(target_os = "linux")] { - use x11::xlib; use raw_window_handle::{HasRawWindowHandle, RawWindowHandle}; - + use x11::xlib; + unsafe { let handle = window.raw_window_handle(); if let Ok(RawWindowHandle::Xlib(h)) = handle { @@ -102,11 +116,11 @@ pub fn run() { } } } - + Ok(()) }) .plugin(tauri_plugin_opener::init()) .invoke_handler(tauri::generate_handler![greet, send_key]) .run(tauri::generate_context!()) .expect("error while running tauri application"); -} \ No newline at end of file +} diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json index 64c770c..d8073f4 100644 --- a/src-tauri/tauri.conf.json +++ b/src-tauri/tauri.conf.json @@ -26,6 +26,29 @@ "csp": null } }, + "plugins": { + "cli":{ + "description": "On-Screen keyboard for Human-Computer Interface studies", + "args": [ + { + "name": "order", + "short": "o", + "takesValue": true, + "multiple": true + }, + { + "name": "limit", + "short": "l", + "takesValue": true + }, + { + "name": "demo", + "short": "d", + "takesValue": true + } + ] + } + }, "bundle": { "active": true, "targets": "all", diff --git a/src/app/app.component.ts b/src/app/app.component.ts index 4ace7cd..26d1cdc 100644 --- a/src/app/app.component.ts +++ b/src/app/app.component.ts @@ -1,7 +1,8 @@ -import { Component, ViewChild, ElementRef, AfterViewInit } from '@angular/core'; +import { Component, ViewChild, ElementRef, AfterViewInit, OnInit } from '@angular/core'; import { CommonModule } from '@angular/common'; import { RouterOutlet } from '@angular/router'; import { invoke } from "@tauri-apps/api/core"; +import { getMatches } from '@tauri-apps/plugin-cli'; import { QwertyKeyboardComponent } from './keyboards/qwerty-keyboard.component'; import { DvorakKeyboardComponent } from './keyboards/dvorak-keyboard.component'; import { CircleKeyboardComponent } from './keyboards/circle-keyboard.component'; @@ -17,9 +18,13 @@ import { CircleKeyboardComponent } from './keyboards/circle-keyboard.component'; CircleKeyboardComponent ], templateUrl: './app.component.html', - styleUrl: './app.component.css' + styleUrl: './app.component.css', }) -export class AppComponent { +export class AppComponent implements OnInit { + async ngOnInit() { + const matches = await getMatches(); + console.log(matches); + } greetingMessage = ""; currentLayout: 'qwerty' | 'dvorak' | 'circle' = 'qwerty'; shiftActive = false;