Skip to content

Commit

Permalink
Implement configurable hotkey feature (#18)
Browse files Browse the repository at this point in the history
* Implement configurable hotkey feature

* Write on the go

* Don't dismiss error when writing buffer

* Implement config manager

* Better write logic for Hotkey Display

* Final batch of implementation
  • Loading branch information
huytd committed Feb 17, 2023
1 parent ba9884d commit 4a03672
Show file tree
Hide file tree
Showing 9 changed files with 457 additions and 14 deletions.
74 changes: 74 additions & 0 deletions src/config.rs
@@ -0,0 +1,74 @@
use std::{
collections::HashMap,
fs::File,
io::{Read, Result, Write},
path::PathBuf,
sync::Mutex,
};

use once_cell::sync::Lazy;

use crate::platform::get_home_dir;

pub static CONFIG_MANAGER: Lazy<Mutex<ConfigStore>> = Lazy::new(|| Mutex::new(ConfigStore::new()));

pub struct ConfigStore {
data: HashMap<String, String>,
}

impl ConfigStore {
fn get_config_path() -> PathBuf {
get_home_dir()
.expect("Cannot read home directory!")
.join(".goxkey")
}

fn load_config_data() -> Result<HashMap<String, String>> {
let mut data = HashMap::new();
let config_path = ConfigStore::get_config_path();
let mut file = File::open(config_path.as_path());
let mut buf = String::new();
if let Ok(mut file) = file {
file.read_to_string(&mut buf);
} else {
buf = format!(
"{} = {}\n{} = {}",
HOTKEY_CONFIG_KEY, "super+ctrl+space", TYPING_METHOD_CONFIG_KEY, "telex"
);
}
buf.lines().for_each(|line| {
if let Some((key, value)) = line.split_once('=') {
data.insert(key.trim().to_owned(), value.trim().to_owned());
}
});
Ok(data)
}

fn write_config_data(data: &HashMap<String, String>) -> Result<()> {
let config_path = ConfigStore::get_config_path();
let mut file = File::create(config_path.as_path())?;
let mut content = String::new();
for (key, value) in data {
content.push_str(&format!("{} = {}\n", key, value));
}
file.write_all(content.as_bytes())
}

pub fn new() -> Self {
Self {
data: ConfigStore::load_config_data().expect("Cannot read config file!"),
}
}

pub fn read(&self, key: &str) -> String {
return self.data.get(key).unwrap_or(&String::new()).to_string();
}

pub fn write(&mut self, key: &str, value: &str) {
self.data.insert(key.to_string(), value.to_string());
ConfigStore::write_config_data(&self.data).expect("Cannot write to config file!");
}
}

pub const HOTKEY_CONFIG_KEY: &str = "hotkey";
pub const TYPING_METHOD_CONFIG_KEY: &str = "method";
125 changes: 125 additions & 0 deletions src/hotkey.rs
@@ -0,0 +1,125 @@
use std::{ascii::AsciiExt, fmt::Display};

use crate::platform::{
KeyModifier, KEY_DELETE, KEY_ENTER, KEY_ESCAPE, KEY_SPACE, KEY_TAB, SYMBOL_ALT, SYMBOL_CTRL,
SYMBOL_SHIFT, SYMBOL_SUPER,
};

pub struct Hotkey {
modifiers: KeyModifier,
keycode: char,
}

impl Hotkey {
pub fn from_str(input: &str) -> Self {
let mut modifiers = KeyModifier::new();
let mut keycode: char = '\0';
input
.split('+')
.for_each(|token| match token.trim().to_uppercase().as_str() {
"SHIFT" => modifiers.add_shift(),
"ALT" => modifiers.add_alt(),
"SUPER" => modifiers.add_super(),
"CTRL" => modifiers.add_control(),
"ENTER" => keycode = KEY_ENTER,
"SPACE" => keycode = KEY_SPACE,
"TAB" => keycode = KEY_TAB,
"DELETE" => keycode = KEY_DELETE,
"ESC" => keycode = KEY_ESCAPE,
c => {
keycode = c.chars().last().unwrap();
}
});
Self { modifiers, keycode }
}

pub fn from(modifiers: KeyModifier, keycode: char) -> Self {
Self { modifiers, keycode }
}

pub fn is_match(&self, modifiers: KeyModifier, keycode: &char) -> bool {
return self.modifiers == modifiers && self.keycode.eq_ignore_ascii_case(keycode);
}

pub fn inner(&self) -> (KeyModifier, char) {
(self.modifiers, self.keycode)
}
}

impl Display for Hotkey {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
if self.modifiers.is_control() {
write!(f, "{} ", SYMBOL_CTRL)?;
}
if self.modifiers.is_shift() {
write!(f, "{} ", SYMBOL_SHIFT)?;
}
if self.modifiers.is_alt() {
write!(f, "{} ", SYMBOL_ALT)?;
}
if self.modifiers.is_super() {
write!(f, "{} ", SYMBOL_SUPER)?;
}
match self.keycode {
KEY_ENTER => write!(f, "Enter"),
KEY_SPACE => write!(f, "Space"),
KEY_TAB => write!(f, "Tab"),
KEY_DELETE => write!(f, "Del"),
KEY_ESCAPE => write!(f, "Esc"),
c => write!(f, "{}", c.to_ascii_uppercase()),
}
}
}

#[test]
fn test_parse() {
let hotkey = Hotkey::from_str("super+shift+z");
let mut actual_modifier = KeyModifier::new();
actual_modifier.add_shift();
actual_modifier.add_super();
assert_eq!(hotkey.modifiers, actual_modifier);
assert_eq!(hotkey.keycode, 'Z');
assert_eq!(hotkey.is_match(actual_modifier, &'z'), true);
}

#[test]
fn test_parse_long_input() {
let hotkey = Hotkey::from_str("super+shift+ctrl+alt+w");
let mut actual_modifier = KeyModifier::new();
actual_modifier.add_shift();
actual_modifier.add_super();
actual_modifier.add_control();
actual_modifier.add_alt();
assert_eq!(hotkey.modifiers, actual_modifier);
assert_eq!(hotkey.keycode, 'W');
assert_eq!(hotkey.is_match(actual_modifier, &'W'), true);
}

#[test]
fn test_parse_with_named_keycode() {
let hotkey = Hotkey::from_str("super+ctrl+space");
let mut actual_modifier = KeyModifier::new();
actual_modifier.add_super();
actual_modifier.add_control();
assert_eq!(hotkey.modifiers, actual_modifier);
assert_eq!(hotkey.keycode, KEY_SPACE);
assert_eq!(hotkey.is_match(actual_modifier, &KEY_SPACE), true);
}

#[test]
fn test_display() {
assert_eq!(
format!("{}", Hotkey::from_str("super+ctrl+space")),
format!("{} {} Space", SYMBOL_CTRL, SYMBOL_SUPER)
);

assert_eq!(
format!("{}", Hotkey::from_str("super+alt+z")),
format!("{} {} Z", SYMBOL_ALT, SYMBOL_SUPER)
);

assert_eq!(
format!("{}", Hotkey::from_str("ctrl+shift+o")),
format!("{} {} O", SYMBOL_CTRL, SYMBOL_SHIFT)
);
}
62 changes: 60 additions & 2 deletions src/input.rs
@@ -1,7 +1,16 @@
use druid::Data;
use std::{fmt::Display, str::FromStr};

use druid::{Data, Target};
use log::debug;
use once_cell::sync::Lazy;

use crate::{
config::{CONFIG_MANAGER, HOTKEY_CONFIG_KEY, TYPING_METHOD_CONFIG_KEY},
hotkey::Hotkey,
ui::UPDATE_UI,
UI_EVENT_SINK,
};

// According to Google search, the longest possible Vietnamese word
// is "nghiêng", which is 7 letters long. Add a little buffer for
// tone and marks, I guess the longest possible buffer length would
Expand Down Expand Up @@ -29,20 +38,47 @@ pub enum TypingMethod {
Telex,
}

impl FromStr for TypingMethod {
type Err = ();

fn from_str(s: &str) -> Result<Self, Self::Err> {
Ok(match s.to_ascii_lowercase().as_str() {
"vni" => TypingMethod::VNI,
_ => TypingMethod::Telex,
})
}
}

impl Display for TypingMethod {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
"{}",
match self {
Self::VNI => "vni",
Self::Telex => "telex",
}
)
}
}

pub struct InputState {
buffer: String,
display_buffer: String,
method: TypingMethod,
hotkey: Hotkey,
enabled: bool,
should_track: bool,
}

impl InputState {
pub fn new() -> Self {
let config = CONFIG_MANAGER.lock().unwrap();
Self {
buffer: String::new(),
display_buffer: String::new(),
method: TypingMethod::Telex,
method: TypingMethod::from_str(&config.read(TYPING_METHOD_CONFIG_KEY)).unwrap(),
hotkey: Hotkey::from_str(&config.read(HOTKEY_CONFIG_KEY)),
enabled: true,
should_track: true,
}
Expand Down Expand Up @@ -80,12 +116,34 @@ impl InputState {
pub fn set_method(&mut self, method: TypingMethod) {
self.method = method;
self.new_word();
CONFIG_MANAGER
.lock()
.unwrap()
.write(TYPING_METHOD_CONFIG_KEY, &method.to_string());
if let Some(event_sink) = UI_EVENT_SINK.get() {
_ = event_sink.submit_command(UPDATE_UI, (), Target::Auto);
}
}

pub fn get_method(&self) -> TypingMethod {
self.method
}

pub fn set_hotkey(&mut self, key_sequence: &str) {
self.hotkey = Hotkey::from_str(key_sequence);
CONFIG_MANAGER
.lock()
.unwrap()
.write(HOTKEY_CONFIG_KEY, key_sequence);
if let Some(event_sink) = UI_EVENT_SINK.get() {
_ = event_sink.submit_command(UPDATE_UI, (), Target::Auto);
}
}

pub fn get_hotkey(&self) -> &Hotkey {
return &self.hotkey;
}

pub fn should_transform_keys(&self, c: &char) -> bool {
self.enabled
&& match self.method {
Expand Down
6 changes: 4 additions & 2 deletions src/main.rs
@@ -1,3 +1,5 @@
mod config;
mod hotkey;
mod input;
mod platform;
mod ui;
Expand Down Expand Up @@ -37,7 +39,7 @@ fn event_handler(handle: Handle, keycode: Option<char>, modifiers: KeyModifier)
match keycode {
Some(keycode) => {
// Toggle Vietnamese input mod with Ctrl + Cmd + Space key
if modifiers.is_control() && modifiers.is_super() && keycode == KEY_SPACE {
if INPUT_STATE.get_hotkey().is_match(modifiers, &keycode) {
INPUT_STATE.toggle_vietnamese();
if let Some(event_sink) = UI_EVENT_SINK.get() {
_ = event_sink.submit_command(UPDATE_UI, (), Target::Auto);
Expand Down Expand Up @@ -99,7 +101,7 @@ fn main() {

let win = WindowDesc::new(ui::main_ui_builder)
.title("gõkey")
.window_size((320.0, 200.0))
.window_size((320.0, 234.0))
.resizable(false);
let app = AppLauncher::with_window(win);
let event_sink = app.get_external_handle();
Expand Down
9 changes: 9 additions & 0 deletions src/platform/linux.rs
Expand Up @@ -2,6 +2,15 @@

use super::CallbackFn;

pub const SYMBOL_SHIFT: &str = "⇧";
pub const SYMBOL_CTRL: &str = "⌃";
pub const SYMBOL_SUPER: &str = "❖";
pub const SYMBOL_ALT: &str = "⌥";

pub fn get_home_dir() -> Option<PathBuf> {
env::var("HOME").ok().map(PathBuf::from)
}

pub fn send_backspace(count: usize) -> Result<(), ()> {
todo!()
}
Expand Down
11 changes: 10 additions & 1 deletion src/platform/macos.rs
@@ -1,4 +1,4 @@
use std::ptr;
use std::{env, os, path::PathBuf, ptr};

use super::{CallbackFn, KeyModifier, KEY_DELETE, KEY_ENTER, KEY_ESCAPE, KEY_SPACE, KEY_TAB};
use core_foundation::runloop::{kCFRunLoopCommonModes, CFRunLoop};
Expand All @@ -12,6 +12,15 @@ use core_graphics::{

pub type Handle = CGEventTapProxy;

pub const SYMBOL_SHIFT: &str = "⇧";
pub const SYMBOL_CTRL: &str = "⌃";
pub const SYMBOL_SUPER: &str = "⌘";
pub const SYMBOL_ALT: &str = "⌥";

pub fn get_home_dir() -> Option<PathBuf> {
env::var("HOME").ok().map(PathBuf::from)
}

// Modified from http://ritter.ist.psu.edu/projects/RUI/macosx/rui.c
fn get_char(keycode: CGKeyCode) -> Option<char> {
match keycode {
Expand Down

0 comments on commit 4a03672

Please sign in to comment.