feat: terminal VNI input — force VNI in terminals, remove from bypass_apps
- Add terminal_apps / terminal_input_method to config - AppStateManager tracks global vs effective method - Engine uses effective method (VNI in terminals, global elsewhere) - Terminals removed from bypass_apps, moved to terminal_apps - Tray still shows global method (user's setting) - NOTE: NOTES/terminal-vni.md documents the design
This commit is contained in:
parent
7e5281244b
commit
3ccf243f52
6 changed files with 316 additions and 31 deletions
110
NOTES/terminal-vni.md
Normal file
110
NOTES/terminal-vni.md
Normal file
|
|
@ -0,0 +1,110 @@
|
||||||
|
# Terminal VNI Input — Design & Implementation
|
||||||
|
|
||||||
|
## Goal
|
||||||
|
|
||||||
|
Make Vietnamese input work in terminal emulators without breaking TUI keyboard shortcuts.
|
||||||
|
|
||||||
|
## Approach: A + C
|
||||||
|
|
||||||
|
### A — Remove terminals from `bypass_apps`
|
||||||
|
|
||||||
|
All terminals are currently in `bypass_apps` (default config), which skips ALL engine
|
||||||
|
processing when the active window is a terminal. Removing them lets keystrokes flow
|
||||||
|
through the bamboo engine.
|
||||||
|
|
||||||
|
### C — Force VNI when terminal detected
|
||||||
|
|
||||||
|
When the active window is a terminal, the engine automatically uses VNI rules
|
||||||
|
(`1-9` for tones/marks) regardless of the global VNI/Telex setting.
|
||||||
|
This avoids key conflicts with TUI apps (vim's `j`, less's `s`, shell's `x`, etc.).
|
||||||
|
|
||||||
|
## How It Works
|
||||||
|
|
||||||
|
```
|
||||||
|
User config: input_method = "telex"
|
||||||
|
Terminal window focused → effective method = "vni" (forced by terminal_apps)
|
||||||
|
GUI window focused → effective method = "telex" (user's global setting)
|
||||||
|
```
|
||||||
|
|
||||||
|
- **Engine** runs with effective method
|
||||||
|
- **Tray** shows global method (so user sees their configured setting)
|
||||||
|
- **Ctrl+LeftShift** toggles global method, recomputes effective method
|
||||||
|
- **Ctrl+Space** toggles VN/EN as before
|
||||||
|
|
||||||
|
## Config Changes
|
||||||
|
|
||||||
|
```toml
|
||||||
|
[app_state]
|
||||||
|
terminal_apps = ["kitty", "alacritty", "foot", "wezterm", "konsole",
|
||||||
|
"gnome-terminal", "gnome-terminal-server", "kgx", "st", "urxvt", "xterm",
|
||||||
|
"termite", "terminator", "tilix", "deepin-terminal", "pantheon-terminal"]
|
||||||
|
terminal_input_method = "vni"
|
||||||
|
```
|
||||||
|
|
||||||
|
`bypass_apps` reduced to: `["steam", "dota", "csgo", "minecraft", "factorio"]`
|
||||||
|
|
||||||
|
## Implementation
|
||||||
|
|
||||||
|
### 1. `daemon/src/config.rs`
|
||||||
|
|
||||||
|
- Add `terminal_apps` (`Vec<String>`) and `terminal_input_method` (`String`) to `AppStateConfig`
|
||||||
|
- Add `default_terminal_apps()` returning the terminal list
|
||||||
|
- Add `default_terminal_method()` returning `"vni"`
|
||||||
|
- Remove all terminal names from `default_bypass_apps()`
|
||||||
|
|
||||||
|
### 2. `daemon/src/app_state.rs`
|
||||||
|
|
||||||
|
- Add fields: `terminal_apps`, `terminal_input_method`, `global_method`, `effective_method`
|
||||||
|
- `new()` accepts `terminal_apps`, `terminal_input_method`, `global_method`
|
||||||
|
- `update_effective_method()`: if current_app matches any terminal, effective = terminal method; else effective = global method. Called on window change.
|
||||||
|
- `set_terminal_config()`: updates terminal_apps/terminal_input_method from config reload
|
||||||
|
- `set_global_method()`: updates global_method, recomputes effective
|
||||||
|
- `effective_method()` getter
|
||||||
|
- `is_terminal_app()` — checks if current_app is a terminal
|
||||||
|
- `update_with_app()` calls `update_effective_method()` internally
|
||||||
|
- `update_lists()` also handles terminal_apps
|
||||||
|
|
||||||
|
### 3. `daemon/src/main.rs`
|
||||||
|
|
||||||
|
- `Daemon::new()` — pass terminal config to `AppStateManager`, call `update_effective_method()`
|
||||||
|
- `toggle_method()` — after toggling global method, call `app_state.set_global_method()` then `engine.set_method(app_state.effective_method())`
|
||||||
|
- `check_app_change_with()` — after app change, if effective method changed from engine's current, call `engine.set_method(effective)`
|
||||||
|
- `is_vn_control_key()` calls — change from `daemon.config.input_method` to `daemon.app_state.effective_method()`
|
||||||
|
- Config reload — update `update_lists()` call to include terminal fields
|
||||||
|
- Method status file — still writes **global** method (for tray display)
|
||||||
|
|
||||||
|
### 4. `install.sh` — Update default config block
|
||||||
|
|
||||||
|
### 5. `README.md` — Update config example
|
||||||
|
|
||||||
|
### 6. `NOTES/terminal-vni.md` — This file
|
||||||
|
|
||||||
|
## Testing Checklist
|
||||||
|
|
||||||
|
### Linux Mint (X11)
|
||||||
|
|
||||||
|
- [ ] Type VNI in shell: `viet1 nam` → `viết nam`
|
||||||
|
- [ ] Type Telex in shell: `vieets nam` → `vieets nam` (Telex NOT active in terminal)
|
||||||
|
- [ ] Ctrl+Space toggles VN/EN
|
||||||
|
- [ ] Ctrl+LeftShift toggles global method (terminal unaffected, tray shows global)
|
||||||
|
- [ ] Vim insert mode: VNI works, `j`/`x`/`s` pass through as regular keys
|
||||||
|
- [ ] Gemini-cli: VNI typed text appears correctly
|
||||||
|
- [ ] sudo passwd: engine auto-disables
|
||||||
|
- [ ] Switch terminal ↔ GUI: method resets per app
|
||||||
|
- [ ] Tray icon shows global method, not terminal override
|
||||||
|
|
||||||
|
### Ubuntu 24.04+ (Wayland)
|
||||||
|
|
||||||
|
- [ ] Same VNI typing tests
|
||||||
|
- [ ] GNOME Shell D-Bus window detection
|
||||||
|
- [ ] wl-copy paste-once path for Unicode chars
|
||||||
|
|
||||||
|
## Edge Cases
|
||||||
|
|
||||||
|
| Case | Behavior |
|
||||||
|
|------|----------|
|
||||||
|
| Terminal in bypass_apps | No IME at all (configurable override for power users) |
|
||||||
|
| User wants Telex in terminals | Set `terminal_input_method = "telex"` in config |
|
||||||
|
| Multiple terminals open | Each follows the same rule |
|
||||||
|
| IDE integrated terminal | Window class is "code", not terminal. Needs manual config |
|
||||||
|
| Password prompt in terminal | Process-tree detection still disables engine regardless of method |
|
||||||
|
|
@ -208,9 +208,12 @@ password_apps = ["pinentry", "pinentry-gtk-2", "pinentry-qt",
|
||||||
|
|
||||||
[app_state]
|
[app_state]
|
||||||
enabled = true
|
enabled = true
|
||||||
english_apps = ["code", "vim", "kitty", "foot"]
|
english_apps = ["code", "vim"]
|
||||||
vietnamese_apps = ["telegram", "discord", "firefox"]
|
vietnamese_apps = ["telegram", "discord", "firefox"]
|
||||||
bypass_apps = ["kitty", "alacritty", "steam"]
|
bypass_apps = ["steam"]
|
||||||
|
terminal_apps = ["kitty", "alacritty", "gnome-terminal", "konsole", "foot",
|
||||||
|
"wezterm", "st", "urxvt", "xterm"]
|
||||||
|
terminal_input_method = "vni"
|
||||||
|
|
||||||
[macros]
|
[macros]
|
||||||
ko = "không"
|
ko = "không"
|
||||||
|
|
|
||||||
|
|
@ -495,6 +495,14 @@ pub struct AppStateManager {
|
||||||
vietnamese_apps: Vec<String>,
|
vietnamese_apps: Vec<String>,
|
||||||
/// Bypass apps from config
|
/// Bypass apps from config
|
||||||
bypass_apps: Vec<String>,
|
bypass_apps: Vec<String>,
|
||||||
|
/// Terminal emulator class names (force VNI mode)
|
||||||
|
terminal_apps: Vec<String>,
|
||||||
|
/// Input method forced in terminals
|
||||||
|
terminal_input_method: String,
|
||||||
|
/// User's global input method (VNI/Telex)
|
||||||
|
global_method: String,
|
||||||
|
/// Effective method after terminal override
|
||||||
|
effective_method: String,
|
||||||
/// Global enabled state
|
/// Global enabled state
|
||||||
global_enabled: bool,
|
global_enabled: bool,
|
||||||
/// Password detection config
|
/// Password detection config
|
||||||
|
|
@ -514,14 +522,27 @@ impl AppStateManager {
|
||||||
english_apps: Vec<String>,
|
english_apps: Vec<String>,
|
||||||
vietnamese_apps: Vec<String>,
|
vietnamese_apps: Vec<String>,
|
||||||
bypass_apps: Vec<String>,
|
bypass_apps: Vec<String>,
|
||||||
|
terminal_apps: Vec<String>,
|
||||||
|
terminal_input_method: String,
|
||||||
|
global_method: String,
|
||||||
global_enabled: bool,
|
global_enabled: bool,
|
||||||
) -> Self {
|
) -> Self {
|
||||||
|
let effective_method = Self::compute_effective_method(
|
||||||
|
&global_method,
|
||||||
|
&terminal_input_method,
|
||||||
|
&terminal_apps,
|
||||||
|
"",
|
||||||
|
);
|
||||||
Self {
|
Self {
|
||||||
current_app: String::new(),
|
current_app: String::new(),
|
||||||
overrides: HashMap::new(),
|
overrides: HashMap::new(),
|
||||||
english_apps: english_apps.iter().map(|s| s.to_lowercase()).collect(),
|
english_apps: english_apps.iter().map(|s| s.to_lowercase()).collect(),
|
||||||
vietnamese_apps: vietnamese_apps.iter().map(|s| s.to_lowercase()).collect(),
|
vietnamese_apps: vietnamese_apps.iter().map(|s| s.to_lowercase()).collect(),
|
||||||
bypass_apps: bypass_apps.iter().map(|s| s.to_lowercase()).collect(),
|
bypass_apps: bypass_apps.iter().map(|s| s.to_lowercase()).collect(),
|
||||||
|
terminal_apps: terminal_apps.iter().map(|s| s.to_lowercase()).collect(),
|
||||||
|
terminal_input_method,
|
||||||
|
global_method,
|
||||||
|
effective_method,
|
||||||
global_enabled,
|
global_enabled,
|
||||||
password_enabled: false,
|
password_enabled: false,
|
||||||
check_atspi2: true,
|
check_atspi2: true,
|
||||||
|
|
@ -533,6 +554,22 @@ impl AppStateManager {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Compute effective method: use terminal override if current_app is a terminal,
|
||||||
|
/// otherwise use the global method.
|
||||||
|
fn compute_effective_method(
|
||||||
|
global_method: &str,
|
||||||
|
terminal_method: &str,
|
||||||
|
terminal_apps: &[String],
|
||||||
|
current_app: &str,
|
||||||
|
) -> String {
|
||||||
|
for pattern in terminal_apps {
|
||||||
|
if current_app.contains(pattern.as_str()) {
|
||||||
|
return terminal_method.to_string();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
global_method.to_string()
|
||||||
|
}
|
||||||
|
|
||||||
/// Update password detection config
|
/// Update password detection config
|
||||||
pub fn set_password_config(
|
pub fn set_password_config(
|
||||||
&mut self,
|
&mut self,
|
||||||
|
|
@ -549,6 +586,48 @@ impl AppStateManager {
|
||||||
self.password_apps = password_apps.iter().map(|s| s.to_lowercase()).collect();
|
self.password_apps = password_apps.iter().map(|s| s.to_lowercase()).collect();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Update terminal detection config
|
||||||
|
pub fn set_terminal_config(
|
||||||
|
&mut self,
|
||||||
|
terminal_apps: Vec<String>,
|
||||||
|
terminal_input_method: String,
|
||||||
|
) {
|
||||||
|
self.terminal_apps = terminal_apps.iter().map(|s| s.to_lowercase()).collect();
|
||||||
|
self.terminal_input_method = terminal_input_method;
|
||||||
|
self.update_effective_method();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Set the user's global input method and recompute effective method
|
||||||
|
pub fn set_global_method(&mut self, method: &str) {
|
||||||
|
self.global_method = method.to_string();
|
||||||
|
self.update_effective_method();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Recompute effective method based on terminal override
|
||||||
|
pub fn update_effective_method(&mut self) {
|
||||||
|
self.effective_method = Self::compute_effective_method(
|
||||||
|
&self.global_method,
|
||||||
|
&self.terminal_input_method,
|
||||||
|
&self.terminal_apps,
|
||||||
|
&self.current_app,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get the effective input method (terminal override applied)
|
||||||
|
pub fn effective_method(&self) -> &str {
|
||||||
|
&self.effective_method
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Check if the current app is a terminal emulator
|
||||||
|
pub fn is_terminal_app(&self) -> bool {
|
||||||
|
for pattern in &self.terminal_apps {
|
||||||
|
if self.current_app.contains(pattern.as_str()) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
false
|
||||||
|
}
|
||||||
|
|
||||||
/// Check if the current focused widget is a password field
|
/// Check if the current focused widget is a password field
|
||||||
/// Returns true if password detected, forcing English mode
|
/// Returns true if password detected, forcing English mode
|
||||||
pub fn check_password_field(&mut self) -> bool {
|
pub fn check_password_field(&mut self) -> bool {
|
||||||
|
|
@ -615,12 +694,22 @@ impl AppStateManager {
|
||||||
}
|
}
|
||||||
|
|
||||||
let old_app = self.current_app.clone();
|
let old_app = self.current_app.clone();
|
||||||
|
let old_is_terminal = self.is_terminal_app();
|
||||||
self.current_app = new_class;
|
self.current_app = new_class;
|
||||||
|
|
||||||
eprintln!("[vietc] App: {} → {}", old_app, self.current_app);
|
eprintln!("[vietc] App: {} → {}", old_app, self.current_app);
|
||||||
|
|
||||||
|
// Recompute effective method on window change
|
||||||
|
self.update_effective_method();
|
||||||
|
let new_is_terminal = self.is_terminal_app();
|
||||||
|
let method_changed = old_is_terminal != new_is_terminal;
|
||||||
|
|
||||||
let should_enable = self.get_default_state();
|
let should_enable = self.get_default_state();
|
||||||
Some(should_enable)
|
if method_changed {
|
||||||
|
Some(should_enable) // signal caller that method might have changed
|
||||||
|
} else {
|
||||||
|
Some(should_enable)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get the default Vietnamese state for the current app
|
/// Get the default Vietnamese state for the current app
|
||||||
|
|
@ -680,15 +769,21 @@ impl AppStateManager {
|
||||||
english_apps: Vec<String>,
|
english_apps: Vec<String>,
|
||||||
vietnamese_apps: Vec<String>,
|
vietnamese_apps: Vec<String>,
|
||||||
bypass_apps: Vec<String>,
|
bypass_apps: Vec<String>,
|
||||||
|
terminal_apps: Vec<String>,
|
||||||
|
terminal_input_method: String,
|
||||||
) -> &Self {
|
) -> &Self {
|
||||||
self.english_apps = english_apps.iter().map(|s| s.to_lowercase()).collect();
|
self.english_apps = english_apps.iter().map(|s| s.to_lowercase()).collect();
|
||||||
self.vietnamese_apps = vietnamese_apps.iter().map(|s| s.to_lowercase()).collect();
|
self.vietnamese_apps = vietnamese_apps.iter().map(|s| s.to_lowercase()).collect();
|
||||||
self.bypass_apps = bypass_apps.iter().map(|s| s.to_lowercase()).collect();
|
self.bypass_apps = bypass_apps.iter().map(|s| s.to_lowercase()).collect();
|
||||||
|
self.terminal_apps = terminal_apps.iter().map(|s| s.to_lowercase()).collect();
|
||||||
|
self.terminal_input_method = terminal_input_method;
|
||||||
|
self.update_effective_method();
|
||||||
eprintln!(
|
eprintln!(
|
||||||
"[vietc] App lists updated: {} English, {} Vietnamese, {} Bypass",
|
"[vietc] App lists updated: {} English, {} Vietnamese, {} Bypass, {} Terminal",
|
||||||
self.english_apps.len(),
|
self.english_apps.len(),
|
||||||
self.vietnamese_apps.len(),
|
self.vietnamese_apps.len(),
|
||||||
self.bypass_apps.len()
|
self.bypass_apps.len(),
|
||||||
|
self.terminal_apps.len()
|
||||||
);
|
);
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -94,6 +94,12 @@ pub struct AppStateConfig {
|
||||||
|
|
||||||
#[serde(default = "default_bypass_apps")]
|
#[serde(default = "default_bypass_apps")]
|
||||||
pub bypass_apps: Vec<String>,
|
pub bypass_apps: Vec<String>,
|
||||||
|
|
||||||
|
#[serde(default = "default_terminal_apps")]
|
||||||
|
pub terminal_apps: Vec<String>,
|
||||||
|
|
||||||
|
#[serde(default = "default_terminal_method")]
|
||||||
|
pub terminal_input_method: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for AutoRestoreConfig {
|
impl Default for AutoRestoreConfig {
|
||||||
|
|
@ -112,6 +118,8 @@ impl Default for AppStateConfig {
|
||||||
english_apps: default_english_apps(),
|
english_apps: default_english_apps(),
|
||||||
vietnamese_apps: default_vietnamese_apps(),
|
vietnamese_apps: default_vietnamese_apps(),
|
||||||
bypass_apps: default_bypass_apps(),
|
bypass_apps: default_bypass_apps(),
|
||||||
|
terminal_apps: default_terminal_apps(),
|
||||||
|
terminal_input_method: default_terminal_method(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -175,6 +183,16 @@ fn default_english_apps() -> Vec<String> {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn default_bypass_apps() -> Vec<String> {
|
fn default_bypass_apps() -> Vec<String> {
|
||||||
|
vec![
|
||||||
|
"steam".into(),
|
||||||
|
"dota".into(),
|
||||||
|
"csgo".into(),
|
||||||
|
"minecraft".into(),
|
||||||
|
"factorio".into(),
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
fn default_terminal_apps() -> Vec<String> {
|
||||||
vec![
|
vec![
|
||||||
"terminal".into(),
|
"terminal".into(),
|
||||||
"kitty".into(),
|
"kitty".into(),
|
||||||
|
|
@ -183,17 +201,26 @@ fn default_bypass_apps() -> Vec<String> {
|
||||||
"wezterm".into(),
|
"wezterm".into(),
|
||||||
"konsole".into(),
|
"konsole".into(),
|
||||||
"gnome-terminal".into(),
|
"gnome-terminal".into(),
|
||||||
|
"gnome-terminal-server".into(),
|
||||||
|
"kgx".into(),
|
||||||
"st".into(),
|
"st".into(),
|
||||||
"urxvt".into(),
|
"urxvt".into(),
|
||||||
"xterm".into(),
|
"xterm".into(),
|
||||||
"steam".into(),
|
"termite".into(),
|
||||||
"dota".into(),
|
"terminator".into(),
|
||||||
"csgo".into(),
|
"tilix".into(),
|
||||||
"minecraft".into(),
|
"deepin-terminal".into(),
|
||||||
"factorio".into(),
|
"pantheon-terminal".into(),
|
||||||
|
"blackbox".into(),
|
||||||
|
"contour".into(),
|
||||||
|
"cool-retro-term".into(),
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn default_terminal_method() -> String {
|
||||||
|
"vni".into()
|
||||||
|
}
|
||||||
|
|
||||||
fn default_vietnamese_apps() -> Vec<String> {
|
fn default_vietnamese_apps() -> Vec<String> {
|
||||||
vec![
|
vec![
|
||||||
"telegram".into(),
|
"telegram".into(),
|
||||||
|
|
@ -393,12 +420,16 @@ foo = "bar"
|
||||||
[app_state]
|
[app_state]
|
||||||
english_apps = ["vim", "neovim"]
|
english_apps = ["vim", "neovim"]
|
||||||
vietnamese_apps = ["zalo", "messenger"]
|
vietnamese_apps = ["zalo", "messenger"]
|
||||||
bypass_apps = ["kitty"]
|
bypass_apps = ["steam"]
|
||||||
|
terminal_apps = ["kitty"]
|
||||||
|
terminal_input_method = "telex"
|
||||||
"#;
|
"#;
|
||||||
let config: Config = toml::from_str(toml).unwrap();
|
let config: Config = toml::from_str(toml).unwrap();
|
||||||
assert_eq!(config.app_state.english_apps, vec!["vim", "neovim"]);
|
assert_eq!(config.app_state.english_apps, vec!["vim", "neovim"]);
|
||||||
assert_eq!(config.app_state.vietnamese_apps, vec!["zalo", "messenger"]);
|
assert_eq!(config.app_state.vietnamese_apps, vec!["zalo", "messenger"]);
|
||||||
assert_eq!(config.app_state.bypass_apps, vec!["kitty"]);
|
assert_eq!(config.app_state.bypass_apps, vec!["steam"]);
|
||||||
|
assert_eq!(config.app_state.terminal_apps, vec!["kitty"]);
|
||||||
|
assert_eq!(config.app_state.terminal_input_method, "telex");
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|
@ -420,11 +451,31 @@ bypass_apps = ["kitty"]
|
||||||
#[test]
|
#[test]
|
||||||
fn default_config_bypass_apps() {
|
fn default_config_bypass_apps() {
|
||||||
let config = Config::default();
|
let config = Config::default();
|
||||||
assert!(config.app_state.bypass_apps.contains(&"kitty".to_string()));
|
assert!(config.app_state.bypass_apps.contains(&"steam".to_string()));
|
||||||
assert!(config
|
assert!(!config
|
||||||
.app_state
|
.app_state
|
||||||
.bypass_apps
|
.bypass_apps
|
||||||
.contains(&"alacritty".to_string()));
|
.contains(&"kitty".to_string()));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn default_config_terminal_apps() {
|
||||||
|
let config = Config::default();
|
||||||
|
assert!(config.app_state.terminal_apps.contains(&"kitty".to_string()));
|
||||||
|
assert!(config.app_state.terminal_apps.contains(&"gnome-terminal".to_string()));
|
||||||
|
assert_eq!(config.app_state.terminal_input_method, "vni");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parse_terminal_config() {
|
||||||
|
let toml = r#"
|
||||||
|
[app_state]
|
||||||
|
terminal_apps = ["foot", "alacritty"]
|
||||||
|
terminal_input_method = "telex"
|
||||||
|
"#;
|
||||||
|
let config: Config = toml::from_str(toml).unwrap();
|
||||||
|
assert_eq!(config.app_state.terminal_apps, vec!["foot", "alacritty"]);
|
||||||
|
assert_eq!(config.app_state.terminal_input_method, "telex");
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|
|
||||||
|
|
@ -142,6 +142,9 @@ impl Daemon {
|
||||||
config.app_state.english_apps.clone(),
|
config.app_state.english_apps.clone(),
|
||||||
config.app_state.vietnamese_apps.clone(),
|
config.app_state.vietnamese_apps.clone(),
|
||||||
config.app_state.bypass_apps.clone(),
|
config.app_state.bypass_apps.clone(),
|
||||||
|
config.app_state.terminal_apps.clone(),
|
||||||
|
config.app_state.terminal_input_method.clone(),
|
||||||
|
config.input_method.clone(),
|
||||||
config.start_enabled,
|
config.start_enabled,
|
||||||
);
|
);
|
||||||
app_state.load_overrides();
|
app_state.load_overrides();
|
||||||
|
|
@ -189,17 +192,23 @@ impl Daemon {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn toggle_method(&mut self) {
|
fn toggle_method(&mut self) {
|
||||||
let new_method = match self.config.input_method.as_str() {
|
let new_global = match self.config.input_method.as_str() {
|
||||||
"vni" => InputMethod::Telex,
|
"vni" => "telex",
|
||||||
_ => InputMethod::Vni,
|
_ => "vni",
|
||||||
};
|
};
|
||||||
self.config.input_method = match new_method {
|
self.config.input_method = new_global.into();
|
||||||
InputMethod::Vni => "vni".into(),
|
self.app_state.set_global_method(new_global);
|
||||||
InputMethod::Telex => "telex".into(),
|
let effective = self.app_state.effective_method();
|
||||||
|
let engine_method = match effective {
|
||||||
|
"vni" => InputMethod::Vni,
|
||||||
|
_ => InputMethod::Telex,
|
||||||
};
|
};
|
||||||
self.engine.set_method(new_method);
|
self.engine.set_method(engine_method);
|
||||||
self.write_method_status();
|
self.write_method_status();
|
||||||
log_info(&format!("[vietc] Input method toggled to: {}", self.config.input_method));
|
log_info(&format!(
|
||||||
|
"[vietc] Input method toggled: global={}, effective={}",
|
||||||
|
self.config.input_method, effective
|
||||||
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
fn sync_status_file(&mut self) {
|
fn sync_status_file(&mut self) {
|
||||||
|
|
@ -226,11 +235,6 @@ impl Daemon {
|
||||||
|
|
||||||
match Config::load_from(&self.config_path) {
|
match Config::load_from(&self.config_path) {
|
||||||
Ok(new_config) => {
|
Ok(new_config) => {
|
||||||
let method = match new_config.input_method.as_str() {
|
|
||||||
"vni" => InputMethod::Vni,
|
|
||||||
_ => InputMethod::Telex,
|
|
||||||
};
|
|
||||||
self.engine.set_method(method);
|
|
||||||
self.engine
|
self.engine
|
||||||
.set_auto_restore(new_config.auto_restore.enabled);
|
.set_auto_restore(new_config.auto_restore.enabled);
|
||||||
|
|
||||||
|
|
@ -239,12 +243,23 @@ impl Daemon {
|
||||||
self.engine.add_macro(shortcut.clone(), expansion.clone());
|
self.engine.add_macro(shortcut.clone(), expansion.clone());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
self.app_state.set_global_method(&new_config.input_method);
|
||||||
self.app_state.update_lists(
|
self.app_state.update_lists(
|
||||||
new_config.app_state.english_apps.clone(),
|
new_config.app_state.english_apps.clone(),
|
||||||
new_config.app_state.vietnamese_apps.clone(),
|
new_config.app_state.vietnamese_apps.clone(),
|
||||||
new_config.app_state.bypass_apps.clone(),
|
new_config.app_state.bypass_apps.clone(),
|
||||||
|
new_config.app_state.terminal_apps.clone(),
|
||||||
|
new_config.app_state.terminal_input_method.clone(),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Apply effective method (terminal override considered)
|
||||||
|
let effective = self.app_state.effective_method();
|
||||||
|
let engine_method = match effective {
|
||||||
|
"vni" => InputMethod::Vni,
|
||||||
|
_ => InputMethod::Telex,
|
||||||
|
};
|
||||||
|
self.engine.set_method(engine_method);
|
||||||
|
|
||||||
self.app_state.set_password_config(
|
self.app_state.set_password_config(
|
||||||
new_config.password_detection.enabled,
|
new_config.password_detection.enabled,
|
||||||
new_config.password_detection.check_atspi2,
|
new_config.password_detection.check_atspi2,
|
||||||
|
|
@ -464,6 +479,14 @@ impl Daemon {
|
||||||
self.engine.set_enabled(should_enable);
|
self.engine.set_enabled(should_enable);
|
||||||
self.write_status();
|
self.write_status();
|
||||||
}
|
}
|
||||||
|
// Apply effective method (terminal override)
|
||||||
|
let effective = self.app_state.effective_method();
|
||||||
|
let engine_method = match effective {
|
||||||
|
"vni" => InputMethod::Vni,
|
||||||
|
_ => InputMethod::Telex,
|
||||||
|
};
|
||||||
|
// set_method also resets the engine buffer (safe — window already changed)
|
||||||
|
self.engine.set_method(engine_method);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1238,7 +1261,7 @@ fn run_with_evdev(
|
||||||
if let Some(ch) = key_to_char(key) {
|
if let Some(ch) = key_to_char(key) {
|
||||||
let mut commands = daemon.process_key(ch);
|
let mut commands = daemon.process_key(ch);
|
||||||
if !commands.is_empty()
|
if !commands.is_empty()
|
||||||
&& is_vn_control_key(&daemon.config.input_method, ch)
|
&& is_vn_control_key(daemon.app_state.effective_method(), ch)
|
||||||
{
|
{
|
||||||
for cmd in &mut commands {
|
for cmd in &mut commands {
|
||||||
if let OutputCommand::Backspace(ref mut n) = cmd {
|
if let OutputCommand::Backspace(ref mut n) = cmd {
|
||||||
|
|
@ -1388,7 +1411,7 @@ fn run_with_evdev(
|
||||||
}
|
}
|
||||||
// Skip upcoming auto-repeat pile-up from injection delay
|
// Skip upcoming auto-repeat pile-up from injection delay
|
||||||
skip_count = 3;
|
skip_count = 3;
|
||||||
} else if is_vn_control_key(&daemon.config.input_method, ch)
|
} else if is_vn_control_key(daemon.app_state.effective_method(), ch)
|
||||||
&& daemon.engine.buffer().chars().count() <= buf_before
|
&& daemon.engine.buffer().chars().count() <= buf_before
|
||||||
{
|
{
|
||||||
// Tone/mark key truly absorbed with no effect (no
|
// Tone/mark key truly absorbed with no effect (no
|
||||||
|
|
|
||||||
|
|
@ -109,7 +109,10 @@ password_apps = ["pinentry", "pinentry-gtk-2", "pinentry-qt", "kwallet"]
|
||||||
enabled = true
|
enabled = true
|
||||||
english_apps = ["code", "vim"]
|
english_apps = ["code", "vim"]
|
||||||
vietnamese_apps = ["telegram", "discord", "firefox"]
|
vietnamese_apps = ["telegram", "discord", "firefox"]
|
||||||
bypass_apps = ["kitty", "alacritty", "steam"]
|
bypass_apps = ["steam"]
|
||||||
|
terminal_apps = ["kitty", "alacritty", "gnome-terminal", "konsole", "foot",
|
||||||
|
"wezterm", "st", "urxvt", "xterm"]
|
||||||
|
terminal_input_method = "vni"
|
||||||
EOF
|
EOF
|
||||||
|
|
||||||
echo -e "${GREEN}=== Done! ===${NC}"
|
echo -e "${GREEN}=== Done! ===${NC}"
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue