release: v0.1.7 — password detection, Telex enabled, GNOME Wayland support
This commit is contained in:
parent
d34180537a
commit
6beeee2e69
15 changed files with 845 additions and 101 deletions
36
CHANGELOG.md
36
CHANGELOG.md
|
|
@ -1,6 +1,40 @@
|
||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
## v0.1.6 (2026-06-29)
|
## v0.1.7 (2026-07-01)
|
||||||
|
|
||||||
|
### Password Auto-Detection
|
||||||
|
|
||||||
|
- **AT-SPI2 D-Bus integration**: Queries `org.a11y.atspi.Registry.GetFocus` + `GetRole` to detect password fields (role 62 = `PASSWORD_TEXT`). Automatically disables Vietnamese input when typing into a password field, re-enables when focus moves away.
|
||||||
|
- **Window-class fallback**: Password dialogs (pinentry, polkit, kwallet, ssh-askpass) are detected via `password_apps` config list.
|
||||||
|
- **Window-title fallback**: Window titles containing "password", "passphrase", "sudo", "mật khẩu" trigger automatic English mode.
|
||||||
|
|
||||||
|
### Telex Input Method
|
||||||
|
|
||||||
|
- **Telex now fully enabled**: Both VNI and Telex are supported. Switch via Ctrl+Shift hotkey or tray menu "Input Method > Telex / VNI".
|
||||||
|
- **Method status file** (`~/.config/vietc/method`): Daemon writes the current method so the tray can display it.
|
||||||
|
- **Tray indicator**: Red "VN" for VNI, Blue "TLX" for Telex, Gray "EN" for English mode.
|
||||||
|
- **Config option**: `toggle_method_key = "shift"` configures the Ctrl+Shift method toggle combo.
|
||||||
|
|
||||||
|
### GNOME/Wayland Support
|
||||||
|
|
||||||
|
- **Native GNOME Shell D-Bus integration**: Queries `org.gnome.Shell.Eval` for focused window class, ID, and title — works on Wayland GNOME where xdotool/xprop are unavailable.
|
||||||
|
- **Window detection chain**: GNOME Shell D-Bus → wlrctl → xdotool → /proc — ensures window tracking works across all environments.
|
||||||
|
- **Compositor detection**: Added GNOME/Mutter detection via `pgrep gnome-shell` and `XDG_CURRENT_DESKTOP`.
|
||||||
|
- **Dependencies**: `dbus` crate (0.9) added for both AT-SPI2 and GNOME Shell D-Bus queries.
|
||||||
|
|
||||||
|
### CLI Enhancements
|
||||||
|
|
||||||
|
- **Pass-through characters**: All characters now appear in output (not just those that emit engine events).
|
||||||
|
- **Screen display**: Backspace characters are properly applied to show what would appear on screen.
|
||||||
|
- **State reset**: Each input line starts with a clean engine state.
|
||||||
|
- **New commands**: `:help`, `:status`, `:vi`, `:en`, `:ar on|off`, `:macros`, `:macro add/rm/clear`, `:events`, `:events clear`.
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
- **Engine state correctly reset between input lines** in CLI test harness.
|
||||||
|
- **Flush characters forwarded** after macro expansion / auto-restore replacement to preserve spacing.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
### uinput-First Injection
|
### uinput-First Injection
|
||||||
|
|
||||||
|
|
|
||||||
59
README.md
59
README.md
|
|
@ -2,8 +2,8 @@
|
||||||
<img src="https://img.shields.io/badge/Platform-Linux-blue?style=for-the-badge" alt="Platform">
|
<img src="https://img.shields.io/badge/Platform-Linux-blue?style=for-the-badge" alt="Platform">
|
||||||
<img src="https://img.shields.io/badge/Language-Rust-orange?style=for-the-badge" alt="Rust">
|
<img src="https://img.shields.io/badge/Language-Rust-orange?style=for-the-badge" alt="Rust">
|
||||||
<img src="https://img.shields.io/badge/License-MIT-green?style=for-the-badge" alt="License">
|
<img src="https://img.shields.io/badge/License-MIT-green?style=for-the-badge" alt="License">
|
||||||
<img src="https://img.shields.io/badge/Version-0.1.6-purple?style=for-the-badge" alt="Version">
|
<img src="https://img.shields.io/badge/Version-0.1.7-purple?style=for-the-badge" alt="Version">
|
||||||
<img src="https://img.shields.io/badge/Tests-106_passing-brightgreen?style=for-the-badge" alt="Tests">
|
<img src="https://img.shields.io/badge/Tests-118_passing-brightgreen?style=for-the-badge" alt="Tests">
|
||||||
<img src="https://img.shields.io/badge/Event_Sourcing-✓-blueviolet?style=for-the-badge" alt="Event Sourcing">
|
<img src="https://img.shields.io/badge/Event_Sourcing-✓-blueviolet?style=for-the-badge" alt="Event Sourcing">
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
|
|
@ -61,9 +61,11 @@ Physical Keyboard
|
||||||
│ │
|
│ │
|
||||||
│ Modifier keys (Ctrl/Alt/Super) → forward directly │
|
│ Modifier keys (Ctrl/Alt/Super) → forward directly │
|
||||||
│ Ctrl+Space → toggle Vietnamese ON/OFF │
|
│ Ctrl+Space → toggle Vietnamese ON/OFF │
|
||||||
|
│ Ctrl+Shift → toggle VNI/Telex input method │
|
||||||
|
│ Password detected → auto-disable Vietnamese │
|
||||||
│ Backspace → replay_backspace() │
|
│ Backspace → replay_backspace() │
|
||||||
│ Characters → replay_and_inject(ch) │
|
│ Characters → replay_and_inject(ch) │
|
||||||
│ VNI control keys → consume when no match │
|
│ VNI/Telex control keys → consume when no match │
|
||||||
└──────────────────────────────────────────────────────────────┘
|
└──────────────────────────────────────────────────────────────┘
|
||||||
│
|
│
|
||||||
▼
|
▼
|
||||||
|
|
@ -149,14 +151,15 @@ vietc/
|
||||||
├── daemon/ # Main daemon process
|
├── daemon/ # Main daemon process
|
||||||
│ ├── main.rs # Event loops, Backspace-Replay, CPU pinning
|
│ ├── main.rs # Event loops, Backspace-Replay, CPU pinning
|
||||||
│ ├── config.rs # TOML config loader + hot reload
|
│ ├── config.rs # TOML config loader + hot reload
|
||||||
│ ├── app_state.rs # Per-app Vietnamese/English memory
|
│ ├── app_state.rs # Per-app VN/EN memory + password detection
|
||||||
|
│ ├── password_detector.rs # AT-SPI2 D-Bus password field detection
|
||||||
│ └── display.rs # X11/Wayland/compositor detection
|
│ └── display.rs # X11/Wayland/compositor detection
|
||||||
│
|
│
|
||||||
├── uinputd/ # Privileged uinput backspace daemon (VMK-style)
|
├── uinputd/ # Privileged uinput backspace daemon (VMK-style)
|
||||||
│ └── main.rs # Unix socket server for /dev/uinput injection
|
│ └── main.rs # Unix socket server for /dev/uinput injection
|
||||||
│
|
│
|
||||||
├── ui/ # System tray icon
|
├── ui/ # System tray icon
|
||||||
│ └── main.rs # Tray + daemon launcher
|
│ └── tray.rs # Tray with VN/TLX/EN mode display
|
||||||
│
|
│
|
||||||
├── cli/ # Interactive test harness
|
├── cli/ # Interactive test harness
|
||||||
├── packaging/ # .deb packaging scripts
|
├── packaging/ # .deb packaging scripts
|
||||||
|
|
@ -214,7 +217,12 @@ vietc/
|
||||||
|
|
||||||
## Input Methods
|
## Input Methods
|
||||||
|
|
||||||
### VNI (default, Telex coming in next version)
|
Both **VNI** and **Telex** are fully supported. Switch between them via:
|
||||||
|
- **Ctrl+Shift** hotkey (toggle at runtime)
|
||||||
|
- **System tray** menu: "Input Method > Telex / VNI"
|
||||||
|
- **Config file**: `input_method = "vni"` or `"telex"`
|
||||||
|
|
||||||
|
### VNI
|
||||||
|
|
||||||
| Key | Result | Example |
|
| Key | Result | Example |
|
||||||
|-----|--------|---------|
|
|-----|--------|---------|
|
||||||
|
|
@ -228,7 +236,25 @@ vietc/
|
||||||
| `8` | ă | `a8` → `ă` |
|
| `8` | ă | `a8` → `ă` |
|
||||||
| `9` | đ | `d9` → `đ` |
|
| `9` | đ | `d9` → `đ` |
|
||||||
|
|
||||||
Flexible typing: type the full syllable, then add marks/tone keys at the end. Example: `nguye6n4` → `nguyễn`. The engine scans backward up to 5 characters to find the target vowel.
|
### Telex
|
||||||
|
|
||||||
|
| Key | Result | Example |
|
||||||
|
|-----|--------|---------|
|
||||||
|
| `s` | á (sắc) | `as` → `á` |
|
||||||
|
| `f` | à (huyền) | `af` → `à` |
|
||||||
|
| `r` | ả (hỏi) | `ar` → `ả` |
|
||||||
|
| `x` | ã (ngã) | `ax` → `ã` |
|
||||||
|
| `j` | ạ (nặng) | `aj` → `ạ` |
|
||||||
|
| `aa` | â | `aa` → `â` |
|
||||||
|
| `ee` | ê | `ee` → `ê` |
|
||||||
|
| `oo` | ô | `oo` → `ô` |
|
||||||
|
| `ow` | ơ | `ow` → `ơ` |
|
||||||
|
| `aw` | ă | `aw` → `ă` |
|
||||||
|
| `uw` | ư | `uw` → `ư` |
|
||||||
|
| `dd` | đ | `dd` → `đ` |
|
||||||
|
| `w` | ươ (uo cluster) | `chuongw` → `chương` |
|
||||||
|
|
||||||
|
Flexible typing: type the full syllable, then add marks/tone keys at the end. Examples: `tieengs` → `tiếng`, `nguyeexn` → `nguyễn`, `chafo` → `chào`. The engine scans backward up to 5 characters to find the target vowel.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
@ -248,6 +274,10 @@ Flexible typing: type the full syllable, then add marks/tone keys at the end. Ex
|
||||||
| **Window-Switch Reset** | Active window ID verified on every keystroke — Alt+Tab instantly clears engine state. No stale composition across apps |
|
| **Window-Switch Reset** | Active window ID verified on every keystroke — Alt+Tab instantly clears engine state. No stale composition across apps |
|
||||||
| **CPU Priority** | Pins daemon to P-cores (0-3) + nice(-10) for low-latency input |
|
| **CPU Priority** | Pins daemon to P-cores (0-3) + nice(-10) for low-latency input |
|
||||||
| **Uinput Injection** | Uses `/dev/uinput` for reliable keyboard injection without X11 dependency. Falls back to XTest on systems without uinput access |
|
| **Uinput Injection** | Uses `/dev/uinput` for reliable keyboard injection without X11 dependency. Falls back to XTest on systems without uinput access |
|
||||||
|
| **Password Auto-Detection** | AT-SPI2 + window-class + window-title — automatically disables Vietnamese when typing into password fields |
|
||||||
|
| **Method Toggle** | Ctrl+Shift switches between VNI and Telex at runtime; tray icon shows current mode (VN/TLX/EN) |
|
||||||
|
| **GNOME/Wayland Support** | Native GNOME Shell D-Bus integration for window detection, app memory, and password detection on Wayland |
|
||||||
|
| **VNI & Telex** | Both input methods fully supported, switchable at runtime |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
@ -287,7 +317,7 @@ System tray icon + daemon + desktop entry. Requires user to be in the `input` gr
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Install
|
# Install
|
||||||
sudo dpkg -i vietc_0.1.6-1_amd64.deb
|
sudo dpkg -i vietc_0.1.7-1_amd64.deb
|
||||||
|
|
||||||
# Log out and log back in (for input group membership to take effect)
|
# Log out and log back in (for input group membership to take effect)
|
||||||
# Then launch "Viet+" from your application menu
|
# Then launch "Viet+" from your application menu
|
||||||
|
|
@ -318,7 +348,8 @@ Config file: `~/.config/vietc/config.toml` or `./vietc.toml`
|
||||||
|
|
||||||
```toml
|
```toml
|
||||||
input_method = "vni" # "vni" or "telex"
|
input_method = "vni" # "vni" or "telex"
|
||||||
toggle_key = "space" # Ctrl+Space to toggle
|
toggle_key = "space" # Ctrl+Space to toggle VN/EN
|
||||||
|
toggle_method_key = "shift" # Ctrl+Shift to toggle VNI/Telex
|
||||||
start_enabled = true # Vietnamese by default
|
start_enabled = true # Vietnamese by default
|
||||||
grab = true # grab keyboard (evdev)
|
grab = true # grab keyboard (evdev)
|
||||||
|
|
||||||
|
|
@ -326,6 +357,16 @@ grab = true # grab keyboard (evdev)
|
||||||
enabled = true
|
enabled = true
|
||||||
trigger_keys = ["space", "escape"]
|
trigger_keys = ["space", "escape"]
|
||||||
|
|
||||||
|
[password_detection]
|
||||||
|
enabled = true
|
||||||
|
check_atspi2 = true # AT-SPI2 accessibility bus detection
|
||||||
|
check_window_title = true
|
||||||
|
title_keywords = ["password", "passphrase", "secret", "mật khẩu", "sudo"]
|
||||||
|
password_apps = ["pinentry", "pinentry-gtk-2", "pinentry-qt",
|
||||||
|
"lxqt-sudo", "kdesudo", "gksudo",
|
||||||
|
"polkit-gnome-authentication-agent-1",
|
||||||
|
"kwallet", "gnome-keyring", "ssh-askpass"]
|
||||||
|
|
||||||
[app_state]
|
[app_state]
|
||||||
enabled = true
|
enabled = true
|
||||||
english_apps = ["code", "vim", "kitty", "foot"]
|
english_apps = ["code", "vim", "kitty", "foot"]
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
[package]
|
[package]
|
||||||
name = "vietc-cli"
|
name = "vietc-cli"
|
||||||
version = "0.1.6"
|
version = "0.1.7"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
description = "Viet+ CLI Test Harness"
|
description = "Viet+ CLI Test Harness"
|
||||||
|
|
||||||
|
|
|
||||||
320
cli/src/main.rs
320
cli/src/main.rs
|
|
@ -1,16 +1,47 @@
|
||||||
// SPDX-License-Identifier: MIT
|
|
||||||
use std::io::{self, Write};
|
use std::io::{self, Write};
|
||||||
use vietc_engine::{Engine, EngineEvent, InputMethod};
|
use vietc_engine::{Engine, EngineEvent, EventStore, InputEvent, InputMethod};
|
||||||
|
|
||||||
|
struct CliState {
|
||||||
|
engine: Engine,
|
||||||
|
method: InputMethod,
|
||||||
|
events: EventStore,
|
||||||
|
macros: Vec<(String, String)>,
|
||||||
|
auto_restore: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl CliState {
|
||||||
|
fn new() -> Self {
|
||||||
|
Self {
|
||||||
|
engine: Engine::new(InputMethod::Telex),
|
||||||
|
method: InputMethod::Telex,
|
||||||
|
events: EventStore::new(),
|
||||||
|
macros: Vec::new(),
|
||||||
|
auto_restore: true,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn set_method(&mut self, method: InputMethod) {
|
||||||
|
self.method = method;
|
||||||
|
self.engine.set_method(method);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn status(&self) {
|
||||||
|
println!(" Method: {:?}", self.method);
|
||||||
|
println!(" Enabled: {}", self.engine.is_enabled());
|
||||||
|
println!(" Auto-restore: {}", self.auto_restore);
|
||||||
|
println!(" Buffer: {:?}", self.engine.buffer());
|
||||||
|
println!(" Macros: {} defined", self.macros.len());
|
||||||
|
for (s, e) in &self.macros {
|
||||||
|
println!(" {} -> {}", s, e);
|
||||||
|
}
|
||||||
|
println!(" Events: {} recorded", self.events.len());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn main() {
|
fn main() {
|
||||||
let mut engine = Engine::new(InputMethod::Telex);
|
let mut state = CliState::new();
|
||||||
|
|
||||||
println!("Viet+ IME - Test Harness");
|
print_help();
|
||||||
println!("==========================");
|
|
||||||
println!("Type Vietnamese using Telex input.");
|
|
||||||
println!("Press Enter to flush, type 'quit' to exit.");
|
|
||||||
println!("Toggle method with ':vni' or ':telex'");
|
|
||||||
println!();
|
|
||||||
|
|
||||||
loop {
|
loop {
|
||||||
print!("> ");
|
print!("> ");
|
||||||
|
|
@ -20,82 +51,69 @@ fn main() {
|
||||||
io::stdin().read_line(&mut input).unwrap();
|
io::stdin().read_line(&mut input).unwrap();
|
||||||
let input = input.trim();
|
let input = input.trim();
|
||||||
|
|
||||||
|
if input.is_empty() {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
if input == "quit" || input == "exit" {
|
if input == "quit" || input == "exit" {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
if input == ":vni" {
|
if input.starts_with(':') {
|
||||||
engine.set_method(InputMethod::Vni);
|
handle_command(&mut state, input);
|
||||||
println!("[Switched to VNI]");
|
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
if input == ":telex" {
|
state.engine.reset();
|
||||||
engine.set_method(InputMethod::Telex);
|
|
||||||
println!("[Switched to Telex]");
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if input == ":reset" {
|
|
||||||
engine.reset();
|
|
||||||
println!("[Engine reset]");
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if input == ":buffer" {
|
|
||||||
println!("[Buffer: {:?}]", engine.buffer());
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut output = String::new();
|
let mut output = String::new();
|
||||||
let mut events = Vec::new();
|
let mut events = Vec::new();
|
||||||
|
|
||||||
for ch in input.chars() {
|
for ch in input.chars() {
|
||||||
if let Some(event) = engine.process_key(ch) {
|
state.events.push(InputEvent::KeyTyped(ch));
|
||||||
|
|
||||||
|
match state.engine.process_key(ch) {
|
||||||
|
None => {
|
||||||
|
output.push(ch);
|
||||||
|
}
|
||||||
|
Some(event) => {
|
||||||
events.push((ch, event.clone()));
|
events.push((ch, event.clone()));
|
||||||
match &event {
|
match &event {
|
||||||
EngineEvent::Flush(text) => {
|
EngineEvent::Insert(text) | EngineEvent::Flush(text) => {
|
||||||
output.push_str(text);
|
output.push_str(text);
|
||||||
}
|
}
|
||||||
EngineEvent::Insert(text) => {
|
EngineEvent::Paste(text) => {
|
||||||
output.push_str(text);
|
output.push_str(text);
|
||||||
}
|
}
|
||||||
EngineEvent::AutoRestore(word) => {
|
|
||||||
// Auto-restore: delete the word and re-insert it
|
|
||||||
for _ in 0..word.len() {
|
|
||||||
output.push('\x08'); // backspace
|
|
||||||
}
|
|
||||||
output.push_str(word);
|
|
||||||
}
|
|
||||||
EngineEvent::Replace { backspaces, insert } => {
|
EngineEvent::Replace { backspaces, insert } => {
|
||||||
for _ in 0..*backspaces {
|
for _ in 0..*backspaces {
|
||||||
output.push('\x08');
|
output.push('\x08');
|
||||||
}
|
}
|
||||||
output.push_str(insert);
|
output.push_str(insert);
|
||||||
|
if is_flush_char(ch) {
|
||||||
|
output.push(ch);
|
||||||
}
|
}
|
||||||
EngineEvent::UndoTones {
|
}
|
||||||
backspaces,
|
EngineEvent::UndoTones { backspaces, restored } => {
|
||||||
restored,
|
|
||||||
} => {
|
|
||||||
for _ in 0..*backspaces {
|
for _ in 0..*backspaces {
|
||||||
output.push('\x08');
|
output.push('\x08');
|
||||||
}
|
}
|
||||||
output.push_str(restored);
|
output.push_str(restored);
|
||||||
}
|
}
|
||||||
EngineEvent::Paste(text) => {
|
EngineEvent::AutoRestore(word) => {
|
||||||
output.push_str(text);
|
for _ in 0..word.len() {
|
||||||
|
output.push('\x08');
|
||||||
|
}
|
||||||
|
output.push_str(word);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Flush remaining buffer
|
if let Some(event) = state.engine.flush() {
|
||||||
if let Some(event) = engine.flush() {
|
|
||||||
match &event {
|
match &event {
|
||||||
EngineEvent::Flush(text) => {
|
EngineEvent::Flush(text) | EngineEvent::Insert(text) => {
|
||||||
output.push_str(text);
|
|
||||||
}
|
|
||||||
EngineEvent::Insert(text) => {
|
|
||||||
output.push_str(text);
|
output.push_str(text);
|
||||||
}
|
}
|
||||||
_ => {}
|
_ => {}
|
||||||
|
|
@ -104,10 +122,204 @@ fn main() {
|
||||||
}
|
}
|
||||||
|
|
||||||
println!(" Events: {:?}", events);
|
println!(" Events: {:?}", events);
|
||||||
println!(" Output: {:?}", output);
|
println!(" Raw: {:?}", output);
|
||||||
|
|
||||||
// Show what it would look like
|
let display = apply_backspaces(&output);
|
||||||
let display: String = output.chars().filter(|c| *c != '\x08').collect();
|
println!(" Screen: {}", display);
|
||||||
println!(" Display: {}", display);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn print_help() {
|
||||||
|
println!("Viet+ IME - Test Harness");
|
||||||
|
println!("=========================");
|
||||||
|
println!("Type text with VNI/Telex to see engine output.");
|
||||||
|
println!();
|
||||||
|
println!("Commands:");
|
||||||
|
println!(" :help Show this help");
|
||||||
|
println!(" :status Show engine state");
|
||||||
|
println!(" :vi Enable Vietnamese mode");
|
||||||
|
println!(" :en Disable Vietnamese mode");
|
||||||
|
println!(" :ar on|off Toggle auto-restore");
|
||||||
|
println!(" :vni Switch to VNI input");
|
||||||
|
println!(" :telex Switch to Telex input");
|
||||||
|
println!(" :reset Reset engine buffer");
|
||||||
|
println!(" :buffer Show composing buffer");
|
||||||
|
println!(" :events Show event store history");
|
||||||
|
println!(" :events clear Clear event store");
|
||||||
|
println!(" :macros List macros");
|
||||||
|
println!(" :macro add <s> <e> Add macro shortcut->expansion");
|
||||||
|
println!(" :macro rm <s> Remove a macro");
|
||||||
|
println!(" :macro clear Clear all macros");
|
||||||
|
println!(" quit/exit Quit");
|
||||||
|
println!();
|
||||||
|
}
|
||||||
|
|
||||||
|
fn handle_command(state: &mut CliState, input: &str) {
|
||||||
|
let parts: Vec<&str> = input.splitn(4, ' ').collect();
|
||||||
|
let cmd = parts[0];
|
||||||
|
|
||||||
|
match cmd {
|
||||||
|
":help" | ":h" => print_help(),
|
||||||
|
|
||||||
|
":status" | ":st" => state.status(),
|
||||||
|
|
||||||
|
":vi" => {
|
||||||
|
state.engine.set_enabled(true);
|
||||||
|
println!("[Vietnamese mode ON]");
|
||||||
|
}
|
||||||
|
|
||||||
|
":en" => {
|
||||||
|
state.engine.set_enabled(false);
|
||||||
|
println!("[Vietnamese mode OFF]");
|
||||||
|
}
|
||||||
|
|
||||||
|
":ar" => {
|
||||||
|
if parts.len() < 2 {
|
||||||
|
println!("[Usage: :ar on|off]");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
match parts[1] {
|
||||||
|
"on" => {
|
||||||
|
state.auto_restore = true;
|
||||||
|
state.engine.set_auto_restore(true);
|
||||||
|
println!("[Auto-restore ON]");
|
||||||
|
}
|
||||||
|
"off" => {
|
||||||
|
state.auto_restore = false;
|
||||||
|
state.engine.set_auto_restore(false);
|
||||||
|
println!("[Auto-restore OFF]");
|
||||||
|
}
|
||||||
|
_ => println!("[Usage: :ar on|off]"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
":vni" => {
|
||||||
|
state.set_method(InputMethod::Vni);
|
||||||
|
println!("[Switched to VNI]");
|
||||||
|
}
|
||||||
|
|
||||||
|
":telex" => {
|
||||||
|
state.set_method(InputMethod::Telex);
|
||||||
|
println!("[Switched to Telex]");
|
||||||
|
}
|
||||||
|
|
||||||
|
":reset" => {
|
||||||
|
state.engine.reset();
|
||||||
|
println!("[Engine reset]");
|
||||||
|
}
|
||||||
|
|
||||||
|
":buffer" => {
|
||||||
|
println!("[Buffer: {:?}]", state.engine.buffer());
|
||||||
|
}
|
||||||
|
|
||||||
|
":events" | ":ev" => {
|
||||||
|
if parts.len() > 1 && parts[1] == "clear" {
|
||||||
|
state.events.clear();
|
||||||
|
println!("[Event store cleared]");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if state.events.is_empty() {
|
||||||
|
println!("[No events]");
|
||||||
|
} else {
|
||||||
|
println!("[Events: {}]", state.events.len());
|
||||||
|
for (i, event) in state.events.iter().enumerate() {
|
||||||
|
println!(" {}: {:?}", i, event);
|
||||||
|
}
|
||||||
|
println!(" Raw keystrokes: {:?}", state.events.raw_keystrokes());
|
||||||
|
println!(" Pattern hash: {}", state.events.pattern_hash());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
":macros" => {
|
||||||
|
if state.macros.is_empty() {
|
||||||
|
println!("[No macros defined]");
|
||||||
|
} else {
|
||||||
|
println!("[Macros: {}]", state.macros.len());
|
||||||
|
for (s, e) in &state.macros {
|
||||||
|
println!(" {} -> {}", s, e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
":macro" => {
|
||||||
|
if parts.len() < 2 {
|
||||||
|
println!("[Usage: :macro add <shortcut> <expansion> or :macro rm <s> or :macro clear]");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
match parts[1] {
|
||||||
|
"add" | "a" => {
|
||||||
|
if parts.len() < 4 {
|
||||||
|
println!("[Usage: :macro add <shortcut> <expansion>]");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let shortcut = parts[2].to_string();
|
||||||
|
let expansion = parts[3].to_string();
|
||||||
|
state.engine.add_macro(shortcut.clone(), expansion.clone());
|
||||||
|
if let Some(pos) = state.macros.iter().position(|(s, _)| *s == shortcut) {
|
||||||
|
state.macros[pos].1 = expansion.clone();
|
||||||
|
} else {
|
||||||
|
state.macros.push((shortcut.clone(), expansion.clone()));
|
||||||
|
}
|
||||||
|
println!("[Macro added: {} -> {}]", shortcut, expansion);
|
||||||
|
}
|
||||||
|
"rm" | "remove" | "del" => {
|
||||||
|
if parts.len() < 3 {
|
||||||
|
println!("[Usage: :macro rm <shortcut>]");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let shortcut = parts[2];
|
||||||
|
if let Some(pos) = state.macros.iter().position(|(s, _)| s == shortcut) {
|
||||||
|
state.macros.remove(pos);
|
||||||
|
state.engine.clear_macros();
|
||||||
|
for (s, e) in &state.macros {
|
||||||
|
state.engine.add_macro(s.clone(), e.clone());
|
||||||
|
}
|
||||||
|
println!("[Macro removed: {}]", shortcut);
|
||||||
|
} else {
|
||||||
|
println!("[Macro not found: {}]", shortcut);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"clear" | "c" => {
|
||||||
|
state.engine.clear_macros();
|
||||||
|
state.macros.clear();
|
||||||
|
println!("[All macros cleared]");
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
let shortcut = parts[1].to_string();
|
||||||
|
let expansion = parts.get(2).map(|s| s.to_string()).unwrap_or_default();
|
||||||
|
if expansion.is_empty() {
|
||||||
|
println!("[Usage: :macro add <shortcut> <expansion>]");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
state.engine.add_macro(shortcut.clone(), expansion.clone());
|
||||||
|
if let Some(pos) = state.macros.iter().position(|(s, _)| *s == shortcut) {
|
||||||
|
state.macros[pos].1 = expansion.clone();
|
||||||
|
} else {
|
||||||
|
state.macros.push((shortcut.clone(), expansion.clone()));
|
||||||
|
}
|
||||||
|
println!("[Macro added: {} -> {}]", shortcut, expansion);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_ => {
|
||||||
|
println!("[Unknown command: {}. Type :help for available commands]", cmd);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn is_flush_char(ch: char) -> bool {
|
||||||
|
matches!(ch, ' ' | '\t' | '.' | ',' | '!' | '?' | ';' | ':' | '\n')
|
||||||
|
}
|
||||||
|
|
||||||
|
fn apply_backspaces(s: &str) -> String {
|
||||||
|
let mut result = String::new();
|
||||||
|
for ch in s.chars() {
|
||||||
|
if ch == '\x08' {
|
||||||
|
result.pop();
|
||||||
|
} else {
|
||||||
|
result.push(ch);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
result
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
[package]
|
[package]
|
||||||
name = "vietc-daemon"
|
name = "vietc-daemon"
|
||||||
version = "0.1.6"
|
version = "0.1.7"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
description = "Viet+ background daemon"
|
description = "Viet+ background daemon"
|
||||||
|
|
||||||
|
|
@ -21,3 +21,4 @@ serde = { version = "1", features = ["derive"] }
|
||||||
evdev = "0.12"
|
evdev = "0.12"
|
||||||
libc = "0.2"
|
libc = "0.2"
|
||||||
dirs = "5"
|
dirs = "5"
|
||||||
|
dbus = "0.9"
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,8 @@ use std::collections::HashMap;
|
||||||
use std::fs;
|
use std::fs;
|
||||||
use std::process::Command;
|
use std::process::Command;
|
||||||
|
|
||||||
|
use crate::password_detector::PasswordDetector;
|
||||||
|
|
||||||
/// Query _NET_ACTIVE_WINDOW directly via X11 client library (dlopen).
|
/// Query _NET_ACTIVE_WINDOW directly via X11 client library (dlopen).
|
||||||
/// Works inside the Flatpak sandbox where xdotool/xprop are unavailable
|
/// Works inside the Flatpak sandbox where xdotool/xprop are unavailable
|
||||||
/// but libX11.so.6 is present in the GNOME runtime. No external process
|
/// but libX11.so.6 is present in the GNOME runtime. No external process
|
||||||
|
|
@ -111,10 +113,44 @@ fn get_active_window_x11_dlopen() -> Option<String> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Get the active window's title (lowercase)
|
||||||
|
pub fn get_active_window_title() -> Option<String> {
|
||||||
|
// Try GNOME Shell D-Bus (Wayland GNOME)
|
||||||
|
if let Some(title) = get_gnome_window_title() {
|
||||||
|
return Some(title.to_lowercase());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try X11 via xdotool
|
||||||
|
if let Ok(output) = Command::new("xdotool")
|
||||||
|
.args(["getactivewindow", "getwindowname"])
|
||||||
|
.output()
|
||||||
|
{
|
||||||
|
if output.status.success() {
|
||||||
|
let title = String::from_utf8_lossy(&output.stdout).trim().to_string();
|
||||||
|
if !title.is_empty() {
|
||||||
|
return Some(title.to_lowercase());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Query GNOME Shell via D-Bus for the focused window's title
|
||||||
|
fn get_gnome_window_title() -> Option<String> {
|
||||||
|
let js = "global.display.focus_window?.get_title() ?? ''";
|
||||||
|
let (_, title) = gnome_shell_eval(js)?;
|
||||||
|
if title.is_empty() { None } else { Some(title) }
|
||||||
|
}
|
||||||
|
|
||||||
/// Get the active window's X11 ID (unique per window — even within the same
|
/// Get the active window's X11 ID (unique per window — even within the same
|
||||||
/// application). Returns a unique window-identifier string.
|
/// application). Returns a unique window-identifier string.
|
||||||
pub fn get_active_window_id() -> Option<String> {
|
pub fn get_active_window_id() -> Option<String> {
|
||||||
// Try xdotool first (fast, direct)
|
// Try GNOME Shell D-Bus (Wayland GNOME) — returns hex window ID
|
||||||
|
if let Some(id) = get_gnome_active_window_id() {
|
||||||
|
return Some(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try xdotool first (fast, direct, X11)
|
||||||
if let Ok(output) = Command::new("xdotool")
|
if let Ok(output) = Command::new("xdotool")
|
||||||
.args(["getactivewindow"])
|
.args(["getactivewindow"])
|
||||||
.output()
|
.output()
|
||||||
|
|
@ -152,9 +188,21 @@ pub fn get_active_window_id() -> Option<String> {
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Query GNOME Shell via D-Bus for the focused window's XID
|
||||||
|
fn get_gnome_active_window_id() -> Option<String> {
|
||||||
|
let js = "global.display.focus_window?.get_id()?.toString(16) ?? ''";
|
||||||
|
let (_, id) = gnome_shell_eval(js)?;
|
||||||
|
if id.is_empty() { None } else { Some(format!("0x{}", id)) }
|
||||||
|
}
|
||||||
|
|
||||||
/// Detect the currently focused window's class name
|
/// Detect the currently focused window's class name
|
||||||
pub fn get_focused_window_class() -> Option<String> {
|
pub fn get_focused_window_class() -> Option<String> {
|
||||||
// Try Wayland first (wlr-foreign-toplevel)
|
// Try GNOME Shell D-Bus (Wayland GNOME)
|
||||||
|
if let Some(class) = get_gnome_focused_wm_class() {
|
||||||
|
return Some(class);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try Wayland via wlrctl (wlroots compositors)
|
||||||
if let Some(class) = get_wayland_window_class() {
|
if let Some(class) = get_wayland_window_class() {
|
||||||
return Some(class);
|
return Some(class);
|
||||||
}
|
}
|
||||||
|
|
@ -172,6 +220,29 @@ pub fn get_focused_window_class() -> Option<String> {
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Query GNOME Shell via D-Bus for the focused window's WM class (app ID)
|
||||||
|
fn get_gnome_focused_wm_class() -> Option<String> {
|
||||||
|
let js = "global.display.focus_window?.get_wm_class() ?? ''";
|
||||||
|
let (_, result) = gnome_shell_eval(js)?;
|
||||||
|
if result.is_empty() { None } else { Some(result.to_lowercase()) }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Execute JavaScript in GNOME Shell and return (success, output)
|
||||||
|
fn gnome_shell_eval(js: &str) -> Option<(bool, String)> {
|
||||||
|
use std::time::Duration;
|
||||||
|
let conn = dbus::blocking::Connection::new_session().ok()?;
|
||||||
|
let proxy = dbus::blocking::Proxy::new(
|
||||||
|
"org.gnome.Shell",
|
||||||
|
"/org/gnome/Shell",
|
||||||
|
Duration::from_secs(1),
|
||||||
|
&conn,
|
||||||
|
);
|
||||||
|
let (success, output): (bool, String) = proxy
|
||||||
|
.method_call("org.gnome.Shell", "Eval", (js,))
|
||||||
|
.ok()?;
|
||||||
|
Some((success, output))
|
||||||
|
}
|
||||||
|
|
||||||
fn get_x11_window_class() -> Option<String> {
|
fn get_x11_window_class() -> Option<String> {
|
||||||
let output = Command::new("xdotool")
|
let output = Command::new("xdotool")
|
||||||
.args(["getactivewindow", "getwindowclassname"])
|
.args(["getactivewindow", "getwindowclassname"])
|
||||||
|
|
@ -235,6 +306,16 @@ pub struct AppStateManager {
|
||||||
bypass_apps: Vec<String>,
|
bypass_apps: Vec<String>,
|
||||||
/// Global enabled state
|
/// Global enabled state
|
||||||
global_enabled: bool,
|
global_enabled: bool,
|
||||||
|
/// Password detection config
|
||||||
|
password_enabled: bool,
|
||||||
|
check_atspi2: bool,
|
||||||
|
check_window_title: bool,
|
||||||
|
title_keywords: Vec<String>,
|
||||||
|
password_apps: Vec<String>,
|
||||||
|
/// Password detector (AT-SPI2)
|
||||||
|
password_detector: PasswordDetector,
|
||||||
|
/// Cached password field state
|
||||||
|
is_password_field: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl AppStateManager {
|
impl AppStateManager {
|
||||||
|
|
@ -251,9 +332,82 @@ impl AppStateManager {
|
||||||
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(),
|
||||||
global_enabled,
|
global_enabled,
|
||||||
|
password_enabled: false,
|
||||||
|
check_atspi2: true,
|
||||||
|
check_window_title: true,
|
||||||
|
title_keywords: Vec::new(),
|
||||||
|
password_apps: Vec::new(),
|
||||||
|
password_detector: PasswordDetector::new(),
|
||||||
|
is_password_field: false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Update password detection config
|
||||||
|
pub fn set_password_config(
|
||||||
|
&mut self,
|
||||||
|
enabled: bool,
|
||||||
|
check_atspi2: bool,
|
||||||
|
check_window_title: bool,
|
||||||
|
title_keywords: Vec<String>,
|
||||||
|
password_apps: Vec<String>,
|
||||||
|
) {
|
||||||
|
self.password_enabled = enabled;
|
||||||
|
self.check_atspi2 = check_atspi2;
|
||||||
|
self.check_window_title = check_window_title;
|
||||||
|
self.title_keywords = title_keywords.iter().map(|s| s.to_lowercase()).collect();
|
||||||
|
self.password_apps = password_apps.iter().map(|s| s.to_lowercase()).collect();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Check if the current focused widget is a password field
|
||||||
|
/// Returns true if password detected, forcing English mode
|
||||||
|
pub fn check_password_field(&mut self) -> bool {
|
||||||
|
if !self.password_enabled {
|
||||||
|
self.is_password_field = false;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Layer 1: AT-SPI2 (most accurate, works in terminals and dialogs)
|
||||||
|
if self.check_atspi2 {
|
||||||
|
if let Some(is_password) = self.password_detector.check() {
|
||||||
|
self.is_password_field = is_password;
|
||||||
|
if is_password {
|
||||||
|
log_password_detection("AT-SPI2", &self.current_app);
|
||||||
|
}
|
||||||
|
return is_password;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Layer 2: Window class match (for known password dialogs)
|
||||||
|
for pattern in &self.password_apps {
|
||||||
|
if self.current_app.contains(pattern.as_str()) {
|
||||||
|
self.is_password_field = true;
|
||||||
|
log_password_detection("window-class", &self.current_app);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Layer 3: Window title heuristic (for sudo prompts, browser dialogs)
|
||||||
|
if self.check_window_title {
|
||||||
|
if let Some(title) = get_active_window_title() {
|
||||||
|
for keyword in &self.title_keywords {
|
||||||
|
if title.contains(keyword.as_str()) {
|
||||||
|
self.is_password_field = true;
|
||||||
|
log_password_detection("window-title", &title);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
self.is_password_field = false;
|
||||||
|
false
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Is the current widget a password field? (cached)
|
||||||
|
pub fn is_password_field(&self) -> bool {
|
||||||
|
self.is_password_field
|
||||||
|
}
|
||||||
|
|
||||||
/// Check if focused app changed with a pre-detected class and return whether engine should be enabled
|
/// Check if focused app changed with a pre-detected class and return whether engine should be enabled
|
||||||
pub fn update_with_app(&mut self, new_class: String) -> Option<bool> {
|
pub fn update_with_app(&mut self, new_class: String) -> Option<bool> {
|
||||||
if new_class == self.current_app {
|
if new_class == self.current_app {
|
||||||
|
|
@ -270,7 +424,7 @@ impl AppStateManager {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get the default Vietnamese state for the current app
|
/// Get the default Vietnamese state for the current app
|
||||||
fn get_default_state(&self) -> bool {
|
pub fn get_default_state(&self) -> bool {
|
||||||
if !self.global_enabled {
|
if !self.global_enabled {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
@ -378,6 +532,10 @@ impl AppStateManager {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn log_password_detection(method: &str, context: &str) {
|
||||||
|
eprintln!("[vietc] Password field detected via {}: {}", method, context);
|
||||||
|
}
|
||||||
|
|
||||||
fn override_path() -> std::path::PathBuf {
|
fn override_path() -> std::path::PathBuf {
|
||||||
std::env::var("XDG_CONFIG_HOME")
|
std::env::var("XDG_CONFIG_HOME")
|
||||||
.ok()
|
.ok()
|
||||||
|
|
|
||||||
|
|
@ -14,12 +14,18 @@ pub struct Config {
|
||||||
#[serde(default = "default_toggle_key")]
|
#[serde(default = "default_toggle_key")]
|
||||||
pub toggle_key: String,
|
pub toggle_key: String,
|
||||||
|
|
||||||
|
#[serde(default = "default_toggle_method_key")]
|
||||||
|
pub toggle_method_key: String,
|
||||||
|
|
||||||
#[serde(default = "default_start_enabled")]
|
#[serde(default = "default_start_enabled")]
|
||||||
pub start_enabled: bool,
|
pub start_enabled: bool,
|
||||||
|
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub auto_restore: AutoRestoreConfig,
|
pub auto_restore: AutoRestoreConfig,
|
||||||
|
|
||||||
|
#[serde(default)]
|
||||||
|
pub password_detection: PasswordDetectionConfig,
|
||||||
|
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub app_state: AppStateConfig,
|
pub app_state: AppStateConfig,
|
||||||
|
|
||||||
|
|
@ -33,6 +39,37 @@ pub struct Config {
|
||||||
pub debug: bool,
|
pub debug: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Deserialize)]
|
||||||
|
#[allow(dead_code)]
|
||||||
|
pub struct PasswordDetectionConfig {
|
||||||
|
#[serde(default = "default_true")]
|
||||||
|
pub enabled: bool,
|
||||||
|
|
||||||
|
#[serde(default = "default_true")]
|
||||||
|
pub check_atspi2: bool,
|
||||||
|
|
||||||
|
#[serde(default = "default_true")]
|
||||||
|
pub check_window_title: bool,
|
||||||
|
|
||||||
|
#[serde(default = "default_title_keywords")]
|
||||||
|
pub title_keywords: Vec<String>,
|
||||||
|
|
||||||
|
#[serde(default = "default_password_apps")]
|
||||||
|
pub password_apps: Vec<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for PasswordDetectionConfig {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
enabled: true,
|
||||||
|
check_atspi2: true,
|
||||||
|
check_window_title: true,
|
||||||
|
title_keywords: default_title_keywords(),
|
||||||
|
password_apps: default_password_apps(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug, Deserialize)]
|
#[derive(Debug, Deserialize)]
|
||||||
#[allow(dead_code)]
|
#[allow(dead_code)]
|
||||||
pub struct AutoRestoreConfig {
|
pub struct AutoRestoreConfig {
|
||||||
|
|
@ -85,6 +122,9 @@ fn default_input_method() -> String {
|
||||||
fn default_toggle_key() -> String {
|
fn default_toggle_key() -> String {
|
||||||
"space".into()
|
"space".into()
|
||||||
}
|
}
|
||||||
|
fn default_toggle_method_key() -> String {
|
||||||
|
"shift".into()
|
||||||
|
}
|
||||||
fn default_start_enabled() -> bool {
|
fn default_start_enabled() -> bool {
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
|
|
@ -97,6 +137,30 @@ fn default_false() -> bool {
|
||||||
fn default_restore_keys() -> Vec<String> {
|
fn default_restore_keys() -> Vec<String> {
|
||||||
vec!["space".into(), "escape".into()]
|
vec!["space".into(), "escape".into()]
|
||||||
}
|
}
|
||||||
|
fn default_title_keywords() -> Vec<String> {
|
||||||
|
vec![
|
||||||
|
"password".into(),
|
||||||
|
"passphrase".into(),
|
||||||
|
"secret".into(),
|
||||||
|
"mật khẩu".into(),
|
||||||
|
"mk".into(),
|
||||||
|
"sudo".into(),
|
||||||
|
]
|
||||||
|
}
|
||||||
|
fn default_password_apps() -> Vec<String> {
|
||||||
|
vec![
|
||||||
|
"pinentry".into(),
|
||||||
|
"pinentry-gtk-2".into(),
|
||||||
|
"pinentry-qt".into(),
|
||||||
|
"lxqt-sudo".into(),
|
||||||
|
"kdesudo".into(),
|
||||||
|
"gksudo".into(),
|
||||||
|
"polkit-gnome-authentication-agent-1".into(),
|
||||||
|
"kwallet".into(),
|
||||||
|
"gnome-keyring".into(),
|
||||||
|
"ssh-askpass".into(),
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
fn default_english_apps() -> Vec<String> {
|
fn default_english_apps() -> Vec<String> {
|
||||||
vec![
|
vec![
|
||||||
|
|
@ -192,8 +256,10 @@ impl Default for Config {
|
||||||
Self {
|
Self {
|
||||||
input_method: default_input_method(),
|
input_method: default_input_method(),
|
||||||
toggle_key: default_toggle_key(),
|
toggle_key: default_toggle_key(),
|
||||||
|
toggle_method_key: default_toggle_method_key(),
|
||||||
start_enabled: default_start_enabled(),
|
start_enabled: default_start_enabled(),
|
||||||
auto_restore: AutoRestoreConfig::default(),
|
auto_restore: AutoRestoreConfig::default(),
|
||||||
|
password_detection: PasswordDetectionConfig::default(),
|
||||||
app_state: AppStateConfig::default(),
|
app_state: AppStateConfig::default(),
|
||||||
macros,
|
macros,
|
||||||
grab: false, // default false so daemon works without root (needs input group for uinput)
|
grab: false, // default false so daemon works without root (needs input group for uinput)
|
||||||
|
|
@ -401,4 +467,30 @@ unknown_field = "value"
|
||||||
let config: Config = toml::from_str(toml).unwrap();
|
let config: Config = toml::from_str(toml).unwrap();
|
||||||
assert_eq!(config.input_method, "telex");
|
assert_eq!(config.input_method, "telex");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parse_password_detection() {
|
||||||
|
let toml = r#"
|
||||||
|
[password_detection]
|
||||||
|
enabled = true
|
||||||
|
check_atspi2 = true
|
||||||
|
check_window_title = true
|
||||||
|
title_keywords = ["password", "passphrase"]
|
||||||
|
password_apps = ["pinentry", "kwallet"]
|
||||||
|
"#;
|
||||||
|
let config: Config = toml::from_str(toml).unwrap();
|
||||||
|
assert!(config.password_detection.enabled);
|
||||||
|
assert!(config.password_detection.check_atspi2);
|
||||||
|
assert_eq!(config.password_detection.title_keywords, vec!["password", "passphrase"]);
|
||||||
|
assert_eq!(config.password_detection.password_apps, vec!["pinentry", "kwallet"]);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parse_toggle_method_key() {
|
||||||
|
let toml = r#"
|
||||||
|
toggle_method_key = "shift"
|
||||||
|
"#;
|
||||||
|
let config: Config = toml::from_str(toml).unwrap();
|
||||||
|
assert_eq!(config.toggle_method_key, "shift");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -85,5 +85,19 @@ pub fn detect_compositor() -> Option<String> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Check for GNOME/Mutter (Ubuntu default)
|
||||||
|
if let Ok(output) = Command::new("pgrep").arg("-x").arg("gnome-shell").output() {
|
||||||
|
if output.status.success() {
|
||||||
|
return Some("GNOME (Mutter)".into());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check XDG_CURRENT_DESKTOP for GNOME
|
||||||
|
if let Ok(desktop) = std::env::var("XDG_CURRENT_DESKTOP") {
|
||||||
|
if desktop.to_lowercase().contains("gnome") {
|
||||||
|
return Some("GNOME".into());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -37,10 +37,12 @@ fn boost_thread_priority() {
|
||||||
mod app_state;
|
mod app_state;
|
||||||
mod config;
|
mod config;
|
||||||
mod display;
|
mod display;
|
||||||
|
mod password_detector;
|
||||||
|
|
||||||
use app_state::AppStateManager;
|
use app_state::AppStateManager;
|
||||||
use config::Config;
|
use config::Config;
|
||||||
|
|
||||||
|
|
||||||
#[cfg(feature = "x11")]
|
#[cfg(feature = "x11")]
|
||||||
use vietc_protocol::x11_capture::X11Capture;
|
use vietc_protocol::x11_capture::X11Capture;
|
||||||
use vietc_protocol::x11_capture::SKIP_RECORD_EVENTS;
|
use vietc_protocol::x11_capture::SKIP_RECORD_EVENTS;
|
||||||
|
|
@ -143,6 +145,13 @@ impl Daemon {
|
||||||
config.start_enabled,
|
config.start_enabled,
|
||||||
);
|
);
|
||||||
app_state.load_overrides();
|
app_state.load_overrides();
|
||||||
|
app_state.set_password_config(
|
||||||
|
config.password_detection.enabled,
|
||||||
|
config.password_detection.check_atspi2,
|
||||||
|
config.password_detection.check_window_title,
|
||||||
|
config.password_detection.title_keywords.clone(),
|
||||||
|
config.password_detection.password_apps.clone(),
|
||||||
|
);
|
||||||
|
|
||||||
let config_modified = fs::metadata(&config_path)
|
let config_modified = fs::metadata(&config_path)
|
||||||
.and_then(|m| m.modified())
|
.and_then(|m| m.modified())
|
||||||
|
|
@ -171,6 +180,28 @@ impl Daemon {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn write_method_status(&self) {
|
||||||
|
if let Some(parent) = self.config_path.parent() {
|
||||||
|
let method_path = parent.join("method");
|
||||||
|
let method = &self.config.input_method;
|
||||||
|
let _ = std::fs::write(method_path, method);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn toggle_method(&mut self) {
|
||||||
|
let new_method = match self.config.input_method.as_str() {
|
||||||
|
"vni" => InputMethod::Telex,
|
||||||
|
_ => InputMethod::Vni,
|
||||||
|
};
|
||||||
|
self.config.input_method = match new_method {
|
||||||
|
InputMethod::Vni => "vni".into(),
|
||||||
|
InputMethod::Telex => "telex".into(),
|
||||||
|
};
|
||||||
|
self.engine.set_method(new_method);
|
||||||
|
self.write_method_status();
|
||||||
|
log_info(&format!("[vietc] Input method toggled to: {}", self.config.input_method));
|
||||||
|
}
|
||||||
|
|
||||||
fn sync_status_file(&mut self) {
|
fn sync_status_file(&mut self) {
|
||||||
if let Some(parent) = self.config_path.parent() {
|
if let Some(parent) = self.config_path.parent() {
|
||||||
let status_path = parent.join("status");
|
let status_path = parent.join("status");
|
||||||
|
|
@ -214,6 +245,14 @@ impl Daemon {
|
||||||
new_config.app_state.bypass_apps.clone(),
|
new_config.app_state.bypass_apps.clone(),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
self.app_state.set_password_config(
|
||||||
|
new_config.password_detection.enabled,
|
||||||
|
new_config.password_detection.check_atspi2,
|
||||||
|
new_config.password_detection.check_window_title,
|
||||||
|
new_config.password_detection.title_keywords.clone(),
|
||||||
|
new_config.password_detection.password_apps.clone(),
|
||||||
|
);
|
||||||
|
|
||||||
self.grab_enabled = new_config.grab;
|
self.grab_enabled = new_config.grab;
|
||||||
self.config = new_config;
|
self.config = new_config;
|
||||||
self.config_modified = modified;
|
self.config_modified = modified;
|
||||||
|
|
@ -996,6 +1035,31 @@ fn run_with_evdev(
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Ctrl+LeftShift: toggle VNI/Telex input method
|
||||||
|
if value == 1 && is_method_toggle_state(&key_state)
|
||||||
|
{
|
||||||
|
daemon.toggle_method();
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Password field check: disable engine if typing into a password field
|
||||||
|
if value == 1 {
|
||||||
|
let is_pw = daemon.app_state.is_password_field();
|
||||||
|
let currently_enabled = daemon.engine.is_enabled();
|
||||||
|
if is_pw && currently_enabled {
|
||||||
|
daemon.engine.set_enabled(false);
|
||||||
|
daemon.write_status();
|
||||||
|
log_info("[vietc] Password field detected — engine disabled");
|
||||||
|
} else if !is_pw && !currently_enabled && daemon.config.start_enabled {
|
||||||
|
// Only re-enable if we're not in a manual toggle state
|
||||||
|
let default_state = daemon.app_state.get_default_state();
|
||||||
|
if default_state {
|
||||||
|
daemon.engine.set_enabled(true);
|
||||||
|
daemon.write_status();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if !grabbed {
|
if !grabbed {
|
||||||
// Legacy mode: only forward to engine on press events
|
// Legacy mode: only forward to engine on press events
|
||||||
if value != 1 {
|
if value != 1 {
|
||||||
|
|
@ -1074,6 +1138,15 @@ fn run_with_evdev(
|
||||||
};
|
};
|
||||||
daemon.check_app_change_with(class);
|
daemon.check_app_change_with(class);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Re-check password field status on window change
|
||||||
|
if daemon.config.password_detection.enabled {
|
||||||
|
let is_pw = daemon.app_state.check_password_field();
|
||||||
|
if is_pw && daemon.engine.is_enabled() {
|
||||||
|
daemon.engine.set_enabled(false);
|
||||||
|
daemon.write_status();
|
||||||
|
}
|
||||||
|
}
|
||||||
} else if daemon.config.app_state.enabled {
|
} else if daemon.config.app_state.enabled {
|
||||||
let class = shared_window_class.lock().unwrap().clone();
|
let class = shared_window_class.lock().unwrap().clone();
|
||||||
if !class.is_empty() {
|
if !class.is_empty() {
|
||||||
|
|
@ -1351,6 +1424,19 @@ fn is_caps_lock_on(device: &evdev::Device) -> bool {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn is_method_toggle_state(key_state: &evdev::AttributeSet<evdev::Key>) -> bool {
|
||||||
|
let ctrl_pressed = key_state.contains(evdev::Key::KEY_LEFTCTRL)
|
||||||
|
|| key_state.contains(evdev::Key::KEY_RIGHTCTRL);
|
||||||
|
let shift_pressed = key_state.contains(evdev::Key::KEY_LEFTSHIFT);
|
||||||
|
// Require Ctrl + LeftShift specifically, no other modifiers
|
||||||
|
ctrl_pressed && shift_pressed
|
||||||
|
&& !key_state.contains(evdev::Key::KEY_RIGHTSHIFT)
|
||||||
|
&& !key_state.contains(evdev::Key::KEY_LEFTALT)
|
||||||
|
&& !key_state.contains(evdev::Key::KEY_RIGHTALT)
|
||||||
|
&& !key_state.contains(evdev::Key::KEY_LEFTMETA)
|
||||||
|
&& !key_state.contains(evdev::Key::KEY_RIGHTMETA)
|
||||||
|
}
|
||||||
|
|
||||||
fn is_toggle_combination_state(key_state: &evdev::AttributeSet<evdev::Key>, key: &str) -> bool {
|
fn is_toggle_combination_state(key_state: &evdev::AttributeSet<evdev::Key>, key: &str) -> bool {
|
||||||
let ctrl_pressed = key_state.contains(evdev::Key::KEY_LEFTCTRL)
|
let ctrl_pressed = key_state.contains(evdev::Key::KEY_LEFTCTRL)
|
||||||
|| key_state.contains(evdev::Key::KEY_RIGHTCTRL);
|
|| key_state.contains(evdev::Key::KEY_RIGHTCTRL);
|
||||||
|
|
|
||||||
58
daemon/src/password_detector.rs
Normal file
58
daemon/src/password_detector.rs
Normal file
|
|
@ -0,0 +1,58 @@
|
||||||
|
use std::collections::HashMap;
|
||||||
|
use std::time::Duration;
|
||||||
|
|
||||||
|
use dbus::arg::{RefArg, Variant};
|
||||||
|
use dbus::blocking::Connection;
|
||||||
|
|
||||||
|
const ROLE_PASSWORD_TEXT: i32 = 62;
|
||||||
|
|
||||||
|
pub struct PasswordDetector {
|
||||||
|
cached: Option<bool>,
|
||||||
|
atspi_ok: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PasswordDetector {
|
||||||
|
pub fn new() -> Self {
|
||||||
|
Self { cached: None, atspi_ok: false }
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn check(&mut self) -> Option<bool> {
|
||||||
|
let r = self.check_atspi2();
|
||||||
|
self.atspi_ok = r.is_some();
|
||||||
|
if let Some(v) = r {
|
||||||
|
self.cached = Some(v);
|
||||||
|
}
|
||||||
|
r
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn is_available(&self) -> bool {
|
||||||
|
self.atspi_ok
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn cached_result(&self) -> Option<bool> {
|
||||||
|
self.cached
|
||||||
|
}
|
||||||
|
|
||||||
|
fn check_atspi2(&self) -> Option<bool> {
|
||||||
|
let conn = Connection::new_session().ok()?;
|
||||||
|
let timeout = Duration::from_secs(2);
|
||||||
|
|
||||||
|
let proxy = conn.with_proxy(
|
||||||
|
"org.a11y.atspi.Registry",
|
||||||
|
"/org/a11y/atspi/registry",
|
||||||
|
timeout,
|
||||||
|
);
|
||||||
|
|
||||||
|
let (bus_name, props, _children): (String, HashMap<String, Variant<Box<dyn RefArg>>>, Vec<Variant<Box<dyn RefArg>>>) =
|
||||||
|
proxy.method_call("org.a11y.atspi.Registry", "GetFocus", ()).ok()?;
|
||||||
|
|
||||||
|
let path_variant = props.get("path")?;
|
||||||
|
let path_str = path_variant.0.as_str()?;
|
||||||
|
|
||||||
|
let acc_proxy = conn.with_proxy(&bus_name, path_str, timeout);
|
||||||
|
|
||||||
|
let (role,): (i32,) = acc_proxy.method_call("org.a11y.atspi.Accessible", "GetRole", ()).ok()?;
|
||||||
|
|
||||||
|
Some(role == ROLE_PASSWORD_TEXT)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
[package]
|
[package]
|
||||||
name = "vietc-engine"
|
name = "vietc-engine"
|
||||||
version = "0.1.6"
|
version = "0.1.7"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
description = "Viet+ Vietnamese IME Core Engine"
|
description = "Viet+ Vietnamese IME Core Engine"
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
[package]
|
[package]
|
||||||
name = "vietc-protocol"
|
name = "vietc-protocol"
|
||||||
version = "0.1.6"
|
version = "0.1.7"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
description = "Viet+ keystroke injection backends (X11/Wayland)"
|
description = "Viet+ keystroke injection backends (X11/Wayland)"
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
[package]
|
[package]
|
||||||
name = "vietc-tray"
|
name = "vietc-tray"
|
||||||
version = "0.1.6"
|
version = "0.1.7"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
description = "Viet+ system tray icon"
|
description = "Viet+ system tray icon"
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -12,6 +12,23 @@ fn write_status(state: &str) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn read_method() -> String {
|
||||||
|
let path = dirs::config_dir()
|
||||||
|
.map(|d| d.join("vietc").join("method"))
|
||||||
|
.unwrap_or_else(|| std::path::PathBuf::from("/tmp/vietc-method"));
|
||||||
|
std::fs::read_to_string(&path)
|
||||||
|
.map(|s| s.trim().to_string())
|
||||||
|
.unwrap_or_else(|_| {
|
||||||
|
config::Config::load().input_method
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn write_method(method: &str) {
|
||||||
|
if let Some(config_dir) = dirs::config_dir() {
|
||||||
|
let _ = std::fs::write(config_dir.join("vietc").join("method"), method);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn read_status() -> String {
|
fn read_status() -> String {
|
||||||
let path = dirs::config_dir()
|
let path = dirs::config_dir()
|
||||||
.map(|d| d.join("vietc").join("status"))
|
.map(|d| d.join("vietc").join("status"))
|
||||||
|
|
@ -68,6 +85,11 @@ fn ensure_icons() {
|
||||||
<text x="64" y="96" text-anchor="middle" fill="#ffffff" font-size="48" font-weight="bold" font-family="system-ui, sans-serif">VN</text>
|
<text x="64" y="96" text-anchor="middle" fill="#ffffff" font-size="48" font-weight="bold" font-family="system-ui, sans-serif">VN</text>
|
||||||
</svg>"##;
|
</svg>"##;
|
||||||
|
|
||||||
|
let svg_tlx = r##"<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 128 128">
|
||||||
|
<rect x="8" y="8" width="112" height="112" rx="24" fill="#2563eb"/>
|
||||||
|
<text x="64" y="96" text-anchor="middle" fill="#ffffff" font-size="48" font-weight="bold" font-family="system-ui, sans-serif">TLX</text>
|
||||||
|
</svg>"##;
|
||||||
|
|
||||||
let svg_en = r##"<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 128 128">
|
let svg_en = r##"<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 128 128">
|
||||||
<rect x="8" y="8" width="112" height="112" rx="24" fill="#4b5563"/>
|
<rect x="8" y="8" width="112" height="112" rx="24" fill="#4b5563"/>
|
||||||
<text x="64" y="96" text-anchor="middle" fill="#ffffff" font-size="48" font-weight="bold" font-family="system-ui, sans-serif">EN</text>
|
<text x="64" y="96" text-anchor="middle" fill="#ffffff" font-size="48" font-weight="bold" font-family="system-ui, sans-serif">EN</text>
|
||||||
|
|
@ -78,11 +100,15 @@ fn ensure_icons() {
|
||||||
if let Some(home_icons) = &home {
|
if let Some(home_icons) = &home {
|
||||||
let _ = std::fs::create_dir_all(&home_icons);
|
let _ = std::fs::create_dir_all(&home_icons);
|
||||||
let vn_path = home_icons.join("vietc-vn.svg");
|
let vn_path = home_icons.join("vietc-vn.svg");
|
||||||
|
let tlx_path = home_icons.join("vietc-tlx.svg");
|
||||||
let en_path = home_icons.join("vietc-en.svg");
|
let en_path = home_icons.join("vietc-en.svg");
|
||||||
|
|
||||||
if !vn_path.exists() {
|
if !vn_path.exists() {
|
||||||
let _ = std::fs::write(&vn_path, svg_vn);
|
let _ = std::fs::write(&vn_path, svg_vn);
|
||||||
}
|
}
|
||||||
|
if !tlx_path.exists() {
|
||||||
|
let _ = std::fs::write(&tlx_path, svg_tlx);
|
||||||
|
}
|
||||||
if !en_path.exists() {
|
if !en_path.exists() {
|
||||||
let _ = std::fs::write(&en_path, svg_en);
|
let _ = std::fs::write(&en_path, svg_en);
|
||||||
}
|
}
|
||||||
|
|
@ -95,11 +121,15 @@ fn ensure_icons() {
|
||||||
let _ = std::fs::create_dir_all(&icons_dir);
|
let _ = std::fs::create_dir_all(&icons_dir);
|
||||||
|
|
||||||
let vn_theme = icons_dir.join("hicolor/scalable/apps/vietc-vn.svg");
|
let vn_theme = icons_dir.join("hicolor/scalable/apps/vietc-vn.svg");
|
||||||
|
let tlx_theme = icons_dir.join("hicolor/scalable/apps/vietc-tlx.svg");
|
||||||
let en_theme = icons_dir.join("hicolor/scalable/apps/vietc-en.svg");
|
let en_theme = icons_dir.join("hicolor/scalable/apps/vietc-en.svg");
|
||||||
|
|
||||||
if !vn_theme.exists() {
|
if !vn_theme.exists() {
|
||||||
let _ = std::fs::write(&vn_theme, svg_vn);
|
let _ = std::fs::write(&vn_theme, svg_vn);
|
||||||
}
|
}
|
||||||
|
if !tlx_theme.exists() {
|
||||||
|
let _ = std::fs::write(&tlx_theme, svg_tlx);
|
||||||
|
}
|
||||||
if !en_theme.exists() {
|
if !en_theme.exists() {
|
||||||
let _ = std::fs::write(&en_theme, svg_en);
|
let _ = std::fs::write(&en_theme, svg_en);
|
||||||
}
|
}
|
||||||
|
|
@ -247,12 +277,17 @@ impl Tray for VietTray {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn icon_name(&self) -> String {
|
fn icon_name(&self) -> String {
|
||||||
|
let is_tlx = self.mode == "vn" && self.im == "telex";
|
||||||
if is_flatpak() {
|
if is_flatpak() {
|
||||||
if self.mode == "vn" {
|
if is_tlx {
|
||||||
|
"io.github.vietc.VietPlus.vietc-tlx".into()
|
||||||
|
} else if self.mode == "vn" {
|
||||||
"io.github.vietc.VietPlus.vietc-vn".into()
|
"io.github.vietc.VietPlus.vietc-vn".into()
|
||||||
} else {
|
} else {
|
||||||
"io.github.vietc.VietPlus.vietc-en".into()
|
"io.github.vietc.VietPlus.vietc-en".into()
|
||||||
}
|
}
|
||||||
|
} else if is_tlx {
|
||||||
|
"vietc-tlx".into()
|
||||||
} else if self.mode == "vn" {
|
} else if self.mode == "vn" {
|
||||||
"vietc-vn".into()
|
"vietc-vn".into()
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -280,10 +315,13 @@ impl Tray for VietTray {
|
||||||
|
|
||||||
fn icon_pixmap(&self) -> Vec<ksni::Icon> {
|
fn icon_pixmap(&self) -> Vec<ksni::Icon> {
|
||||||
let is_vn = self.mode == "vn";
|
let is_vn = self.mode == "vn";
|
||||||
let bg_color = if is_vn {
|
let is_tlx = self.mode == "vn" && self.im == "telex";
|
||||||
[255, 224, 36, 36] // A, R, G, B
|
let bg_color = if is_vn && !is_tlx {
|
||||||
|
[255, 224, 36, 36] // Red for VNI
|
||||||
|
} else if is_tlx {
|
||||||
|
[255, 37, 99, 235] // Blue for Telex
|
||||||
} else {
|
} else {
|
||||||
[255, 75, 85, 99]
|
[255, 75, 85, 99] // Gray for English
|
||||||
};
|
};
|
||||||
let fg_color = [255, 255, 255, 255];
|
let fg_color = [255, 255, 255, 255];
|
||||||
|
|
||||||
|
|
@ -319,7 +357,18 @@ impl Tray for VietTray {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if is_vn {
|
if is_tlx {
|
||||||
|
// T
|
||||||
|
draw_line(&mut data, 6, 10, 15, 10, fg_color);
|
||||||
|
draw_line(&mut data, 6, 11, 15, 11, fg_color);
|
||||||
|
draw_line(&mut data, 10, 10, 10, 21, fg_color);
|
||||||
|
draw_line(&mut data, 11, 10, 11, 21, fg_color);
|
||||||
|
// X
|
||||||
|
draw_line(&mut data, 18, 10, 26, 21, fg_color);
|
||||||
|
draw_line(&mut data, 19, 10, 27, 21, fg_color);
|
||||||
|
draw_line(&mut data, 26, 10, 18, 21, fg_color);
|
||||||
|
draw_line(&mut data, 27, 10, 19, 21, fg_color);
|
||||||
|
} else if is_vn {
|
||||||
// V
|
// V
|
||||||
draw_line(&mut data, 6, 10, 11, 21, fg_color);
|
draw_line(&mut data, 6, 10, 11, 21, fg_color);
|
||||||
draw_line(&mut data, 7, 10, 12, 21, fg_color);
|
draw_line(&mut data, 7, 10, 12, 21, fg_color);
|
||||||
|
|
@ -369,7 +418,7 @@ impl Tray for VietTray {
|
||||||
|
|
||||||
fn menu(&self) -> Vec<MenuItem<Self>> {
|
fn menu(&self) -> Vec<MenuItem<Self>> {
|
||||||
let is_vn = self.mode == "vn";
|
let is_vn = self.mode == "vn";
|
||||||
let im_index = if self.im == "telex" { 0 } else { 1 };
|
let im_index = if self.im == "telex" { 0_usize } else { 1_usize };
|
||||||
|
|
||||||
let mut items = vec![
|
let mut items = vec![
|
||||||
CheckmarkItem {
|
CheckmarkItem {
|
||||||
|
|
@ -409,13 +458,12 @@ impl Tray for VietTray {
|
||||||
let mut cfg = config::Config::load();
|
let mut cfg = config::Config::load();
|
||||||
cfg.input_method = im.into();
|
cfg.input_method = im.into();
|
||||||
let _ = cfg.save();
|
let _ = cfg.save();
|
||||||
|
write_method(im);
|
||||||
this.im = im.into();
|
this.im = im.into();
|
||||||
}),
|
}),
|
||||||
options: vec![
|
options: vec![
|
||||||
RadioItem {
|
RadioItem {
|
||||||
label: "Telex (next version)".into(),
|
label: "Telex".into(),
|
||||||
enabled: false,
|
|
||||||
disposition: Disposition::Informative,
|
|
||||||
..Default::default()
|
..Default::default()
|
||||||
},
|
},
|
||||||
RadioItem {
|
RadioItem {
|
||||||
|
|
@ -542,7 +590,7 @@ pub fn run() {
|
||||||
loop {
|
loop {
|
||||||
std::thread::sleep(std::time::Duration::from_millis(100));
|
std::thread::sleep(std::time::Duration::from_millis(100));
|
||||||
let mode = read_status();
|
let mode = read_status();
|
||||||
let im = current_im();
|
let im = read_method();
|
||||||
let autostart = config::is_autostart_installed();
|
let autostart = config::is_autostart_installed();
|
||||||
// Also check status_changed flag for immediate updates
|
// Also check status_changed flag for immediate updates
|
||||||
let _ = handle.update(move |t| {
|
let _ = handle.update(move |t| {
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
[package]
|
[package]
|
||||||
name = "vietc-uinputd"
|
name = "vietc-uinputd"
|
||||||
version = "0.1.6"
|
version = "0.1.7"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
description = "Viet+ privileged uinput backspace injection daemon"
|
description = "Viet+ privileged uinput backspace injection daemon"
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue