Skip to content

Commit

Permalink
feat: Auto toggle Vietnamese input mode based on application name
Browse files Browse the repository at this point in the history
Closes #38

With this change, we track the front-most application on every event (that might sound inefficient, but let's ship it and see how things goes).

The auto-toggling rule are implemented as:

- If the app is configured as Vietnamese, switch to Vietnamese mode
- If the app is configured as English, switch to English mode
- If the app is not configured, do nothing

Also, when the user toggling typing mode manually, we will track it and store the config.

All the configs are stored in ~/.goxkey file.
  • Loading branch information
huytd committed Sep 27, 2023
1 parent db0edbc commit 6e793c4
Show file tree
Hide file tree
Showing 5 changed files with 162 additions and 51 deletions.
138 changes: 95 additions & 43 deletions src/config.rs
Original file line number Diff line number Diff line change
@@ -1,10 +1,5 @@
use std::{
collections::HashMap,
fs::File,
io::{Read, Result, Write},
path::PathBuf,
sync::Mutex,
};
use std::{fs::File, io::{Result, Write}, io, path::PathBuf, sync::Mutex};
use std::io::BufRead;

use once_cell::sync::Lazy;

Expand All @@ -13,7 +8,17 @@ 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>,
hotkey: String,
method: String,
vn_apps: Vec<String>,
en_apps: Vec<String>
}

fn parse_vec_string(line: String) -> Vec<String> {
line.split(',')
.map(|s| s.trim().to_string())
.filter(|s| !s.is_empty())
.collect()
}

impl ConfigStore {
Expand All @@ -23,52 +28,99 @@ impl ConfigStore {
.join(".goxkey")
}

fn load_config_data() -> Result<HashMap<String, String>> {
let mut data = HashMap::new();
let config_path = ConfigStore::get_config_path();
let 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(&mut self) -> Result<()> {
let mut file = File::create(ConfigStore::get_config_path())?;

writeln!(file, "{} = {}", HOTKEY_CONFIG_KEY, self.hotkey)?;
writeln!(file, "{} = {}", TYPING_METHOD_CONFIG_KEY, self.method)?;
writeln!(file, "{} = {}", VN_APPS_CONFIG_KEY, self.vn_apps.join(","))?;
writeln!(file, "{} = {}", EN_APPS_CONFIG_KEY, self.en_apps.join(","))?;

Ok(())
}

fn write_config_data(data: &HashMap<String, String>) -> Result<()> {
pub fn new() -> Self {
let mut config = Self {
hotkey: "ctrl+space".to_string(),
method: "telex".to_string(),
vn_apps: Vec::new(),
en_apps: Vec::new()
};

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));

if let Ok(file) = File::open(config_path) {
let reader = io::BufReader::new(file);
for line in reader.lines() {
if let Some((left, right)) = line.unwrap_or_default().split_once(" = ") {
match left {
HOTKEY_CONFIG_KEY => config.hotkey = right.to_string(),
TYPING_METHOD_CONFIG_KEY => config.method = right.to_string(),
VN_APPS_CONFIG_KEY => config.vn_apps = parse_vec_string(right.to_string()),
EN_APPS_CONFIG_KEY => config.en_apps = parse_vec_string(right.to_string()),
_ => { }
}
}
}
}
file.write_all(content.as_bytes())

config
}

pub fn new() -> Self {
Self {
data: ConfigStore::load_config_data().expect("Cannot read config file!"),
// Hotkey
pub fn get_hotkey(&self) -> &str {
&self.hotkey
}

pub fn set_hotkey(&mut self, hotkey: &str) {
self.hotkey = hotkey.to_string();
self.save();
}

// Method
pub fn get_method(&self) -> &str {
&self.method
}

pub fn set_method(&mut self, method: &str) {
self.method = method.to_string();
self.save();
}

pub fn is_vietnamese_app(&self, app_name: &str) -> bool {
self.vn_apps.contains(&app_name.to_string())
}

pub fn is_english_app(&self, app_name: &str) -> bool {
self.en_apps.contains(&app_name.to_string())
}

pub fn add_vietnamese_app(&mut self, app_name: &str) {
if self.is_english_app(app_name) {
// Remove from english apps
self.en_apps.retain(|x| x != app_name);
}
self.vn_apps.push(app_name.to_string());
self.save();
}

pub fn read(&self, key: &str) -> String {
return self.data.get(key).unwrap_or(&String::new()).to_string();
pub fn add_english_app(&mut self, app_name: &str) {
if self.is_vietnamese_app(app_name) {
// Remove from vietnamese apps
self.vn_apps.retain(|x| x != app_name);
}
self.en_apps.push(app_name.to_string());
self.save();
}

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!");
// Save config to file
fn save(&mut self) {
self.write_config_data()
.expect("Failed to write config");
}
}

pub const HOTKEY_CONFIG_KEY: &str = "hotkey";
pub const TYPING_METHOD_CONFIG_KEY: &str = "method";
const HOTKEY_CONFIG_KEY: &str = "hotkey";
const TYPING_METHOD_CONFIG_KEY: &str = "method";
const VN_APPS_CONFIG_KEY: &str = "vn-apps";
const EN_APPS_CONFIG_KEY: &str = "en-apps";
37 changes: 29 additions & 8 deletions src/input.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,12 @@ use once_cell::sync::{Lazy, OnceCell};
use rdev::{Keyboard, KeyboardState};

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

// According to Google search, the longest possible Vietnamese word
// is "nghiêng", which is 7 letters long. Add a little buffer for
Expand Down Expand Up @@ -152,7 +153,8 @@ pub struct InputState {
hotkey: Hotkey,
enabled: bool,
should_track: bool,
previous_word: String
previous_word: String,
active_app: String
}

impl InputState {
Expand All @@ -161,11 +163,24 @@ impl InputState {
Self {
buffer: String::new(),
display_buffer: String::new(),
method: TypingMethod::from_str(&config.read(TYPING_METHOD_CONFIG_KEY)).unwrap(),
hotkey: Hotkey::from_str(&config.read(HOTKEY_CONFIG_KEY)),
method: TypingMethod::from_str(config.get_method()).unwrap(),
hotkey: Hotkey::from_str(config.get_hotkey()),
enabled: true,
should_track: true,
previous_word: String::new()
previous_word: String::new(),
active_app: String::new()
}
}

pub fn update_active_app(&mut self) {
self.active_app = get_active_app_name();
let config = CONFIG_MANAGER.lock().unwrap();
// Only switch the input mode if we found the app in the config
if config.is_vietnamese_app(&self.active_app) {
self.enabled = true;
}
if config.is_english_app(&self.active_app) {
self.enabled = false;
}
}

Expand Down Expand Up @@ -203,6 +218,12 @@ impl InputState {

pub fn toggle_vietnamese(&mut self) {
self.enabled = !self.enabled;
let mut config = CONFIG_MANAGER.lock().unwrap();
if self.enabled {
config.add_vietnamese_app(&self.active_app);
} else {
config.add_english_app(&self.active_app);
}
self.new_word();
}

Expand All @@ -212,7 +233,7 @@ impl InputState {
CONFIG_MANAGER
.lock()
.unwrap()
.write(TYPING_METHOD_CONFIG_KEY, &method.to_string());
.set_method(&method.to_string());
if let Some(event_sink) = UI_EVENT_SINK.get() {
_ = event_sink.submit_command(UPDATE_UI, (), Target::Auto);
}
Expand All @@ -227,7 +248,7 @@ impl InputState {
CONFIG_MANAGER
.lock()
.unwrap()
.write(HOTKEY_CONFIG_KEY, key_sequence);
.set_hotkey(key_sequence);
if let Some(event_sink) = UI_EVENT_SINK.get() {
_ = event_sink.submit_command(UPDATE_UI, (), Target::Auto);
}
Expand Down Expand Up @@ -345,4 +366,4 @@ impl InputState {
debug!("! Stop tracking");
}
}
}
}
8 changes: 8 additions & 0 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -56,8 +56,16 @@ unsafe fn toggle_vietnamese() {
}
}

unsafe fn auto_toggle_vietnamese() {
INPUT_STATE.update_active_app();
if let Some(event_sink) = UI_EVENT_SINK.get() {
_ = event_sink.submit_command(UPDATE_UI, (), Target::Auto);
}
}

fn event_handler(handle: Handle, pressed_key: Option<PressedKey>, modifiers: KeyModifier) -> bool {
unsafe {
auto_toggle_vietnamese();
match pressed_key {
Some(pressed_key) => {
match pressed_key {
Expand Down
29 changes: 29 additions & 0 deletions src/platform/macos.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ use cocoa::{
base::{nil, YES},
foundation::NSDictionary,
};
use cocoa::base::id;
use core_graphics::{
event::{
CGEventFlags, CGEventTapLocation, CGEventTapOptions, CGEventTapPlacement, CGEventType,
Expand Down Expand Up @@ -36,6 +37,24 @@ pub const SYMBOL_ALT: &str = "⌥";

pub const HIDE_COMMAND: Selector = HIDE_APPLICATION;

#[macro_export]
macro_rules! nsstring_to_string {
($ns_string:expr) => {{
use objc::{sel, sel_impl};
let utf8: id = objc::msg_send![$ns_string, UTF8String];
let string = if !utf8.is_null() {
Some({
std::ffi::CStr::from_ptr(utf8 as *const std::ffi::c_char)
.to_string_lossy()
.into_owned()
})
} else {
None
};
string
}};
}

pub fn get_home_dir() -> Option<PathBuf> {
env::var("HOME").ok().map(PathBuf::from)
}
Expand Down Expand Up @@ -200,3 +219,13 @@ pub fn ensure_accessibility_permission() -> bool {
return AXIsProcessTrustedWithOptions(options as _);
}
}

pub fn get_active_app_name() -> String {
unsafe {
let shared_workspace: id = msg_send![class!(NSWorkspace), sharedWorkspace];
let front_most_app: id = msg_send![shared_workspace, frontmostApplication];
let bundle_url: id = msg_send![front_most_app, bundleURL];
let path: id = msg_send![bundle_url, path];
nsstring_to_string!(path).unwrap_or("/Unknown.app".to_string())
}
}
1 change: 1 addition & 0 deletions src/platform/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ use std::fmt::Display;
use bitflags::bitflags;
pub use os::{
ensure_accessibility_permission, get_home_dir, run_event_listener, send_backspace, send_string,
get_active_app_name,
Handle, HIDE_COMMAND, SYMBOL_ALT, SYMBOL_CTRL, SYMBOL_SHIFT, SYMBOL_SUPER,
};

Expand Down

0 comments on commit 6e793c4

Please sign in to comment.