Full Code of huytd/goxkey for AI

main d833a3b3a764 cached
36 files
279.2 KB
70.4k tokens
524 symbols
1 requests
Download .txt
Showing preview only (294K chars total). Download the full file or copy to clipboard to get everything.
Repository: huytd/goxkey
Branch: main
Commit: d833a3b3a764
Files: 36
Total size: 279.2 KB

Directory structure:
gitextract_0uwjvva_/

├── .editorconfig
├── .github/
│   ├── FUNDING.yml
│   └── workflows/
│       ├── main.yml
│       ├── pr.yml
│       └── update-cask.yml
├── .gitignore
├── CLAUDE.md
├── Cargo.toml
├── Casks/
│   └── goxkey.rb
├── DEVELOPMENT.md
├── LICENSE
├── Makefile
├── NIGHTLY_RELEASE.md
├── README.md
├── icons/
│   └── icon.icns
├── scripts/
│   ├── pre-commit
│   └── release
└── src/
    ├── config.rs
    ├── hotkey.rs
    ├── input.rs
    ├── main.rs
    ├── platform/
    │   ├── linux.rs
    │   ├── macos.rs
    │   ├── macos_ext.rs
    │   ├── mod.rs
    │   └── windows.rs
    ├── scripting/
    │   ├── mod.rs
    │   └── parser.rs
    └── ui/
        ├── colors.rs
        ├── controllers.rs
        ├── data.rs
        ├── locale.rs
        ├── mod.rs
        ├── selectors.rs
        ├── views.rs
        └── widgets.rs

================================================
FILE CONTENTS
================================================

================================================
FILE: .editorconfig
================================================
[*.rs]
indent_style = space
indent_size = 4
tab_width = 4

================================================
FILE: .github/FUNDING.yml
================================================
# These are supported funding model platforms

github: huytd
ko_fi: thefullsnack


================================================
FILE: .github/workflows/main.yml
================================================
on:
  push:
    branches:
      - 'main'

name: Stable

jobs:
  test:
    name: Test project
    runs-on: macos-11 # add other OS later
    steps:
      - uses: actions/checkout@v4
      - uses: dtolnay/rust-toolchain@stable
      - run: cargo test cargo-bundle
  build:
    name: Build project
    permissions: write-all
    runs-on: macos-latest # add other OS later
    steps:
      - uses: actions/checkout@v4
      - uses: dtolnay/rust-toolchain@stable
        with:
          targets: aarch64-apple-darwin
      - run: cargo install cargo-bundle
      - run: cargo bundle --release
      - name: Upload artifact
        uses: actions/upload-artifact@v4
        with:
          name: GoKey.app
          path: target/release/bundle/osx/GoKey.app
          retention-days: 2
      - name: Release nightly
        env:
          GH_TOKEN: ${{ github.token }}
        run: |
          cd target/release/bundle/osx
          zip -r GoKey.zip GoKey.app
          gh release delete-asset nightly-build GoKey.zip
          gh release upload nightly-build GoKey.zip


================================================
FILE: .github/workflows/pr.yml
================================================
on:
  push:
    branches-ignore:
      - 'main'

name: Pull request

jobs:
  test:
    name: Test project
    runs-on: macos-11 # add other OS later
    steps:
      - uses: actions/checkout@v4
      - uses: dtolnay/rust-toolchain@stable
      - run: cargo test cargo-bundle
  build:
    name: Build project
    runs-on: macos-11 # add other OS later
    steps:
      - uses: actions/checkout@v4
      - uses: dtolnay/rust-toolchain@stable
      - run: cargo install cargo-bundle
      - run: cargo bundle --release


================================================
FILE: .github/workflows/update-cask.yml
================================================
on:
  release:
    types: [published]

name: Update Homebrew Cask

jobs:
  update-cask:
    name: Update Casks/goxkey.rb
    runs-on: ubuntu-latest
    permissions:
      contents: write
    steps:
      - uses: actions/checkout@v4

      - name: Compute SHA256 of release asset
        id: sha
        env:
          GH_TOKEN: ${{ github.token }}
          TAG: ${{ github.event.release.tag_name }}
        run: |
          VERSION="${TAG#v}"
          URL="https://github.com/huytd/goxkey/releases/download/${TAG}/GoKey-v${VERSION}.zip"
          SHA=$(curl -sL "$URL" | shasum -a 256 | awk '{print $1}')
          echo "version=$VERSION" >> "$GITHUB_OUTPUT"
          echo "sha256=$SHA" >> "$GITHUB_OUTPUT"

      - name: Update cask version and sha256
        env:
          VERSION: ${{ steps.sha.outputs.version }}
          SHA256: ${{ steps.sha.outputs.sha256 }}
        run: |
          sed -i "s/version \".*\"/version \"$VERSION\"/" Casks/goxkey.rb
          sed -i "s/sha256 \".*\"/sha256 \"$SHA256\"/" Casks/goxkey.rb

      - name: Commit and push
        env:
          VERSION: ${{ steps.sha.outputs.version }}
        run: |
          git config user.name "github-actions[bot]"
          git config user.email "github-actions[bot]@users.noreply.github.com"
          git add Casks/goxkey.rb
          git commit -m "chore: bump cask to v$VERSION"
          git push


================================================
FILE: .gitignore
================================================
/target
.DS_Store
.idea

================================================
FILE: CLAUDE.md
================================================
## Project Overview

Gõkey is a Vietnamese input method editor (IME) for macOS, written in Rust. It
intercepts keyboard events via `CGEventTap`, accumulates typed characters into a
buffer, delegates transformation to the
[`vi-rs`](https://github.com/zerox-dg/vi-rs) crate, then replaces the typed
characters using the backspace technique.

## Commands

```sh
make setup    # Install git hooks (run once after cloning)
make run      # cargo r
make bundle   # cargo bundle (creates .app bundle, requires cargo-bundle)
cargo test    # Run all tests
cargo test <test_name>  # Run a single test by name
```

**Requirements:** `cargo-bundle` must be installed
(`cargo install cargo-bundle`). The app requires macOS Accessibility permission
granted before first run.

## Architecture

### Data Flow

```
macOS CGEventTap → event_handler() in main.rs → INPUT_STATE (global InputState)
    → vi-rs (transformation engine) → send_backspace + send_string → target app
```

App change events feed into `InputState` for auto-toggling Vietnamese per-app.

### Key Modules

- **`src/main.rs`** — Entry point. Sets up the Druid UI window, spawns the
  keyboard event listener thread, and contains `event_handler()` which is the
  core dispatch function for every keystroke.
- **`src/input.rs`** — `InputState`: the central state machine. Manages the
  typing buffer, calls `vi-rs` for transformation, handles macro expansion, word
  restoration (reverting invalid transformations), and app-specific auto-toggle.
- **`src/platform/macos.rs`** — All macOS-specific code: `CGEventTap` setup,
  synthetic key event generation, accessibility permission checks, active app
  detection, system tray.
- **`src/platform/mod.rs`** — Platform abstraction: `PressedKey`, `KeyModifier`
  bitflags, `EventTapType`, and the
  `send_string`/`send_backspace`/`run_event_listener` interface.
- **`src/config.rs`** — `ConfigStore`: reads/writes `~/.goxkey` in a simple
  key-value format. Stores hotkey, input method, macros, VN/EN app lists, and
  allowed words.
- **`src/hotkey.rs`** — Parses hotkey strings (e.g., `"super+shift+z"`) and
  matches them against current key + modifiers.
- **`src/ui/`** — Druid-based settings UI. `views.rs` defines the window layout;
  `data.rs` defines `UIDataAdapter` (Druid data binding); `widgets.rs` has
  custom widgets (`SegmentedControl`, `ToggleSwitch`, `HotkeyBadgesWidget`,
  `AppsListWidget`). The `UPDATE_UI` selector in `selectors.rs` synchronizes
  input state changes to the UI.

### Threading Model

- **Main thread**: Druid UI event loop.
- **Listener thread**: `run_event_listener()` runs the `CGEventTap` callback.
  Communicates back to UI via `EventSink` (stored in global `UI_EVENT_SINK`).
- Global state (`INPUT_STATE`, `UI_EVENT_SINK`) uses `unsafe` static access, as
  callbacks cannot carry context.

### Input Handling Logic (`main.rs` `event_handler`)

1. Check if key matches the toggle hotkey → enable/disable.
2. Non-character keys (arrows, function keys) → reset word tracking.
3. Space/Enter/Tab → finalize word, attempt macro replacement.
4. Backspace → pop character from buffer.
5. Regular character → push to buffer, call `do_transform_keys()` which runs
   `vi-rs` and uses backspace+retype to replace the word.
6. Word restoration: `should_restore_transformed_word()` determines when to
   revert a transformation (e.g., when the user types a non-Vietnamese
   sequence).


================================================
FILE: Cargo.toml
================================================
[package]
description = "Bộ gõ tiếng Việt mã nguồn mở đa hệ điều hành Gõ Key"
edition = "2021"
name = "goxkey"
version = "0.3.1"

# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

[dependencies]
env_logger = "0.10.0"
libc = "0.2.139"
log = "0.4.17"
vi = "0.6.2"
bitflags = "1.3.2"
druid = { features = [
    "image",
    "png",
], git = "https://github.com/huytd/druid", branch = "master" }
once_cell = "1.17.0"
auto-launch = "0.5.0"
nom = "7.1.3"

[target.'cfg(target_os="macos")'.dependencies]
core-foundation = "0.9.3"
core-graphics = "0.22.3"
foreign-types = "0.3.2"
rdev = "0.5.2"
cocoa = "0.24"
objc = "0.2"
objc-foundation = "0.1"
objc_id = "0.1"
accessibility = "0.1.6"
accessibility-sys = "0.1.3"

[package.metadata.bundle]
copyright = "Copyright (c) Huy Tran 2023. All rights reserved."
icon = ["icons/icon.icns", "icons/icon.png"]
identifier = "com.goxkey.app"
name = "GoKey"
version = "0.3.1"


================================================
FILE: Casks/goxkey.rb
================================================
cask "goxkey" do
  version "0.3.0"
  sha256 "e747009b9c78d2ea3d72ed5419c24090553cbc1c7095dc63145de89467c7649e"

  url "https://github.com/huytd/goxkey/releases/download/v#{version}/GoKey-v#{version}.zip"
  name "Gõ Key"
  desc "Vietnamese input method editor for macOS"
  homepage "https://github.com/huytd/goxkey"

  depends_on macos: ">= :monterey"

  app "GoKey.app"

  caveats <<~EOS
    Gõ Key requires Accessibility permission to intercept keyboard events.

    After launching the app, go to:
      System Settings → Privacy & Security → Accessibility
      → enable Gõ Key

    Default toggle shortcut: Ctrl+Space
  EOS
end


================================================
FILE: DEVELOPMENT.md
================================================
## Development

Currently, only macOS is supported. Windows and Linux could also be supported as well but it's not our primary goal. If you're on these OSes, consider contributing! Any help would be greatly appreciated!

This project will only focus on the input handling logic, and provide a frontend for the
input engine ([`vi-rs`](https://github.com/zerox-dg/vi-rs)).

The following diagram explains how `goxkey` communicates with other components like OS's input source and `vi-rs`:

```
INPUT LAYER
+------------------+              FRONTEND                ENGINE
| macOS            | [d,d,a,a,y]  +---------+ "ddaay"     +-------+
|  +- CGEventTap   | -----------> | goxkey  | ----------> | vi-rs |
|                  |              +---------+             +-------+
| Linux   (TBD)    |               |  ^                    |
| Windows (TBD)    |               |  |              "đây" |
+------------------+               |  +--------------------+
                                   |
                                   | (send_key)
                                   v
                                Target
                                Application
```

On macOS, we run an instance of `CGEventTap` to listen for every `keydown` event. A callback function will be called
on every keystroke. In this callback, we have a buffer (`TYPING_BUF`) to keep track of the word that the user is typing.
This buffer will be reset whenever the user hit the `SPACE` or `ENTER` key.

The input engine (`vi-rs`) will receive this buffer and convert it to a correct word, for example: `vieetj` will be
transformed into `việt`.

The result string will be sent back to `goxkey`, and from there, it will perform an edit on the target application. The edit
is done using [the BACKSPACE technique](https://notes.huy.rocks/posts/go-tieng-viet-linux.html#k%C4%A9-thu%E1%BA%ADt-backspace). It's
unreliable but it has the benefit of not having the pre-edit line so it's worth it.

To get yourself familiar with IME, here are some good article on the topic:

- [Vietnamese Keyboard Engine with Prolog](https://followthe.trailing.space/To-the-Root-of-the-Tree-dc170bf0e8de44a6b812ca3e01025236?p=0dd31fe76ebd45dca5b4466c9441fa1c&pm=s), lewtds
- [Ước mơ bộ gõ kiểu Unikey trên Linux](https://followthe.trailing.space/To-the-Root-of-the-Tree-dc170bf0e8de44a6b812ca3e01025236?p=9b12cc2fcdbe43149b10eefc7db6b161&pm=s), lewtds
- [Vấn đề về IME trên Linux](https://viethung.space/blog/2020/07/21/Van-de-ve-IME-tren-Linux/), zerox-dg
- [Bỏ dấu trong tiếng Việt](https://viethung.space/blog/2020/07/14/Bo-dau-trong-tieng-Viet/), zerox-dg
- [Chuyện gõ tiếng Việt trên Linux](https://notes.huy.rocks/posts/go-tieng-viet-linux.html), huytd



## Local development setup

To setup the project locally, first, checkout the code and run the install script to have all the Git hooks configured:

```sh
$ git clone https://github.com/huytd/goxkey && cd goxkey
$ make setup
```

After this step, you can use the `make` commands to run or bundle the code as needed:

```sh
$ make run

# or

$ make bundle
```


================================================
FILE: LICENSE
================================================
BSD 3-Clause License

Copyright (c) 2023, Huy Tran

Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:

1. Redistributions of source code must retain the above copyright notice, this
   list of conditions and the following disclaimer.

2. Redistributions in binary form must reproduce the above copyright notice,
   this list of conditions and the following disclaimer in the documentation
   and/or other materials provided with the distribution.

3. Neither the name of the copyright holder nor the names of its
   contributors may be used to endorse or promote products derived from
   this software without specific prior written permission.

THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.


================================================
FILE: Makefile
================================================
VERSION := $(shell grep '^version' Cargo.toml | head -1 | sed 's/.*= *"\(.*\)"/\1/')

run:
	cargo r

bundle:
	cargo bundle --release

setup:
	mkdir -p .git/hooks
	cp -rf scripts/pre-commit .git/hooks
	chmod +x .git/hooks/pre-commit

# Build, sign, notarize, and produce GoKey-v<VERSION>.zip ready for release.
# Requires: cargo-bundle, a valid "Developer ID Application" cert, and the
# AC_PASSWORD keychain profile configured via xcrun notarytool.
release: bundle
	bash scripts/release
	cd target/release/bundle/osx && \
	  ditto -c -k --keepParent GoKey.app GoKey-v$(VERSION).zip
	@echo "Release asset: target/release/bundle/osx/GoKey-v$(VERSION).zip"
	@echo "SHA256: $$(shasum -a 256 target/release/bundle/osx/GoKey-v$(VERSION).zip | awk '{print $$1}')"

# Update Casks/goxkey.rb with the SHA256 of the just-built release zip.
# Run after `make release` before tagging.
update-cask:
	$(eval SHA256 := $(shell shasum -a 256 target/release/bundle/osx/GoKey-v$(VERSION).zip | awk '{print $$1}'))
	sed -i '' 's/version ".*"/version "$(VERSION)"/' Casks/goxkey.rb
	sed -i '' 's/sha256 ".*"/sha256 "$(SHA256)"/' Casks/goxkey.rb
	@echo "Casks/goxkey.rb updated → version=$(VERSION) sha256=$(SHA256)"


================================================
FILE: NIGHTLY_RELEASE.md
================================================
🍎 Bản build này được release tự động sau mỗi lần update code từ nhóm phát triển. Hiện tại chỉ mới hỗ trợ macOS.

🖥 Để cài đặt và sử dụng, các bạn có thể làm theo các bước sau:

1. Download file **GoKey.zip** về, giải nén ra, sẽ thấy file **Gõ Key.app**
2. Kéo thả file **Gõ Key.app** vào thư mục **/Applications** của macOS
3. Làm theo [hướng dẫn ở đây để cấp quyền Accessibility cho **Gõ Key.app**](https://github.com/huytd/goxkey/wiki/H%C6%B0%E1%BB%9Bng-d%E1%BA%ABn-s%E1%BB%ADa-l%E1%BB%97i-kh%C3%B4ng-g%C3%B5-%C4%91%C6%B0%E1%BB%A3c-ti%E1%BA%BFng-Vi%E1%BB%87t-tr%C3%AAn-macOS#tr%C6%B0%E1%BB%9Dng-h%E1%BB%A3p-l%E1%BB%97i-do-ch%C6%B0a-c%E1%BA%A5p-quy%E1%BB%81n-accessibility)
4. Click phải chuột vào **Gõ Key.app** và chọn Open 
<img width="299" alt="image" src="https://user-images.githubusercontent.com/613943/222664339-1913b636-80da-4775-ad86-b964ea332c1b.png">

🔬 Vì đây là bản build chưa chính thức, và chưa được notarized, macOS sẽ hỏi lại vài lần để chắc là bạn có muốn mở app không, khi xuất hiện các hộp thoại này, xin đừng nhấn nút _"Move to Trash"_ 😂 

<img width="640" src="https://user-images.githubusercontent.com/613943/222976671-10ebb015-0ccb-43ff-a52b-a0579f9be1ce.png">

🐞 Trong quá trình sử dụng, nếu có lỗi xảy ra, xin đừng chửi tác giả, mà vui lòng [Tạo issue mới tại đây](https://github.com/huytd/goxkey/issues) và mô tả vấn đề bạn gặp phải. Nhóm phát triển chân thành cảm ơn sự ủng hộ của các bạn :D 


================================================
FILE: README.md
================================================
<p align="center">
	<img src="./icons/icon.png" width="90px">
</p>


<img width="1585" height="797" alt="screenshots" src="https://github.com/user-attachments/assets/42930e8a-77fb-4493-aa90-3c0bb9f1ab40" />


**Gõkey** - A Vietnamese input method editor.

- :zap: Excellent performance (Gen Z translation: Blazing fast!)
- :crab: Written completely in Rust.
- :keyboard: Supported both Telex and VNI input method.
- :sparkles: Focused on typing experience and features that you will use.

## Why another Vietnamese IME?

> technical curiosity

## About

This is my attempt to build an input method editor using only Rust. It's not the first, and definitely not the last.

The goal is to create an input method editor that enable users to type Vietnamese text on the computer using
either VNI or TELEX method. Other than that, no other features are planned.

## How to install

There are 2 options to download GõKey at this moment: Build from source or Download the Nightly build.

### Option 1: Download the Nightly Build

Nightly build is the prebuilt binary that automatically bundled everytime we merged the code to the `main` branch.

You can download it at the Release page here: https://github.com/huytd/goxkey/releases/tag/nightly-build

### Option 2: Build from source

The source code can be compiled easily:

1. Get the latest stable version of the Rust compiler ([see here](https://rustup.rs/))
2. Install the [cargo-bundle](https://github.com/burtonageo/cargo-bundle) extension, this is necessary for bundling macOS apps
   ```
   cargo install cargo-bundle
   ```
3. Checkout the source code of the **gõkey** project
   ```
   git clone https://github.com/huytd/goxkey && cd goxkey
   ```
4. Run the bundle command:

   ```
   cargo bundle
   ```

After that, you'll find the `GoKey.app` file in the `target/debug/bundle` folder. Copy it to your `/Applications` folder.

5. **(Important!):** Before you run the app, make you you already allowed Accessibility access for the app. Follow the [guide in the Wiki](https://github.com/huytd/goxkey/wiki/H%C6%B0%E1%BB%9Bng-d%E1%BA%ABn-s%E1%BB%ADa-l%E1%BB%97i-kh%C3%B4ng-g%C3%B5-%C4%91%C6%B0%E1%BB%A3c-ti%E1%BA%BFng-Vi%E1%BB%87t-tr%C3%AAn-macOS) to do so.

Without this step, the app will crash and can't be use.

## Development

```sh
# Run with UI-only mode (skip Accessibility permission check)
cargo r -- --skip-permission

# Force a specific UI language (vi or en), ignoring OS language
cargo r -- --lang vi
cargo r -- --lang en
```

## Dependencies

- [core-foundation](https://crates.io/crates/core-foundation), [core-graphics](https://crates.io/crates/core-graphics): for event handling on macOS
- [vi-rs](https://github.com/zerox-dg/vi-rs): the Vietnamese Input Engine

## Fun fact

Do you know how to type gõkey in Telex?

Do this: `gox<cmd>key`


================================================
FILE: scripts/pre-commit
================================================
cargo fmt
git add .


================================================
FILE: scripts/release
================================================
codesign -s "Developer ID Application: Huy Tran" --timestamp --options=runtime target/release/bundle/osx/GoKey.app
ditto -c -k --keepParent target/release/bundle/osx/GoKey.app target/release/bundle/osx/GoKey.zip
xcrun notarytool submit target/release/bundle/osx/GoKey.zip --keychain-profile "AC_PASSWORD" --wait
xcrun stapler staple target/release/bundle/osx/GoKey.app
rm target/release/bundle/osx/GoKey.zip


================================================
FILE: src/config.rs
================================================
use std::collections::BTreeMap;
use std::io::BufRead;
use std::{
    fs::File,
    io,
    io::{Result, Write},
    path::PathBuf,
    sync::Mutex,
};

use once_cell::sync::Lazy;

use crate::platform::get_home_dir;

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

pub struct ConfigStore {
    hotkey: String,
    method: String,
    vn_apps: Vec<String>,
    en_apps: Vec<String>,
    is_macro_enabled: bool,
    is_macro_autocap_enabled: bool,
    macro_table: BTreeMap<String, String>,
    is_auto_toggle_enabled: bool,
    is_gox_mode_enabled: bool,
    is_w_literal_enabled: bool,
    ui_language: String,
    allowed_words: Vec<String>,
}

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

pub(crate) fn parse_kv_string(line: &str) -> Option<(String, String)> {
    if let Some((left, right)) = line.split_once("\"=\"") {
        let left = left.strip_prefix("\"").map(|s| s.replace("\\\"", "\""));
        let right = right.strip_suffix("\"").map(|s| s.replace("\\\"", "\""));
        return left.zip(right);
    }
    return None;
}

pub(crate) fn build_kv_string(k: &str, v: &str) -> String {
    format!(
        "\"{}\"=\"{}\"",
        k.replace("\"", "\\\""),
        v.replace("\"", "\\\"")
    )
}

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

    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(","))?;
        writeln!(
            file,
            "{} = {}",
            ALLOWED_WORDS_CONFIG_KEY,
            self.allowed_words.join(",")
        )?;
        writeln!(
            file,
            "{} = {}",
            AUTOS_TOGGLE_ENABLED_CONFIG_KEY, self.is_auto_toggle_enabled
        )?;
        writeln!(
            file,
            "{} = {}",
            MACRO_ENABLED_CONFIG_KEY, self.is_macro_enabled
        )?;
        writeln!(
            file,
            "{} = {}",
            MACRO_AUTOCAP_ENABLED_CONFIG_KEY, self.is_macro_autocap_enabled
        )?;
        for (k, v) in self.macro_table.iter() {
            writeln!(file, "{} = {}", MACROS_CONFIG_KEY, build_kv_string(k, &v))?;
        }
        writeln!(
            file,
            "{} = {}",
            GOX_MODE_CONFIG_KEY, self.is_gox_mode_enabled
        )?;
        writeln!(
            file,
            "{} = {}",
            W_LITERAL_CONFIG_KEY, self.is_w_literal_enabled
        )?;
        writeln!(file, "{} = {}", UI_LANGUAGE_CONFIG_KEY, self.ui_language)?;
        Ok(())
    }

    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(),
            is_macro_enabled: false,
            is_macro_autocap_enabled: false,
            macro_table: BTreeMap::new(),
            is_auto_toggle_enabled: false,
            is_gox_mode_enabled: false,
            is_w_literal_enabled: false,
            ui_language: "auto".to_string(),
            allowed_words: vec!["đc".to_string()],
        };

        let config_path = ConfigStore::get_config_path();

        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()),
                        ALLOWED_WORDS_CONFIG_KEY => {
                            config.allowed_words = parse_vec_string(right.to_string())
                        }
                        AUTOS_TOGGLE_ENABLED_CONFIG_KEY => {
                            config.is_auto_toggle_enabled = matches!(right.trim(), "true")
                        }
                        MACRO_ENABLED_CONFIG_KEY => {
                            config.is_macro_enabled = matches!(right.trim(), "true")
                        }
                        MACRO_AUTOCAP_ENABLED_CONFIG_KEY => {
                            config.is_macro_autocap_enabled = matches!(right.trim(), "true")
                        }
                        MACROS_CONFIG_KEY => {
                            if let Some((k, v)) = parse_kv_string(right) {
                                config.macro_table.insert(k, v);
                            }
                        }
                        GOX_MODE_CONFIG_KEY => {
                            config.is_gox_mode_enabled = matches!(right.trim(), "true")
                        }
                        W_LITERAL_CONFIG_KEY => {
                            config.is_w_literal_enabled = matches!(right.trim(), "true")
                        }
                        UI_LANGUAGE_CONFIG_KEY => config.ui_language = right.trim().to_string(),
                        _ => {}
                    }
                }
            }
        }

        config
    }

    // 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 get_vn_apps(&self) -> Vec<String> {
        self.vn_apps.clone()
    }

    pub fn get_en_apps(&self) -> Vec<String> {
        self.en_apps.clone()
    }

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

    pub fn add_english_app(&mut self, app_name: &str) {
        if self.is_vietnamese_app(app_name) {
            self.vn_apps.retain(|x| x != app_name);
        }
        if !self.is_english_app(app_name) {
            self.en_apps.push(app_name.to_string());
        }
        self.save();
    }

    pub fn remove_vietnamese_app(&mut self, app_name: &str) {
        self.vn_apps.retain(|x| x != app_name);
        self.save();
    }

    pub fn remove_english_app(&mut self, app_name: &str) {
        self.en_apps.retain(|x| x != app_name);
        self.save();
    }

    pub fn is_allowed_word(&self, word: &str) -> bool {
        self.allowed_words.contains(&word.to_string())
    }

    pub fn is_auto_toggle_enabled(&self) -> bool {
        self.is_auto_toggle_enabled
    }

    pub fn set_auto_toggle_enabled(&mut self, flag: bool) {
        self.is_auto_toggle_enabled = flag;
        self.save();
    }

    pub fn is_gox_mode_enabled(&self) -> bool {
        self.is_gox_mode_enabled
    }

    pub fn set_gox_mode_enabled(&mut self, flag: bool) {
        self.is_gox_mode_enabled = flag;
        self.save();
    }

    pub fn is_w_literal_enabled(&self) -> bool {
        self.is_w_literal_enabled
    }

    pub fn set_w_literal_enabled(&mut self, flag: bool) {
        self.is_w_literal_enabled = flag;
        self.save();
    }

    pub fn get_ui_language(&self) -> &str {
        &self.ui_language
    }

    pub fn set_ui_language(&mut self, lang: &str) {
        self.ui_language = lang.to_string();
        self.save();
    }

    pub fn is_macro_enabled(&self) -> bool {
        self.is_macro_enabled
    }

    pub fn set_macro_enabled(&mut self, flag: bool) {
        self.is_macro_enabled = flag;
        self.save();
    }

    pub fn is_macro_autocap_enabled(&self) -> bool {
        self.is_macro_autocap_enabled
    }

    pub fn set_macro_autocap_enabled(&mut self, flag: bool) {
        self.is_macro_autocap_enabled = flag;
        self.save();
    }

    pub fn get_macro_table(&self) -> &BTreeMap<String, String> {
        &self.macro_table
    }

    pub fn add_macro(&mut self, from: String, to: String) {
        self.macro_table.insert(from, to);
        self.save();
    }

    pub fn delete_macro(&mut self, from: &String) {
        self.macro_table.remove(from);
        self.save();
    }

    // Save config to file
    fn save(&mut self) {
        self.write_config_data().expect("Failed to write config");
    }
}

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";
const MACRO_ENABLED_CONFIG_KEY: &str = "is_macro_enabled";
const MACRO_AUTOCAP_ENABLED_CONFIG_KEY: &str = "is_macro_autocap_enabled";
const AUTOS_TOGGLE_ENABLED_CONFIG_KEY: &str = "is_auto_toggle_enabled";
const MACROS_CONFIG_KEY: &str = "macros";
const GOX_MODE_CONFIG_KEY: &str = "is_gox_mode_enabled";
const W_LITERAL_CONFIG_KEY: &str = "is_w_literal_enabled";
const UI_LANGUAGE_CONFIG_KEY: &str = "ui_language";
const ALLOWED_WORDS_CONFIG_KEY: &str = "allowed_words";


================================================
FILE: src/hotkey.rs
================================================
use std::fmt::Display;

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

pub struct Hotkey {
    modifiers: KeyModifier,
    keycode: Option<char>,
}

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

    pub fn is_match(&self, mut modifiers: KeyModifier, keycode: Option<char>) -> bool {
        // Caps Lock should not interfere with any hotkey
        modifiers.remove(KeyModifier::MODIFIER_CAPSLOCK);
        let letter_matched = keycode.eq(&self.keycode)
            || keycode
                .and_then(|a| self.keycode.map(|b| a.eq_ignore_ascii_case(&b)))
                .is_some_and(|c| c == true);
        self.modifiers == modifiers && letter_matched
    }

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

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

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

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

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

#[test]
fn test_can_match_with_or_without_capslock() {
    let hotkey = Hotkey::from_str("super+ctrl+space");
    let mut actual_modifier = KeyModifier::new();
    actual_modifier.add_super();
    actual_modifier.add_control();
    assert_eq!(hotkey.is_match(actual_modifier, Some(' ')), true);

    actual_modifier.add_capslock();
    assert!(hotkey.is_match(actual_modifier, Some(' ')));
}

#[test]
fn test_parse_with_just_modifiers() {
    let hotkey = Hotkey::from_str("ctrl+shift");
    let mut actual_modifier = KeyModifier::new();
    actual_modifier.add_control();
    actual_modifier.add_shift();
    assert_eq!(hotkey.modifiers, actual_modifier);
    assert_eq!(hotkey.keycode, None);
    assert!(hotkey.is_match(actual_modifier, None));
}

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

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

    assert_eq!(
        format!("{}", Hotkey::from_str("ctrl+shift+o")),
        format!("{} {} O", SYMBOL_CTRL, SYMBOL_SHIFT)
    );
}


================================================
FILE: src/input.rs
================================================
use std::collections::BTreeMap;
use std::{collections::HashMap, fmt::Display, str::FromStr};

use druid::{Data, Target};
use log::debug;
use once_cell::sync::{Lazy, OnceCell};
use rdev::{Keyboard, KeyboardState};
use vi::TransformResult;

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

// According to Google search, the longest possible Vietnamese word
// is "nghiêng", which is 7 letters long. Add a little buffer for
// tone and marks, I guess the longest possible buffer length would
// be around 10 to 12.
const MAX_POSSIBLE_WORD_LENGTH: usize = 10;
const MAX_DUPLICATE_LENGTH: usize = 4;
const TONE_DUPLICATE_PATTERNS: [&str; 17] = [
    "ss", "ff", "jj", "rr", "xx", "ww", "kk", "tt", "nn", "mm", "yy", "hh", "ii", "aaa", "eee",
    "ooo", "ddd",
];

pub static mut INPUT_STATE: Lazy<InputState> = Lazy::new(InputState::new);
pub static mut HOTKEY_MODIFIERS: KeyModifier = KeyModifier::MODIFIER_NONE;
pub static mut HOTKEY_MATCHING: bool = false;
pub static mut HOTKEY_MATCHING_CIRCUIT_BREAK: bool = false;

pub const PREDEFINED_CHARS: [char; 47] = [
    'a', '`', '1', '2', '3', '4', '5', '6', '7', '8', '9', '0', '-', '=', 'q', 'w', 'e', 'r', 't',
    'y', 'u', 'i', 'o', 'p', '[', ']', 's', 'd', 'f', 'g', 'h', 'j', 'k', 'l', ';', '\'', '\\',
    'z', 'x', 'c', 'v', 'b', 'n', 'm', ',', '.', '/',
];

pub const STOP_TRACKING_WORDS: [&str; 4] = [";", "'", "?", "/"];
/// In w-literal mode, replace standalone 'w' with placeholder bytes that the telex
/// engine ignores (falls through to `_ => Transformation::Ignored`), then restore them
/// after transformation. A 'w' is "standalone" when NOT preceded by a Horn/Breve-eligible
/// vowel — those cases (uw→ư, ow→ơ, aw→ă) should still be handled by telex normally.
enum CapPattern {
    Lower,
    TitleCase,
    AllCaps,
}

fn detect_cap_pattern(s: &str) -> CapPattern {
    let mut chars = s.chars().filter(|c| c.is_alphabetic());
    match chars.next() {
        Some(first) if first.is_uppercase() => {
            if chars.all(|c| c.is_uppercase()) {
                CapPattern::AllCaps
            } else {
                CapPattern::TitleCase
            }
        }
        _ => CapPattern::Lower,
    }
}

fn apply_cap_pattern(s: &str, pattern: CapPattern) -> String {
    match pattern {
        CapPattern::Lower => s.to_string(),
        CapPattern::AllCaps => s.to_uppercase(),
        CapPattern::TitleCase => {
            let mut chars = s.chars();
            match chars.next() {
                None => String::new(),
                Some(first) => first.to_uppercase().to_string() + chars.as_str(),
            }
        }
    }
}

fn mask_standalone_w(buffer: &str) -> String {
    // Characters that can accept Horn (w) modification: u, o and all their toned forms.
    // Characters that can accept Breve (w) modification: a and all its toned forms.
    const HORN_BREVE_ELIGIBLE: &str = "uoaUOA\u{01b0}\u{01a1}\u{0103}\
         \u{00fa}\u{00f3}\u{00e1}\u{00f9}\u{00f2}\u{00e0}\
         \u{1ee7}\u{1ecf}\u{1ea3}\u{0169}\u{00f5}\u{00e3}\u{1ecd}\u{1ea1}\
         \u{00da}\u{00d3}\u{00c1}\u{00d9}\u{00d2}\u{00c0}\
         \u{1ee6}\u{1ece}\u{1ea2}\u{0168}\u{00d5}\u{00c3}\u{1ecc}\u{1ea0}";
    let chars: Vec<char> = buffer.chars().collect();
    let mut result = String::with_capacity(buffer.len() + 4);
    for (i, &ch) in chars.iter().enumerate() {
        if ch == 'w' || ch == 'W' {
            let preceded_by_eligible = i > 0 && HORN_BREVE_ELIGIBLE.contains(chars[i - 1]);
            // Also pass through when this 'w' follows a 'w' that was itself
            // preceded by an eligible vowel (e.g. "aww", "uww", "oww").
            // This lets telex see the full "ww" sequence and undo the
            // Horn/Breve modification, producing the raw text.
            let preceded_by_w_after_eligible = i >= 2
                && (chars[i - 1] == 'w' || chars[i - 1] == 'W')
                && HORN_BREVE_ELIGIBLE.contains(chars[i - 2]);
            if preceded_by_eligible || preceded_by_w_after_eligible {
                result.push(ch); // let telex transform it: uw→ư, ow→ơ, aw→ă, or ww→undo
            } else {
                // Mask it — telex ignores \x01/\x02, we restore them after transform
                result.push(if ch == 'w' { '\x01' } else { '\x02' });
            }
        } else {
            result.push(ch);
        }
    }
    result
}

pub fn get_key_from_char(c: char) -> rdev::Key {
    use rdev::Key::*;
    match &c {
        'a' => KeyA,
        '`' => BackQuote,
        '1' => Num1,
        '2' => Num2,
        '3' => Num3,
        '4' => Num4,
        '5' => Num5,
        '6' => Num6,
        '7' => Num7,
        '8' => Num8,
        '9' => Num9,
        '0' => Num0,
        '-' => Minus,
        '=' => Equal,
        'q' => KeyQ,
        'w' => KeyW,
        'e' => KeyE,
        'r' => KeyR,
        't' => KeyT,
        'y' => KeyY,
        'u' => KeyU,
        'i' => KeyI,
        'o' => KeyO,
        'p' => KeyP,
        '[' => LeftBracket,
        ']' => RightBracket,
        's' => KeyS,
        'd' => KeyD,
        'f' => KeyF,
        'g' => KeyG,
        'h' => KeyH,
        'j' => KeyJ,
        'k' => KeyK,
        'l' => KeyL,
        ';' => SemiColon,
        '\'' => Quote,
        '\\' => BackSlash,
        'z' => KeyZ,
        'x' => KeyX,
        'c' => KeyC,
        'v' => KeyV,
        'b' => KeyB,
        'n' => KeyN,
        'm' => KeyM,
        ',' => Comma,
        '.' => Dot,
        '/' => Slash,
        _ => Unknown(0),
    }
}

pub static mut KEYBOARD_LAYOUT_CHARACTER_MAP: OnceCell<HashMap<char, char>> = OnceCell::new();

fn build_keyboard_layout_map(map: &mut HashMap<char, char>) {
    map.clear();
    let mut kb = Keyboard::new().unwrap();
    for c in PREDEFINED_CHARS {
        let key = rdev::EventType::KeyPress(get_key_from_char(c));
        if let Some(s) = kb.add(&key) {
            let ch = s.chars().last().unwrap();
            map.insert(c, ch);
        }
    }
}

pub fn rebuild_keyboard_layout_map() {
    unsafe {
        if let Some(map) = KEYBOARD_LAYOUT_CHARACTER_MAP.get_mut() {
            debug!("Rebuild keyboard layout map...");
            build_keyboard_layout_map(map);
            debug!("Done");
        } else {
            debug!("Creating keyboard layout map...");
            let mut map = HashMap::new();
            build_keyboard_layout_map(&mut map);
            _ = KEYBOARD_LAYOUT_CHARACTER_MAP.set(map);
            debug!("Done");
        }
    }
}

#[allow(clippy::upper_case_acronyms)]
#[derive(PartialEq, Eq, Data, Clone, Copy)]
pub enum TypingMethod {
    VNI,
    Telex,
    TelexVNI,
}

impl FromStr for TypingMethod {
    type Err = ();

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

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

/// Compute the minimal edit needed to transform what is currently displayed (`old`)
/// into the desired output (`new`) by finding their longest common prefix.
///
/// Returns `(backspace_count, suffix)` where:
/// - `backspace_count` is the number of backspaces to send (to erase only the
///   diverging tail of `old`)
/// - `suffix` is the slice of `new` that must be typed after those backspaces
///
/// Both counts are in **Unicode scalar values** (chars), not bytes, because
/// each backspace deletes one displayed character regardless of its byte width.
/// The returned `suffix` is a byte slice of `new` starting at the first
/// diverging char — no allocation, no `.collect()`.
///
/// # Example
/// ```
/// // old = "mô"  (on screen after typing "moo")
/// // new = "mộ"  (engine output after pressing 'j' for nặng tone)
/// // common prefix = "m"  → only "ô" needs deleting, only "ộ" needs typing
/// let (bs, suffix) = get_diff_parts("mô", "mộ");
/// assert_eq!(bs, 1);
/// assert_eq!(suffix, "ộ");
/// ```
pub fn get_diff_parts<'a>(old: &str, new: &'a str) -> (usize, &'a str) {
    // Walk both strings char-by-char simultaneously.
    // We track the byte offset into `new` so we can return a zero-copy suffix slice.
    let mut old_chars = old.chars();
    let mut new_chars = new.char_indices();

    // Number of chars that are identical from the start.
    let mut common = 0usize;
    // Byte offset in `new` where divergence begins (used for the suffix slice).
    let mut diverge_byte = new.len(); // default: full match, empty suffix

    loop {
        match (old_chars.next(), new_chars.next()) {
            (Some(a), Some((byte_pos, b))) if a == b => {
                common += 1;
                diverge_byte = byte_pos + b.len_utf8();
            }
            (_, Some((byte_pos, _))) => {
                // Diverged — note byte position of the first differing char in `new`.
                diverge_byte = byte_pos;
                break;
            }
            (_, None) => {
                // `new` is a prefix of (or equal to) `old` — no suffix to type.
                diverge_byte = new.len();
                break;
            }
        }
    }

    // old_tail_len = number of chars in old that are NOT part of the common prefix.
    let old_len = old.chars().count();
    let backspace_count = old_len.saturating_sub(common);
    let suffix = &new[diverge_byte..];

    (backspace_count, suffix)
}

pub struct InputState {
    buffer: String,
    display_buffer: String,
    method: TypingMethod,
    hotkey: Hotkey,
    enabled: bool,
    should_track: bool,
    previous_word: String,
    previous_display: String,
    can_resume_previous_word: bool,
    active_app: String,
    is_macro_enabled: bool,
    is_macro_autocap_enabled: bool,
    macro_table: BTreeMap<String, String>,
    temporary_disabled: bool,
    previous_modifiers: KeyModifier,
    is_auto_toggle_enabled: bool,
    is_gox_mode_enabled: bool,
    is_w_literal_enabled: bool,
}

impl InputState {
    pub fn new() -> Self {
        let config = CONFIG_MANAGER.lock().unwrap();
        Self {
            buffer: String::new(),
            display_buffer: String::new(),
            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_display: String::new(),
            can_resume_previous_word: false,
            active_app: String::new(),
            is_macro_enabled: config.is_macro_enabled(),
            is_macro_autocap_enabled: config.is_macro_autocap_enabled(),
            macro_table: config.get_macro_table().clone(),
            temporary_disabled: false,
            previous_modifiers: KeyModifier::empty(),
            is_auto_toggle_enabled: config.is_auto_toggle_enabled(),
            is_gox_mode_enabled: config.is_gox_mode_enabled(),
            is_w_literal_enabled: config.is_w_literal_enabled(),
        }
    }

    pub fn update_active_app(&mut self) -> Option<()> {
        let current_active_app = get_active_app_name();
        // Only check if switch app
        if current_active_app == self.active_app {
            return None;
        }
        self.active_app = current_active_app;
        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;
        }
        Some(())
    }

    pub fn set_temporary_disabled(&mut self) {
        self.temporary_disabled = true;
    }

    pub fn is_gox_mode_enabled(&self) -> bool {
        self.is_gox_mode_enabled
    }

    pub fn is_w_literal_enabled(&self) -> bool {
        self.is_w_literal_enabled
    }

    pub fn toggle_w_literal(&mut self) {
        self.is_w_literal_enabled = !self.is_w_literal_enabled;
        CONFIG_MANAGER
            .lock()
            .unwrap()
            .set_w_literal_enabled(self.is_w_literal_enabled);
    }

    pub fn is_enabled(&self) -> bool {
        !self.temporary_disabled && self.enabled
    }

    pub fn is_tracking(&self) -> bool {
        self.should_track
    }

    pub fn is_buffer_empty(&self) -> bool {
        self.buffer.is_empty()
    }

    pub fn new_word(&mut self) {
        if !self.buffer.is_empty() {
            self.clear();
        }
        if self.temporary_disabled {
            self.temporary_disabled = false;
        }
        self.should_track = true;
        self.can_resume_previous_word = false;
    }

    /// Mark that the previous word can be resumed if the user presses
    /// backspace immediately (i.e. the word was ended by space/tab/enter).
    pub fn mark_resumable(&mut self) {
        self.can_resume_previous_word = true;
    }

    /// Try to restore the previous word's buffers so editing can continue.
    /// Returns true if the word was resumed, false otherwise.
    pub fn try_resume_previous_word(&mut self) -> bool {
        if !self.can_resume_previous_word || self.previous_word.is_empty() {
            return false;
        }
        self.buffer = self.previous_word.clone();
        self.display_buffer = self.previous_display.clone();
        self.should_track = true;
        self.can_resume_previous_word = false;
        true
    }

    pub fn get_macro_target(&self) -> Option<String> {
        if !self.is_macro_enabled {
            return None;
        }
        // Exact match
        if let Some(target) = self.macro_table.get(&self.display_buffer) {
            return Some(target.clone());
        }
        // Auto-capitalize: try lowercase lookup, then apply cap pattern
        if self.is_macro_autocap_enabled {
            let lower = self.display_buffer.to_lowercase();
            if let Some(target) = self.macro_table.get(&lower) {
                let pattern = detect_cap_pattern(&self.display_buffer);
                return Some(apply_cap_pattern(target, pattern));
            }
        }
        None
    }

    pub fn is_macro_autocap_enabled(&self) -> bool {
        self.is_macro_autocap_enabled
    }

    pub fn toggle_macro_autocap(&mut self) {
        self.is_macro_autocap_enabled = !self.is_macro_autocap_enabled;
        CONFIG_MANAGER
            .lock()
            .unwrap()
            .set_macro_autocap_enabled(self.is_macro_autocap_enabled);
    }

    pub fn get_typing_buffer(&self) -> &str {
        &self.buffer
    }

    pub fn get_displaying_word(&self) -> &str {
        &self.display_buffer
    }

    pub fn stop_tracking(&mut self) {
        self.clear();
        self.should_track = false;
    }

    pub fn toggle_vietnamese(&mut self) {
        self.enabled = !self.enabled;
        self.temporary_disabled = false;
        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();
    }

    pub fn add_vietnamese_app(&mut self, app_name: &str) {
        CONFIG_MANAGER.lock().unwrap().add_vietnamese_app(app_name);
    }

    pub fn add_english_app(&mut self, app_name: &str) {
        CONFIG_MANAGER.lock().unwrap().add_english_app(app_name);
    }

    pub fn remove_vietnamese_app(&mut self, app_name: &str) {
        CONFIG_MANAGER
            .lock()
            .unwrap()
            .remove_vietnamese_app(app_name);
    }

    pub fn remove_english_app(&mut self, app_name: &str) {
        CONFIG_MANAGER.lock().unwrap().remove_english_app(app_name);
    }

    pub fn get_vn_apps(&self) -> Vec<String> {
        CONFIG_MANAGER.lock().unwrap().get_vn_apps()
    }

    pub fn get_en_apps(&self) -> Vec<String> {
        CONFIG_MANAGER.lock().unwrap().get_en_apps()
    }

    pub fn set_method(&mut self, method: TypingMethod) {
        self.method = method;
        self.new_word();
        CONFIG_MANAGER
            .lock()
            .unwrap()
            .set_method(&method.to_string());
        if let Some(event_sink) = UI_EVENT_SINK.get() {
            _ = event_sink.submit_command(UPDATE_UI, (), Target::Auto);
        }
    }

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

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

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

    pub fn is_auto_toggle_enabled(&self) -> bool {
        self.is_auto_toggle_enabled
    }

    pub fn toggle_auto_toggle(&mut self) {
        self.is_auto_toggle_enabled = !self.is_auto_toggle_enabled;
        CONFIG_MANAGER
            .lock()
            .unwrap()
            .set_auto_toggle_enabled(self.is_auto_toggle_enabled);
    }

    pub fn is_macro_enabled(&self) -> bool {
        self.is_macro_enabled
    }

    pub fn toggle_macro_enabled(&mut self) {
        self.is_macro_enabled = !self.is_macro_enabled;
        CONFIG_MANAGER
            .lock()
            .unwrap()
            .set_macro_enabled(self.is_macro_enabled);
    }

    pub fn get_macro_table(&self) -> &BTreeMap<String, String> {
        &self.macro_table
    }

    pub fn delete_macro(&mut self, from: &String) {
        self.macro_table.remove(from);
        CONFIG_MANAGER.lock().unwrap().delete_macro(from);
    }

    pub fn add_macro(&mut self, from: String, to: String) {
        CONFIG_MANAGER
            .lock()
            .unwrap()
            .add_macro(from.clone(), to.clone());
        self.macro_table.insert(from, to);
    }

    pub fn export_macros_to_file(&self, path: &str) -> std::io::Result<()> {
        use crate::config::build_kv_string;
        use std::fs::File;
        use std::io::Write;
        let mut file = File::create(path)?;
        for (k, v) in &self.macro_table {
            writeln!(file, "{}", build_kv_string(k, v))?;
        }
        Ok(())
    }

    pub fn import_macros_from_file(&mut self, path: &str) -> std::io::Result<usize> {
        use crate::config::parse_kv_string;
        use std::fs::File;
        use std::io::{BufRead, BufReader};
        let file = File::open(path)?;
        let reader = BufReader::new(file);
        let mut count = 0;
        for line in reader.lines() {
            let line = line?;
            let line = line.trim();
            if line.is_empty() {
                continue;
            }
            if let Some((from, to)) = parse_kv_string(line) {
                self.add_macro(from, to);
                count += 1;
            }
        }
        Ok(count)
    }

    pub fn should_transform_keys(&self, c: &char) -> bool {
        self.enabled
    }

    pub fn transform_keys(&self) -> Result<(String, TransformResult), ()> {
        // In w-literal mode (Telex only), replace standalone 'w' with a placeholder
        // before feeding to the telex engine, then restore it in the output.
        // A 'w' is considered standalone if NOT preceded by a Horn/Breve-eligible vowel
        // (u, o for Horn; a for Breve). This preserves uw→ư, ow→ơ, aw→ă etc.
        let effective_buffer = if self.is_w_literal_enabled
            && matches!(self.method, TypingMethod::Telex | TypingMethod::TelexVNI)
        {
            mask_standalone_w(&self.buffer)
        } else {
            self.buffer.clone()
        };

        if self.method == TypingMethod::TelexVNI {
            // Try both methods; prefer VNI when the buffer contains digits
            // (VNI's key differentiator), otherwise fall back to Telex.
            let buffer = effective_buffer;
            let result = std::panic::catch_unwind(move || {
                let has_digits = buffer.chars().any(|c| c.is_ascii_digit());
                if has_digits {
                    let mut output = String::new();
                    let transform_result = vi::vni::transform_buffer(buffer.chars(), &mut output);
                    (output, transform_result)
                } else {
                    let mut output = String::new();
                    let transform_result = vi::telex::transform_buffer(buffer.chars(), &mut output);
                    let output = output.replace('\x01', "w").replace('\x02', "W");
                    (output, transform_result)
                }
            });
            return result.map_err(|_| ());
        }

        let method = self.method;
        let buffer = effective_buffer;
        let is_w_literal = self.is_w_literal_enabled;
        let result = std::panic::catch_unwind(move || {
            let mut output = String::new();
            let transform_result = match method {
                TypingMethod::VNI => vi::vni::transform_buffer(buffer.chars(), &mut output),
                TypingMethod::Telex | TypingMethod::TelexVNI => {
                    vi::telex::transform_buffer(buffer.chars(), &mut output)
                }
            };
            // Restore masked standalone w's back to literal 'w'/'W'
            let output = if is_w_literal {
                output.replace('\x01', "w").replace('\x02', "W")
            } else {
                output
            };
            (output, transform_result)
        });
        if let Ok((output, transform_result)) = result {
            return Ok((output, transform_result));
        }
        Err(())
    }

    pub fn should_send_keyboard_event(&self, word: &str) -> bool {
        !self.display_buffer.eq(word)
    }

    pub fn should_dismiss_selection_if_needed(&self) -> bool {
        const DISMISS_APPS: [&str; 3] = ["Firefox", "Floorp", "Zen"];
        return DISMISS_APPS.iter().any(|app| self.active_app.contains(app));
    }

    pub fn get_backspace_count(&self, is_delete: bool) -> usize {
        let dp_len = self.display_buffer.chars().count();
        let backspace_count = if is_delete && dp_len >= 1 {
            dp_len
        } else {
            dp_len - 1
        };

        if is_in_text_selection() {
            backspace_count + 1
        } else {
            backspace_count
        }
    }

    pub fn replace(&mut self, buf: String) {
        self.display_buffer = buf;
    }

    pub fn push(&mut self, c: char) {
        if let Some(first_char) = self.buffer.chars().next() {
            if first_char.is_numeric() {
                self.buffer.remove(0);
                self.display_buffer.remove(0);
            }
        }
        if self.buffer.len() <= MAX_POSSIBLE_WORD_LENGTH {
            self.buffer.push(c);
            self.display_buffer.push(c);
            debug!(
                "Input buffer: {:?} - Display buffer: {:?}",
                self.buffer, self.display_buffer
            );
        }
    }

    pub fn pop(&mut self) {
        self.buffer.pop();
        if self.buffer.is_empty() {
            self.display_buffer.clear();
            self.new_word();
        }
    }

    pub fn clear(&mut self) {
        self.previous_word = self.buffer.to_owned();
        self.previous_display = self.display_buffer.to_owned();
        self.buffer.clear();
        self.display_buffer.clear();
    }

    pub fn get_previous_word(&self) -> &str {
        &self.previous_word
    }

    pub fn clear_previous_word(&mut self) {
        self.previous_word.clear();
    }

    pub fn previous_word_is_stop_tracking_words(&self) -> bool {
        STOP_TRACKING_WORDS.contains(&self.previous_word.as_str())
    }

    pub fn should_stop_tracking(&mut self) -> bool {
        let len = self.buffer.len();
        if len > MAX_POSSIBLE_WORD_LENGTH {
            return true;
        }
        let buf = &self.buffer;
        if TONE_DUPLICATE_PATTERNS
            .iter()
            .find(|p| buf.to_ascii_lowercase().contains(*p))
            .is_some()
        {
            return true;
        }

        if self.previous_word_is_stop_tracking_words() {
            return true;
        }

        false
    }

    pub fn stop_tracking_if_needed(&mut self) {
        if self.should_stop_tracking() {
            self.stop_tracking();
            debug!("! Stop tracking");
        }
    }

    pub fn get_previous_modifiers(&self) -> KeyModifier {
        self.previous_modifiers
    }

    pub fn save_previous_modifiers(&mut self, modifiers: KeyModifier) {
        self.previous_modifiers = modifiers;
    }

    pub fn is_allowed_word(&self, word: &str) -> bool {
        let config = CONFIG_MANAGER.lock().unwrap();
        return config.is_allowed_word(word);
    }
}

#[cfg(test)]
mod diff_tests {
    use super::get_diff_parts;

    // ── Basic tone application ────────────────────────────────────────────────

    /// "mô" → "mộ": only the vowel+tone char is replaced, "m" stays.
    #[test]
    fn tone_on_vowel_preserves_consonant_prefix() {
        let (bs, sfx) = get_diff_parts("mô", "mộ");
        assert_eq!(bs, 1, "should delete only 'ô'");
        assert_eq!(sfx, "ộ");
    }

    /// "mo" → "mô": typing 'o' again applies the circumflex.
    #[test]
    fn circumflex_application() {
        let (bs, sfx) = get_diff_parts("mo", "mô");
        assert_eq!(bs, 1);
        assert_eq!(sfx, "ô");
    }

    /// "tieng" → "tiếng": "ti" preserved, vowel+tone suffix replaced.
    #[test]
    fn multi_char_prefix_preserved() {
        let (bs, sfx) = get_diff_parts("tieng", "tiếng");
        assert_eq!(bs, 3); // "eng" deleted
        assert_eq!(sfx, "ếng");
    }

    /// "nguyen" → "nguyên": "nguy" is common.
    #[test]
    fn longer_common_prefix() {
        let (bs, sfx) = get_diff_parts("nguyen", "nguyên");
        assert_eq!(bs, 2); // "en" deleted
        assert_eq!(sfx, "ên");
    }

    // ── No-op / identical strings ─────────────────────────────────────────────

    /// Identical strings → 0 backspaces, empty suffix.
    #[test]
    fn identical_strings_no_op() {
        let (bs, sfx) = get_diff_parts("mộ", "mộ");
        assert_eq!(bs, 0);
        assert_eq!(sfx, "");
    }

    // ── Empty edge cases ──────────────────────────────────────────────────────

    #[test]
    fn both_empty() {
        let (bs, sfx) = get_diff_parts("", "");
        assert_eq!(bs, 0);
        assert_eq!(sfx, "");
    }

    #[test]
    fn old_empty_new_nonempty() {
        let (bs, sfx) = get_diff_parts("", "mộ");
        assert_eq!(bs, 0);
        assert_eq!(sfx, "mộ");
    }

    #[test]
    fn old_nonempty_new_empty() {
        let (bs, sfx) = get_diff_parts("mô", "");
        assert_eq!(bs, 2);
        assert_eq!(sfx, "");
    }

    // ── Prefix / suffix relationships ─────────────────────────────────────────

    /// new is a strict prefix of old: delete tail, type nothing.
    #[test]
    fn new_is_prefix_of_old() {
        let (bs, sfx) = get_diff_parts("mộng", "mộ");
        assert_eq!(bs, 2); // delete "ng"
        assert_eq!(sfx, "");
    }

    /// old is a strict prefix of new: 0 backspaces, append tail.
    #[test]
    fn old_is_prefix_of_new() {
        let (bs, sfx) = get_diff_parts("mộ", "mộng");
        assert_eq!(bs, 0);
        assert_eq!(sfx, "ng");
    }

    // ── Completely different strings ──────────────────────────────────────────

    #[test]
    fn no_common_prefix() {
        let (bs, sfx) = get_diff_parts("abc", "xyz");
        assert_eq!(bs, 3);
        assert_eq!(sfx, "xyz");
    }

    // ── Multi-byte / Unicode correctness ─────────────────────────────────────

    /// Each Vietnamese toned vowel is 1 char, possibly 3 bytes.
    /// backspace_count must be in chars, not bytes.
    #[test]
    fn char_count_not_byte_count() {
        let (bs, sfx) = get_diff_parts("ộ", "ô");
        assert_eq!(bs, 1, "one char deleted, not three bytes");
        assert_eq!(sfx, "ô");
    }

    #[test]
    fn all_multibyte_no_common_prefix() {
        let (bs, sfx) = get_diff_parts("ộ", "ể");
        assert_eq!(bs, 1);
        assert_eq!(sfx, "ể");
    }

    // ── Realistic Telex sequences ─────────────────────────────────────────────

    /// "moo" (buffer) → "mô" (engine output).
    #[test]
    fn telex_moo_to_mo_hat() {
        let (bs, sfx) = get_diff_parts("moo", "mô");
        assert_eq!(bs, 2);
        assert_eq!(sfx, "ô");
    }

    /// "cas" → "cá": "c" preserved.
    #[test]
    fn telex_cas_to_ca_sac() {
        let (bs, sfx) = get_diff_parts("cas", "cá");
        assert_eq!(bs, 2);
        assert_eq!(sfx, "á");
    }

    /// "viet" → "việt"
    #[test]
    fn telex_viet_transform() {
        let (bs, sfx) = get_diff_parts("viet", "việt");
        assert_eq!(bs, 2); // common = "vi"
        assert_eq!(sfx, "ệt");
    }

    /// Tone cycling: "tiến" → "tiền" (sắc → huyền), "ti" preserved.
    #[test]
    fn tone_cycling_preserves_prefix() {
        let (bs, sfx) = get_diff_parts("tiến", "tiền");
        assert_eq!(bs, 2);
        assert_eq!(sfx, "ền");
    }

    // ── Suffix slice is a zero-copy view into `new` ───────────────────────────

    #[test]
    fn suffix_is_valid_utf8_slice_of_new() {
        let new = "nguyên";
        let (_, sfx) = get_diff_parts("nguyen", new);
        let new_start = new.as_ptr() as usize;
        let sfx_start = sfx.as_ptr() as usize;
        assert!(sfx_start >= new_start);
        assert!(sfx_start + sfx.len() <= new_start + new.len());
        assert_eq!(sfx, "ên");
    }
}

#[cfg(test)]
mod mask_w_tests {
    use super::mask_standalone_w;

    #[test]
    fn standalone_w_is_masked() {
        // 'w' not preceded by eligible vowel → masked
        assert_eq!(mask_standalone_w("w"), "\x01");
        assert_eq!(mask_standalone_w("rw"), "r\x01");
    }

    #[test]
    fn standalone_upper_w_is_masked() {
        assert_eq!(mask_standalone_w("W"), "\x02");
        assert_eq!(mask_standalone_w("RW"), "R\x02");
    }

    #[test]
    fn w_after_eligible_vowel_is_not_masked() {
        // aw→ă, uw→ư, ow→ơ should pass through
        assert_eq!(mask_standalone_w("aw"), "aw");
        assert_eq!(mask_standalone_w("uw"), "uw");
        assert_eq!(mask_standalone_w("ow"), "ow");
    }

    #[test]
    fn ww_after_eligible_vowel_not_masked() {
        // "aww" → both w's passed through so telex sees "ww" and undoes breve
        assert_eq!(mask_standalone_w("aww"), "aww");
        assert_eq!(mask_standalone_w("uww"), "uww");
        assert_eq!(mask_standalone_w("oww"), "oww");
        assert_eq!(mask_standalone_w("raww"), "raww");
    }

    #[test]
    fn standalone_ww_both_masked() {
        // "ww" with no eligible vowel before → both masked
        assert_eq!(mask_standalone_w("ww"), "\x01\x01");
        assert_eq!(mask_standalone_w("rww"), "r\x01\x01");
    }

    #[test]
    fn mixed_case_ww_after_eligible() {
        assert_eq!(mask_standalone_w("aWW"), "aWW");
        assert_eq!(mask_standalone_w("AWw"), "AWw");
    }
}

#[cfg(test)]
mod tracking_tests {
    use super::InputState;

    #[test]
    fn stop_tracking_disables_tracking() {
        let mut state = InputState::new();
        state.push('r');
        assert!(state.is_tracking());
        state.stop_tracking();
        assert!(!state.is_tracking());
        assert!(state.is_buffer_empty());
    }

    #[test]
    fn new_word_re_enables_tracking_after_stop() {
        let mut state = InputState::new();
        state.push('r');
        state.stop_tracking();
        assert!(!state.is_tracking());
        state.new_word();
        assert!(state.is_tracking());
    }

    #[test]
    fn pop_to_empty_then_new_word_re_enables_tracking() {
        // Simulates: type "raww" → stop_tracking → backspace to empty → new_word
        let mut state = InputState::new();
        state.push('r');
        state.push('a');
        state.push('w');
        state.push('w');
        state.stop_tracking(); // triggered by "ww" pattern
        assert!(!state.is_tracking());
        assert!(state.is_buffer_empty());

        // Backspaces clear the screen (handled by OS), buffer already empty.
        // Calling new_word() re-enables tracking for the next keystrokes.
        state.new_word();
        assert!(state.is_tracking());

        // New characters should be tracked
        state.push('o');
        state.push('o');
        assert_eq!(state.get_typing_buffer(), "oo");
    }

    #[test]
    fn resume_previous_word_re_enables_tracking() {
        let mut state = InputState::new();
        state.push('t');
        state.push('e');
        state.push('s');
        state.push('t');
        // Simulate end-of-word (space) → new_word + mark_resumable
        state.new_word();
        state.mark_resumable();
        assert!(state.is_buffer_empty());

        // Resume should restore the previous word
        assert!(state.try_resume_previous_word());
        assert!(state.is_tracking());
        assert_eq!(state.get_typing_buffer(), "test");
    }
}


================================================
FILE: src/main.rs
================================================
mod config;
mod hotkey;
mod input;
mod platform;
mod scripting;
mod ui;

use std::thread;

use druid::{AppLauncher, ExtEventSink, Target, WindowDesc};
use input::{
    get_diff_parts, rebuild_keyboard_layout_map, TypingMethod, HOTKEY_MATCHING,
    HOTKEY_MATCHING_CIRCUIT_BREAK, HOTKEY_MODIFIERS, INPUT_STATE,
};
use log::debug;
use once_cell::sync::OnceCell;
use platform::{
    add_app_change_callback, add_appearance_change_callback, dispatch_set_systray_title,
    ensure_accessibility_permission, run_event_listener, send_arrow_left, send_arrow_right,
    send_backspace, send_string, EventTapType, Handle, KeyModifier, PressedKey, KEY_DELETE,
    KEY_ENTER, KEY_ESCAPE, KEY_SPACE, KEY_TAB, RAW_ARROW_DOWN, RAW_ARROW_LEFT, RAW_ARROW_RIGHT,
    RAW_ARROW_UP, RAW_KEY_GLOBE,
};
use ui::{get_theme, UIDataAdapter, IS_DARK, THEME, UPDATE_UI};

static UI_EVENT_SINK: OnceCell<ExtEventSink> = OnceCell::new();
const APP_VERSION: &str = env!("CARGO_PKG_VERSION");

fn apply_capslock_to_output(output: String, is_capslock: bool) -> String {
    if is_capslock {
        output.to_uppercase()
    } else {
        output
    }
}

fn normalize_input_char(c: char, is_shift: bool) -> char {
    if is_shift {
        c.to_ascii_uppercase()
    } else {
        c
    }
}

fn do_transform_keys(handle: Handle, is_delete: bool, is_capslock: bool) -> bool {
    unsafe {
        if let Ok((raw_output, transform_result)) = INPUT_STATE.transform_keys() {
            let should_send_event = INPUT_STATE.should_send_keyboard_event(&raw_output);
            let output = apply_capslock_to_output(raw_output, is_capslock);
            debug!("Transformed: {:?}", output);
            if should_send_event || is_delete {
                // This is a workaround for Firefox-based browsers, where macOS's Accessibility API cannot work.
                // We cannot get the selected text in the address bar, so we will go with another
                // hacky way: Always send a space and delete it immediately. This will dismiss the
                // current pre-selected URL and fix the double character issue.
                if INPUT_STATE.should_dismiss_selection_if_needed() {
                    _ = send_string(handle, " ");
                    _ = send_backspace(handle, 1);
                }

                // Compute the minimal diff between what is currently displayed
                // and the new output.  Only delete and retype the diverging
                // suffix — the common prefix stays on screen untouched, which
                // eliminates flicker in Chromium/Electron apps (e.g. Messenger)
                // caused by a VSync frame landing between the backspace burst
                // and the reinsertion of the full word.
                //
                // Exception: when `is_delete` is true the caller wants the
                // entire word erased (e.g. the user pressed Delete/Backspace),
                // so we fall back to full-replace in that case.
                let (backspace_count, suffix_offset, screen_char_count) = if is_delete {
                    let bs = INPUT_STATE.get_backspace_count(is_delete);
                    (bs, 0usize, bs)
                } else {
                    // Clone the display buffer so we hold no borrow into INPUT_STATE
                    // while calling get_diff_parts, which borrows `output`.
                    let displaying = INPUT_STATE.get_displaying_word().to_owned();
                    // `push(c)` was called just before this function, appending the
                    // typed char to display_buffer.  That char has NOT yet appeared on
                    // screen because we are about to block the key event and replace it
                    // ourselves.  Strip it so `old` reflects the true on-screen state.
                    let screen_end = displaying
                        .char_indices()
                        .next_back()
                        .map(|(i, _)| i)
                        .unwrap_or(displaying.len());
                    let screen = &displaying[..screen_end];
                    let sc = screen.chars().count();
                    let (bs, sfx) = get_diff_parts(screen, &output);
                    let offset = output.len() - sfx.len();
                    (bs, offset, sc)
                };
                let suffix = &output[suffix_offset..];
                debug!("Backspace count: {}", backspace_count);

                // When the entire on-screen word would be erased (no common
                // prefix), Chromium/Electron apps fire an "empty value" event
                // that swallows subsequent keystrokes.  Avoid this by keeping
                // one sentinel char on screen: type the new text first, then
                // navigate back to delete the sentinel.
                let needs_sentinel =
                    !is_delete && backspace_count > 1 && backspace_count == screen_char_count;

                if needs_sentinel {
                    // Keep one old char as a sentinel so the field never
                    // empties (Chromium/Electron kill pending events on
                    // empty).  Build the new word left-to-right:
                    let first_char_end = suffix
                        .char_indices()
                        .nth(1)
                        .map(|(i, _)| i)
                        .unwrap_or(suffix.len());
                    let first_char = &suffix[..first_char_end];
                    let rest = &suffix[first_char_end..];
                    // 1. Delete all old chars except the last (sentinel)
                    _ = send_backspace(handle, backspace_count - 1);
                    // 2. Type the first char of the new output
                    _ = send_string(handle, first_char);
                    // 3. Move left behind the first char (before sentinel)
                    _ = send_arrow_left(handle, 1);
                    // 4. Delete the sentinel
                    _ = send_backspace(handle, 1);
                    // 5. Move right past the first char
                    _ = send_arrow_right(handle, 1);
                    // 6. Type the rest of the output
                    if !rest.is_empty() {
                        _ = send_string(handle, rest);
                    }
                } else {
                    _ = send_backspace(handle, backspace_count);
                    if !suffix.is_empty() {
                        _ = send_string(handle, suffix);
                    }
                }
                debug!("Sent suffix: {:?}", suffix);
                INPUT_STATE.replace(output);
                if transform_result.letter_modification_removed
                    || transform_result.tone_mark_removed
                {
                    INPUT_STATE.stop_tracking();
                }
                return true;
            }
        }
    }
    false
}

fn do_restore_word(handle: Handle, is_capslock: bool) {
    unsafe {
        let backspace_count = INPUT_STATE.get_backspace_count(true);
        debug!("Backspace count: {}", backspace_count);
        _ = send_backspace(handle, backspace_count);
        let typing_buffer = INPUT_STATE.get_typing_buffer();
        let output = apply_capslock_to_output(typing_buffer.to_owned(), is_capslock);
        _ = send_string(handle, &output);
        debug!("Sent: {:?}", output);
        INPUT_STATE.replace(output);
    }
}

fn should_restore_transformed_word(
    method: TypingMethod,
    typing_buffer: &str,
    display_buffer: &str,
    is_valid_word: bool,
    is_allowed_word: bool,
) -> bool {
    let is_transformed_word = typing_buffer != display_buffer;
    if !is_transformed_word || is_valid_word || is_allowed_word {
        return false;
    }

    // Keep VNI shorthand words (like d9m -> đm) when ending a word with space/tab/enter.
    let is_vni_numeric_shortcut =
        method == TypingMethod::VNI && typing_buffer.chars().any(|c| c.is_numeric());
    !is_vni_numeric_shortcut
}

fn do_macro_replace(handle: Handle, target: &String) {
    unsafe {
        let backspace_count = INPUT_STATE.get_backspace_count(true);
        debug!("Backspace count: {}", backspace_count);
        _ = send_backspace(handle, backspace_count);
        _ = send_string(handle, target);
        debug!("Sent: {:?}", target);
        INPUT_STATE.replace(target.to_owned());
    }
}

/// Compute the tray title from the current INPUT_STATE and dispatch it
/// directly to the main queue, so the status bar updates instantly.
pub unsafe fn update_systray_title_immediately() {
    let is_enabled = INPUT_STATE.is_enabled();
    let is_gox = INPUT_STATE.is_gox_mode_enabled();
    let title = if is_enabled {
        if is_gox {
            "gõ"
        } else {
            "VN"
        }
    } else if is_gox {
        match INPUT_STATE.get_method() {
            TypingMethod::Telex => "gox",
            TypingMethod::VNI => "go4",
            TypingMethod::TelexVNI => "go+",
        }
    } else {
        "EN"
    };
    dispatch_set_systray_title(title, is_enabled);
}

unsafe fn toggle_vietnamese() {
    INPUT_STATE.toggle_vietnamese();
    update_systray_title_immediately();
    if let Some(event_sink) = UI_EVENT_SINK.get() {
        if let Err(e) = event_sink.submit_command(UPDATE_UI, (), Target::Auto) {
            debug!("Failed to submit UPDATE_UI command: {:?}", e);
        }
    }
}

unsafe fn auto_toggle_vietnamese() {
    if !INPUT_STATE.is_auto_toggle_enabled() {
        return;
    }
    let has_change = INPUT_STATE.update_active_app().is_some();
    if !has_change {
        return;
    }
    update_systray_title_immediately();
    if let Some(event_sink) = UI_EVENT_SINK.get() {
        if let Err(e) = event_sink.submit_command(UPDATE_UI, (), Target::Auto) {
            debug!("Failed to submit UPDATE_UI command: {:?}", e);
        }
    }
}

fn event_handler(
    handle: Handle,
    event_type: EventTapType,
    pressed_key: Option<PressedKey>,
    modifiers: KeyModifier,
) -> bool {
    unsafe {
        let pressed_key_code = pressed_key.and_then(|p| match p {
            PressedKey::Char(c) => Some(c),
            _ => None,
        });

        if event_type == EventTapType::FlagsChanged {
            if modifiers.is_empty() {
                // Modifier keys are released
                if HOTKEY_MATCHING && !HOTKEY_MATCHING_CIRCUIT_BREAK {
                    toggle_vietnamese();
                }
                HOTKEY_MODIFIERS = KeyModifier::MODIFIER_NONE;
                HOTKEY_MATCHING = false;
                HOTKEY_MATCHING_CIRCUIT_BREAK = false;
            } else {
                HOTKEY_MODIFIERS.set(modifiers, true);
            }
        }

        let is_hotkey_matched = INPUT_STATE
            .get_hotkey()
            .is_match(HOTKEY_MODIFIERS, pressed_key_code);
        if HOTKEY_MATCHING && !is_hotkey_matched {
            HOTKEY_MATCHING_CIRCUIT_BREAK = true;
        }
        HOTKEY_MATCHING = is_hotkey_matched;

        // If the hotkey matched on a key press, toggle immediately and
        // suppress the event so macOS does not insert the character
        // (e.g. Option+Z → Ω).  Set HOTKEY_MATCHING_CIRCUIT_BREAK so
        // the FlagsChanged handler does not toggle again on key release.
        if is_hotkey_matched && pressed_key_code.is_some() {
            toggle_vietnamese();
            HOTKEY_MATCHING_CIRCUIT_BREAK = true;
            return true;
        }

        match pressed_key {
            Some(pressed_key) => {
                match pressed_key {
                    PressedKey::Raw(raw_keycode) => {
                        if raw_keycode == RAW_KEY_GLOBE {
                            toggle_vietnamese();
                            return true;
                        }
                        if raw_keycode == RAW_ARROW_UP || raw_keycode == RAW_ARROW_DOWN {
                            INPUT_STATE.new_word();
                        }
                        if raw_keycode == RAW_ARROW_LEFT || raw_keycode == RAW_ARROW_RIGHT {
                            // TODO: Implement a better cursor tracking on each word here
                            INPUT_STATE.new_word();
                        }
                    }
                    PressedKey::Char(keycode) => {
                        if INPUT_STATE.is_enabled() {
                            match keycode {
                                KEY_ENTER | KEY_TAB | KEY_SPACE | KEY_ESCAPE => {
                                    let typing_buffer = INPUT_STATE.get_typing_buffer();
                                    let display_word = INPUT_STATE.get_displaying_word();
                                    let is_valid_word = vi::validation::is_valid_word(display_word);
                                    let is_allowed_word = INPUT_STATE.is_allowed_word(display_word);
                                    if should_restore_transformed_word(
                                        INPUT_STATE.get_method(),
                                        typing_buffer,
                                        display_word,
                                        is_valid_word,
                                        is_allowed_word,
                                    ) {
                                        do_restore_word(handle, modifiers.is_capslock());
                                    }

                                    if INPUT_STATE.previous_word_is_stop_tracking_words() {
                                        INPUT_STATE.clear_previous_word();
                                    }

                                    if keycode == KEY_TAB || keycode == KEY_SPACE {
                                        if let Some(macro_target) = INPUT_STATE.get_macro_target() {
                                            debug!("Macro: {}", macro_target);
                                            do_macro_replace(handle, &macro_target)
                                        }
                                    }

                                    let had_content = !INPUT_STATE.is_buffer_empty();
                                    INPUT_STATE.new_word();
                                    if had_content && (keycode == KEY_SPACE || keycode == KEY_TAB) {
                                        INPUT_STATE.mark_resumable();
                                    }
                                }
                                KEY_DELETE => {
                                    if !modifiers.is_empty() && !modifiers.is_shift() {
                                        INPUT_STATE.new_word();
                                    } else if INPUT_STATE.is_buffer_empty() {
                                        // Buffer is empty — the user just started a new
                                        // word (e.g. after space).  Try to resume editing
                                        // the previous word so backspace + retype works.
                                        // If resume fails, reset to a fresh tracking state
                                        // so the next keystrokes are processed (e.g. after
                                        // stop_tracking from a duplicate pattern like "ww").
                                        if !INPUT_STATE.try_resume_previous_word() {
                                            INPUT_STATE.new_word();
                                        }
                                    } else {
                                        INPUT_STATE.pop();
                                        if !INPUT_STATE.is_buffer_empty() {
                                            return do_transform_keys(
                                                handle,
                                                true,
                                                modifiers.is_capslock(),
                                            );
                                        }
                                    }
                                }
                                c => {
                                    if "()[]{}<>/\\!@#$%^&*-_=+|~`,.;'\"/".contains(c)
                                        || (c.is_numeric() && modifiers.is_shift())
                                    {
                                        // If special characters detected, dismiss the current tracking word
                                        if c.is_numeric() {
                                            INPUT_STATE.push(c);
                                        }
                                        INPUT_STATE.new_word();
                                    } else {
                                        // Otherwise, process the character
                                        if modifiers.is_super() || modifiers.is_alt() {
                                            INPUT_STATE.new_word();
                                        } else if INPUT_STATE.is_tracking() {
                                            INPUT_STATE.push(normalize_input_char(
                                                c,
                                                modifiers.is_shift(),
                                            ));
                                            let ret = do_transform_keys(
                                                handle,
                                                false,
                                                modifiers.is_capslock(),
                                            );
                                            INPUT_STATE.stop_tracking_if_needed();
                                            return ret;
                                        }
                                    }
                                }
                            }
                        } else {
                            match keycode {
                                KEY_ENTER | KEY_TAB | KEY_SPACE | KEY_ESCAPE => {
                                    INPUT_STATE.new_word();
                                }
                                _ => {
                                    if !modifiers.is_empty() {
                                        INPUT_STATE.new_word();
                                    }
                                }
                            }
                        }
                    }
                };
            }
            None => {
                let previous_modifiers = INPUT_STATE.get_previous_modifiers();
                if previous_modifiers.is_empty() {
                    if modifiers.is_control() {
                        if !INPUT_STATE.get_typing_buffer().is_empty() {
                            do_restore_word(handle, modifiers.is_capslock());
                        }
                        INPUT_STATE.set_temporary_disabled();
                    }
                    if modifiers.is_super() || event_type == EventTapType::Other {
                        INPUT_STATE.new_word();
                    }
                }
            }
        }
        INPUT_STATE.save_previous_modifiers(modifiers);
    }
    false
}

#[cfg(test)]
mod tests {
    use super::{apply_capslock_to_output, normalize_input_char, should_restore_transformed_word};
    use crate::input::TypingMethod;

    #[test]
    fn restore_when_invalid_and_not_allowed() {
        let should_restore =
            should_restore_transformed_word(TypingMethod::Telex, "maaa", "màa", false, false);
        assert!(should_restore);
    }

    #[test]
    fn no_restore_for_valid_word() {
        let should_restore =
            should_restore_transformed_word(TypingMethod::Telex, "tieens", "tiến", true, false);
        assert!(!should_restore);
    }

    #[test]
    fn no_restore_for_allowed_word() {
        let should_restore =
            should_restore_transformed_word(TypingMethod::Telex, "ddc", "đc", false, true);
        assert!(!should_restore);
    }

    #[test]
    fn no_restore_for_vni_numeric_shorthand() {
        let should_restore =
            should_restore_transformed_word(TypingMethod::VNI, "d9m", "đm", false, false);
        assert!(!should_restore);
    }

    #[test]
    fn restore_for_vni_invalid_without_numeric_shorthand() {
        let should_restore =
            should_restore_transformed_word(TypingMethod::VNI, "dam", "đm", false, false);
        assert!(should_restore);
    }

    #[test]
    fn normalize_input_char_only_depends_on_shift() {
        assert_eq!(normalize_input_char('d', true), 'D');
        assert_eq!(normalize_input_char('d', false), 'd');
    }

    #[test]
    fn apply_capslock_to_transformed_output() {
        let lower = String::from("duyệt");
        assert_eq!(apply_capslock_to_output(lower.clone(), false), "duyệt");
        assert_eq!(apply_capslock_to_output(lower, true), "DUYỆT");
    }

    #[test]
    fn capslock_path_keeps_telex_tone_position() {
        let mut transformed = String::new();
        vi::telex::transform_buffer("duyeetj".chars(), &mut transformed);

        assert_eq!(apply_capslock_to_output(transformed, true), "DUYỆT");
    }

    #[test]
    fn no_send_needed_for_plain_letter_with_capslock_only_case_change() {
        // For plain letters with Caps Lock, OS already inserts uppercase characters.
        // We should not treat case-only difference as a transform event.
        let mut transformed = String::new();
        vi::telex::transform_buffer("z".chars(), &mut transformed);
        assert_eq!(transformed, "z");
    }
}

fn main() {
    let app_title = format!("gõkey v{APP_VERSION}");
    env_logger::init();
    {
        let config = crate::config::CONFIG_MANAGER.lock().unwrap();
        ui::locale::init_lang(config.get_ui_language());
    }
    let skip_permission = std::env::args().any(|a| a == "--skip-permission");
    if !skip_permission && !ensure_accessibility_permission() {
        // Show the Accessibility Permission Request screen
        let win = WindowDesc::new(ui::permission_request_ui_builder())
            .title(app_title)
            .window_size((500.0, 360.0))
            .resizable(false);
        let app = AppLauncher::with_window(win);
        _ = app.launch(());
    } else {
        // Start the GõKey application
        rebuild_keyboard_layout_map();
        let win = WindowDesc::new(ui::main_ui_builder())
            .title(app_title)
            .window_size((ui::WINDOW_WIDTH, ui::WINDOW_HEIGHT))
            .set_position(ui::center_window_position())
            .set_always_on_top(true)
            .resizable(false);
        let app = AppLauncher::with_window(win);
        let event_sink = app.get_external_handle();
        _ = UI_EVENT_SINK.set(event_sink);
        thread::spawn(|| {
            run_event_listener(&event_handler);
        });
        add_app_change_callback(|| {
            unsafe { auto_toggle_vietnamese() };
        });
        add_appearance_change_callback(|| {
            if let Some(sink) = UI_EVENT_SINK.get() {
                _ = sink.submit_command(UPDATE_UI, (), Target::Auto);
            }
        });
        _ = app
            .configure_env(|env: &mut druid::Env, data: &UIDataAdapter| {
                env.set(THEME.clone(), std::sync::Arc::new(get_theme(data.is_dark)));
                env.set(IS_DARK.clone(), data.is_dark);
            })
            .launch(UIDataAdapter::new());
    }
}


================================================
FILE: src/platform/linux.rs
================================================
// TODO: Implement this

use druid::{commands::CLOSE_WINDOW, Selector};

use super::CallbackFn;

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

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

pub fn send_backspace(count: usize) -> Result<(), ()> {
    todo!()
}

pub fn send_string(string: &str) -> Result<(), ()> {
    todo!()
}

pub fn run_event_listener(callback: &CallbackFn) {
    todo!()
}

pub fn ensure_accessibility_permission() -> bool {
    true
}

pub fn is_in_text_selection() -> bool {
    todo!()
}

pub fn update_launch_on_login(is_enable: bool) {
    todo!()
}

pub fn is_launch_on_login() {
    todo!()
}


================================================
FILE: src/platform/macos.rs
================================================
use std::env::current_exe;
use std::path::Path;
use std::{env, path::PathBuf, ptr};

mod macos_ext;
use auto_launch::{AutoLaunch, AutoLaunchBuilder};
use cocoa::base::id;
use cocoa::{
    base::{nil, BOOL, YES},
    foundation::NSDictionary,
};
use core_graphics::{
    event::{
        CGEventFlags, CGEventTapLocation, CGEventTapOptions, CGEventTapPlacement, CGEventType,
        CGKeyCode, EventField, KeyCode,
    },
    sys,
};
use objc::{class, msg_send, sel, sel_impl};

pub use macos_ext::defer_open_app_file_picker;
pub use macos_ext::defer_open_text_file_picker;
pub use macos_ext::defer_save_text_file_picker;
pub use macos_ext::dispatch_set_systray_title;
pub use macos_ext::SystemTray;
pub use macos_ext::SystemTrayMenuItemKey;
use once_cell::sync::Lazy;

use crate::input::KEYBOARD_LAYOUT_CHARACTER_MAP;
use accessibility::{AXAttribute, AXUIElement};
use accessibility_sys::{kAXFocusedUIElementAttribute, kAXSelectedTextAttribute};
use core_foundation::{
    runloop::{kCFRunLoopCommonModes, CFRunLoop},
    string::CFString,
};

pub use self::macos_ext::Handle;
use self::macos_ext::{
    kAXTrustedCheckOptionPrompt, new_tap, AXIsProcessTrustedWithOptions,
    CGEventCreateKeyboardEvent, CGEventKeyboardSetUnicodeString, CGEventTapPostEvent,
};

use super::{
    CallbackFn, EventTapType, KeyModifier, PressedKey, KEY_DELETE, KEY_ENTER, KEY_ESCAPE,
    KEY_SPACE, KEY_TAB,
};

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

impl From<CGEventType> for EventTapType {
    fn from(value: CGEventType) -> Self {
        match value {
            CGEventType::KeyDown => EventTapType::KeyDown,
            CGEventType::FlagsChanged => EventTapType::FlagsChanged,
            _ => EventTapType::Other,
        }
    }
}

static AUTO_LAUNCH: Lazy<AutoLaunch> = Lazy::new(|| {
    let app_path = get_current_app_path();
    let app_name = Path::new(&app_path)
        .file_stem()
        .and_then(|f| f.to_str())
        .unwrap();
    AutoLaunchBuilder::new()
        .set_app_name(app_name)
        .set_app_path(&app_path)
        .build()
        .unwrap()
});

/// On macOS, current_exe gives path to /Applications/Example.app/MacOS/Example but this results in seeing a Unix Executable in macOS login items. It must be: /Applications/Example.app
/// If it didn't find exactly a single occurrence of .app, it will default to exe path to not break it.
fn get_current_app_path() -> String {
    let current_exe = current_exe().unwrap();
    let exe_path = current_exe.canonicalize().unwrap().display().to_string();
    let parts: Vec<&str> = exe_path.split(".app/").collect();
    return if parts.len() == 2 {
        format!("{}.app", parts.get(0).unwrap().to_string())
    } else {
        exe_path
    };
}

#[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)
}

// List of keycode: https://eastmanreference.com/complete-list-of-applescript-key-codes
fn get_char(keycode: CGKeyCode) -> Option<PressedKey> {
    if let Some(key_map) = unsafe { KEYBOARD_LAYOUT_CHARACTER_MAP.get() } {
        return match keycode {
            0 => Some(PressedKey::Char(key_map[&'a'])),
            1 => Some(PressedKey::Char(key_map[&'s'])),
            2 => Some(PressedKey::Char(key_map[&'d'])),
            3 => Some(PressedKey::Char(key_map[&'f'])),
            4 => Some(PressedKey::Char(key_map[&'h'])),
            5 => Some(PressedKey::Char(key_map[&'g'])),
            6 => Some(PressedKey::Char(key_map[&'z'])),
            7 => Some(PressedKey::Char(key_map[&'x'])),
            8 => Some(PressedKey::Char(key_map[&'c'])),
            9 => Some(PressedKey::Char(key_map[&'v'])),
            11 => Some(PressedKey::Char(key_map[&'b'])),
            12 => Some(PressedKey::Char(key_map[&'q'])),
            13 => Some(PressedKey::Char(key_map[&'w'])),
            14 => Some(PressedKey::Char(key_map[&'e'])),
            15 => Some(PressedKey::Char(key_map[&'r'])),
            16 => Some(PressedKey::Char(key_map[&'y'])),
            17 => Some(PressedKey::Char(key_map[&'t'])),
            31 => Some(PressedKey::Char(key_map[&'o'])),
            32 => Some(PressedKey::Char(key_map[&'u'])),
            34 => Some(PressedKey::Char(key_map[&'i'])),
            35 => Some(PressedKey::Char(key_map[&'p'])),
            37 => Some(PressedKey::Char(key_map[&'l'])),
            38 => Some(PressedKey::Char(key_map[&'j'])),
            40 => Some(PressedKey::Char(key_map[&'k'])),
            45 => Some(PressedKey::Char(key_map[&'n'])),
            46 => Some(PressedKey::Char(key_map[&'m'])),
            18 => Some(PressedKey::Char(key_map[&'1'])),
            19 => Some(PressedKey::Char(key_map[&'2'])),
            20 => Some(PressedKey::Char(key_map[&'3'])),
            21 => Some(PressedKey::Char(key_map[&'4'])),
            22 => Some(PressedKey::Char(key_map[&'6'])),
            23 => Some(PressedKey::Char(key_map[&'5'])),
            25 => Some(PressedKey::Char(key_map[&'9'])),
            26 => Some(PressedKey::Char(key_map[&'7'])),
            28 => Some(PressedKey::Char(key_map[&'8'])),
            29 => Some(PressedKey::Char(key_map[&'0'])),
            27 => Some(PressedKey::Char(key_map[&'-'])),
            33 => Some(PressedKey::Char(key_map[&'['])),
            30 => Some(PressedKey::Char(key_map[&']'])),
            41 => Some(PressedKey::Char(key_map[&';'])),
            43 => Some(PressedKey::Char(key_map[&','])),
            24 => Some(PressedKey::Char(key_map[&'='])),
            42 => Some(PressedKey::Char(key_map[&'\\'])),
            44 => Some(PressedKey::Char(key_map[&'/'])),
            39 => Some(PressedKey::Char(key_map[&'\''])),
            47 => Some(PressedKey::Char(key_map[&'.'])),
            36 | 52 => Some(PressedKey::Char(KEY_ENTER)), // ENTER
            49 => Some(PressedKey::Char(KEY_SPACE)),      // SPACE
            48 => Some(PressedKey::Char(KEY_TAB)),        // TAB
            51 => Some(PressedKey::Char(KEY_DELETE)),     // DELETE
            53 => Some(PressedKey::Char(KEY_ESCAPE)),     // ESC
            _ => Some(PressedKey::Raw(keycode)),
        };
    }
    None
}

pub fn is_in_text_selection() -> bool {
    let system_element = AXUIElement::system_wide();
    let Some(selected_element) = system_element
        .attribute(&AXAttribute::new(&CFString::from_static_string(
            kAXFocusedUIElementAttribute,
        )))
        .map(|elemenet| elemenet.downcast_into::<AXUIElement>())
        .ok()
        .flatten()
    else {
        return false;
    };
    let Some(selected_text) = selected_element
        .attribute(&AXAttribute::new(&CFString::from_static_string(
            kAXSelectedTextAttribute,
        )))
        .map(|text| text.downcast_into::<CFString>())
        .ok()
        .flatten()
    else {
        return false;
    };
    !selected_text.to_string().is_empty()
}

pub fn send_backspace(handle: Handle, count: usize) -> Result<(), ()> {
    let null_event_source = ptr::null_mut() as *mut sys::CGEventSource;
    let (event_bs_down, event_bs_up) = unsafe {
        (
            CGEventCreateKeyboardEvent(null_event_source, KeyCode::DELETE, true),
            CGEventCreateKeyboardEvent(null_event_source, KeyCode::DELETE, false),
        )
    };
    for _ in 0..count {
        unsafe {
            CGEventTapPostEvent(handle, event_bs_down);
            CGEventTapPostEvent(handle, event_bs_up);
        }
    }
    Ok(())
}

pub fn send_arrow_left(handle: Handle, count: usize) -> Result<(), ()> {
    let null_event_source = ptr::null_mut() as *mut sys::CGEventSource;
    let (down, up) = unsafe {
        (
            CGEventCreateKeyboardEvent(null_event_source, super::RAW_ARROW_LEFT as CGKeyCode, true),
            CGEventCreateKeyboardEvent(
                null_event_source,
                super::RAW_ARROW_LEFT as CGKeyCode,
                false,
            ),
        )
    };
    for _ in 0..count {
        unsafe {
            CGEventTapPostEvent(handle, down);
            CGEventTapPostEvent(handle, up);
        }
    }
    Ok(())
}

pub fn send_arrow_right(handle: Handle, count: usize) -> Result<(), ()> {
    let null_event_source = ptr::null_mut() as *mut sys::CGEventSource;
    let (down, up) = unsafe {
        (
            CGEventCreateKeyboardEvent(
                null_event_source,
                super::RAW_ARROW_RIGHT as CGKeyCode,
                true,
            ),
            CGEventCreateKeyboardEvent(
                null_event_source,
                super::RAW_ARROW_RIGHT as CGKeyCode,
                false,
            ),
        )
    };
    for _ in 0..count {
        unsafe {
            CGEventTapPostEvent(handle, down);
            CGEventTapPostEvent(handle, up);
        }
    }
    Ok(())
}

pub fn send_string(handle: Handle, string: &str) -> Result<(), ()> {
    let utf_16_str: Vec<u16> = string.encode_utf16().collect();
    let null_event_source = ptr::null_mut() as *mut sys::CGEventSource;

    unsafe {
        let event_str = CGEventCreateKeyboardEvent(null_event_source, 0, true);
        let buflen = utf_16_str.len() as libc::c_ulong;
        let bufptr = utf_16_str.as_ptr();
        CGEventKeyboardSetUnicodeString(event_str, buflen, bufptr);
        CGEventTapPostEvent(handle, event_str);
    }
    Ok(())
}

pub fn add_app_change_callback<F>(cb: F)
where
    F: Fn() + Send + 'static,
{
    macos_ext::add_app_change_callback(cb);
}

pub fn add_appearance_change_callback<F>(cb: F)
where
    F: Fn() + Send + 'static,
{
    macos_ext::add_appearance_change_callback(cb);
}

pub fn run_event_listener(callback: &CallbackFn) {
    let current = CFRunLoop::get_current();
    if let Ok(event_tap) = new_tap::CGEventTap::new(
        CGEventTapLocation::HID,
        CGEventTapPlacement::HeadInsertEventTap,
        CGEventTapOptions::Default,
        vec![
            CGEventType::KeyDown,
            CGEventType::RightMouseDown,
            CGEventType::LeftMouseDown,
            CGEventType::OtherMouseDown,
            CGEventType::FlagsChanged,
        ],
        |proxy, _, event| {
            if !is_process_trusted() {
                eprintln!("Accessibility access removed!");
                std::process::exit(1);
            }

            let mut modifiers = KeyModifier::new();
            let flags = event.get_flags();
            if flags.contains(CGEventFlags::CGEventFlagShift) {
                modifiers.add_shift();
            }
            if flags.contains(CGEventFlags::CGEventFlagAlphaShift) {
                modifiers.add_capslock();
            }
            if flags.contains(CGEventFlags::CGEventFlagControl) {
                modifiers.add_control();
            }
            if flags.contains(CGEventFlags::CGEventFlagCommand) {
                modifiers.add_super();
            }
            if flags.contains(CGEventFlags::CGEventFlagAlternate) {
                modifiers.add_alt();
            }
            if flags.eq(&CGEventFlags::CGEventFlagNonCoalesced)
                || flags.eq(&CGEventFlags::CGEventFlagNull)
            {
                modifiers = KeyModifier::MODIFIER_NONE;
            }

            let event_tap_type: EventTapType = EventTapType::from(event.get_type());
            match event_tap_type {
                EventTapType::KeyDown => {
                    let source_state_id =
                        event.get_integer_value_field(EventField::EVENT_SOURCE_STATE_ID);
                    if source_state_id == 1 {
                        let key_code = event
                            .get_integer_value_field(EventField::KEYBOARD_EVENT_KEYCODE)
                            as CGKeyCode;

                        if callback(proxy, event_tap_type, get_char(key_code), modifiers) {
                            // block the key if already processed
                            return None;
                        }
                    }
                }
                EventTapType::FlagsChanged => {
                    callback(proxy, event_tap_type, None, modifiers);
                }
                _ => {
                    callback(proxy, event_tap_type, None, KeyModifier::new());
                }
            }
            Some(event.to_owned())
        },
    ) {
        unsafe {
            let loop_source = event_tap.mach_port.create_runloop_source(0).expect("Cannot start event tap. Make sure you have granted Accessibility Access for the application.");
            current.add_source(&loop_source, kCFRunLoopCommonModes);
            event_tap.enable();
            CFRunLoop::run_current();
        }
    }
}

pub fn is_process_trusted() -> bool {
    unsafe { accessibility_sys::AXIsProcessTrusted() }
}

pub fn ensure_accessibility_permission() -> bool {
    unsafe {
        let options = NSDictionary::dictionaryWithObject_forKey_(
            nil,
            msg_send![class!(NSNumber), numberWithBool: YES],
            kAXTrustedCheckOptionPrompt as _,
        );
        return AXIsProcessTrustedWithOptions(options as _);
    }
}

/// Return the RGBA pixel data (pre-multiplied) and dimensions for the icon of
/// the application at `app_path` (e.g. "/Applications/Safari.app").
/// The icon is rendered at `size`×`size` points.  Returns `None` on failure.
pub fn get_app_icon_rgba(app_path: &str, size: u32) -> Option<(Vec<u8>, u32, u32)> {
    unsafe {
        use cocoa::base::nil;
        use cocoa::foundation::NSString;

        let workspace: id = msg_send![class!(NSWorkspace), sharedWorkspace];
        let path_ns = NSString::alloc(nil).init_str(app_path);
        let icon: id = msg_send![workspace, iconForFile: path_ns];
        if icon.is_null() {
            return None;
        }

        let ns_size: cocoa::foundation::NSSize =
            cocoa::foundation::NSSize::new(size as f64, size as f64);
        let _: () = msg_send![icon, setSize: ns_size];

        // Create an NSBitmapImageRep to rasterize into RGBA
        let rep: id = msg_send![class!(NSBitmapImageRep), alloc];
        let planes: *mut u8 = std::ptr::null_mut();
        let color_space_name = NSString::alloc(nil).init_str("NSCalibratedRGBColorSpace");
        let rep: id = msg_send![rep,
            initWithBitmapDataPlanes: &planes
            pixelsWide: size as i64
            pixelsHigh: size as i64
            bitsPerSample: 8_i64
            samplesPerPixel: 4_i64
            hasAlpha: YES
            isPlanar: cocoa::base::NO
            colorSpaceName: color_space_name
            bytesPerRow: (size * 4) as i64
            bitsPerPixel: 32_i64
        ];
        if rep.is_null() {
            return None;
        }

        // Draw the icon into the bitmap context
        let _: () = msg_send![class!(NSGraphicsContext), saveGraphicsState];
        let gfx_ctx: id =
            msg_send![class!(NSGraphicsContext), graphicsContextWithBitmapImageRep: rep];
        let _: () = msg_send![class!(NSGraphicsContext), setCurrentContext: gfx_ctx];

        let draw_rect =
            cocoa::foundation::NSRect::new(cocoa::foundation::NSPoint::new(0.0, 0.0), ns_size);
        let from_rect = cocoa::foundation::NSRect::new(
            cocoa::foundation::NSPoint::new(0.0, 0.0),
            cocoa::foundation::NSSize::new(0.0, 0.0), // zero = entire image
        );
        let _: () = msg_send![icon,
            drawInRect: draw_rect
            fromRect: from_rect
            operation: 2_i64  // NSCompositingOperationSourceOver
            fraction: 1.0_f64
        ];
        let _: () = msg_send![class!(NSGraphicsContext), restoreGraphicsState];

        // Extract pixel data
        let bitmap_data: *const u8 = msg_send![rep, bitmapData];
        if bitmap_data.is_null() {
            let _: () = msg_send![rep, release];
            return None;
        }
        let len = (size * size * 4) as usize;
        let pixels = std::slice::from_raw_parts(bitmap_data, len).to_vec();
        let _: () = msg_send![rep, release];

        Some((pixels, size, size))
    }
}

/// Return the user's preferred language code (e.g. "vi", "en", "ja").
pub fn get_preferred_language() -> String {
    unsafe {
        let langs: id = msg_send![class!(NSLocale), preferredLanguages];
        let first: id = msg_send![langs, firstObject];
        if first.is_null() {
            return "en".to_string();
        }
        nsstring_to_string!(first).unwrap_or_else(|| "en".to_string())
    }
}

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())
    }
}

pub fn update_launch_on_login(is_enable: bool) -> Result<(), auto_launch::Error> {
    match is_enable {
        true => AUTO_LAUNCH.enable(),
        false => AUTO_LAUNCH.disable(),
    }
}

pub fn is_launch_on_login() -> bool {
    AUTO_LAUNCH.is_enabled().unwrap()
}

pub fn is_dark_mode() -> bool {
    unsafe {
        use cocoa::base::nil;
        use cocoa::foundation::NSString;
        let app: id = msg_send![class!(NSApplication), sharedApplication];
        let appearance: id = msg_send![app, effectiveAppearance];
        let name: id = msg_send![appearance, name];
        let dark_aqua = NSString::alloc(nil).init_str("NSAppearanceNameDarkAqua");
        let is_dark: BOOL = msg_send![name, isEqual: dark_aqua];
        is_dark == YES
    }
}


================================================
FILE: src/platform/macos_ext.rs
================================================
use cocoa::appkit::{
    NSApp, NSApplication, NSButton, NSMenu, NSMenuItem, NSStatusBar, NSStatusItem,
};
use cocoa::base::{id, nil, BOOL, NO, YES};
use cocoa::foundation::{NSAutoreleasePool, NSString};
use core_foundation::dictionary::CFDictionaryRef;
use core_foundation::string::CFStringRef;
use core_graphics::{
    event::{CGEventTapProxy, CGKeyCode},
    sys,
};
use druid::{Data, Lens};
use libc::c_void;
use objc::{
    class,
    declare::ClassDecl,
    msg_send,
    runtime::{Class, Object, Sel},
    sel, sel_impl, Message,
};
use objc_foundation::{INSObject, NSObject};
use objc_id::Id;
use once_cell::sync::OnceCell;
use std::mem;

/// Global reference to the NSStatusItem pointer so we can update the tray
/// title directly from any thread (via dispatch_async to the main queue),
/// bypassing Druid's event loop which can lag ~1 s when the window is hidden.
static SYSTRAY_ITEM: OnceCell<usize> = OnceCell::new();

#[derive(Clone, PartialEq, Eq)]
struct Wrapper(*mut objc::runtime::Object);
impl Data for Wrapper {
    fn same(&self, _other: &Self) -> bool {
        true
    }
}

pub enum SystemTrayMenuItemKey {
    ShowUI,
    Enable,
    TypingMethodTelex,
    TypingMethodVNI,
    TypingMethodTelexVNI,
    Exit,
}

#[derive(Clone, Data, Lens, PartialEq, Eq)]
pub struct SystemTray {
    _pool: Wrapper,
    menu: Wrapper,
    item: Wrapper,
}

impl SystemTray {
    pub fn new() -> Self {
        unsafe {
            let pool = NSAutoreleasePool::new(nil);
            let menu = NSMenu::new(nil).autorelease();

            let app = NSApp();
            app.activateIgnoringOtherApps_(YES);
            let item = NSStatusBar::systemStatusBar(nil).statusItemWithLength_(-1.0);
            let button: id = msg_send![item, button];
            let image = create_badge_image("VN", true);
            let _: () = msg_send![button, setImage: image];
            item.setMenu_(menu);

            // Store the raw pointer globally so dispatch_set_systray_title
            // can update the title without going through Druid's event loop.
            let _ = SYSTRAY_ITEM.set(item as usize);

            let s = Self {
                _pool: Wrapper(pool),
                menu: Wrapper(menu),
                item: Wrapper(item),
            };
            s.init_menu_items();
            s
        }
    }

    pub fn set_title(&mut self, title: &str, is_vietnamese: bool) {
        unsafe {
            let button: id = msg_send![self.item.0, button];
            let image = create_badge_image(title, is_vietnamese);
            let _: () = msg_send![button, setImage: image];
            let empty = NSString::alloc(nil).init_str("");
            let _: () = msg_send![button, setTitle: empty];
            let _: () = msg_send![empty, release];
        }
    }

    pub fn init_menu_items(&self) {
        use crate::ui::locale::t;
        self.add_menu_item(t("menu.open_panel"), || ());
        self.add_menu_separator();
        self.add_menu_item(t("menu.disable_vietnamese"), || ());
        self.add_menu_separator();
        self.add_menu_item("Telex ✓", || ());
        self.add_menu_item("VNI", || ());
        self.add_menu_item("Telex+VNI", || ());
        self.add_menu_separator();
        self.add_menu_item(t("menu.quit"), || ());
    }

    pub fn add_menu_separator(&self) {
        unsafe {
            NSMenu::addItem_(self.menu.0, NSMenuItem::separatorItem(nil));
        }
    }

    pub fn add_menu_item<F>(&self, label: &str, cb: F)
    where
        F: Fn() + Send + 'static,
    {
        let cb_obj = Callback::from(Box::new(cb));

        unsafe {
            let no_key = NSString::alloc(nil).init_str("");
            let itemtitle = NSString::alloc(nil).init_str(label);
            let action = sel!(call);
            let item = NSMenuItem::alloc(nil)
                .initWithTitle_action_keyEquivalent_(itemtitle, action, no_key);
            let _: () = msg_send![item, setTarget: cb_obj];

            NSMenu::addItem_(self.menu.0, item);
        }
    }

    pub fn get_menu_item_index_by_key(&self, key: SystemTrayMenuItemKey) -> i64 {
        match key {
            SystemTrayMenuItemKey::ShowUI => 0,
            SystemTrayMenuItemKey::Enable => 2,
            SystemTrayMenuItemKey::TypingMethodTelex => 4,
            SystemTrayMenuItemKey::TypingMethodVNI => 5,
            SystemTrayMenuItemKey::TypingMethodTelexVNI => 6,
            SystemTrayMenuItemKey::Exit => 8,
        }
    }

    pub fn set_menu_item_title(&self, key: SystemTrayMenuItemKey, label: &str) {
        unsafe {
            let item_title = NSString::alloc(nil).init_str(label);
            let index = self.get_menu_item_index_by_key(key);
            let menu_item: id = msg_send![self.menu.0, itemAtIndex: index];
            let _: () = msg_send![menu_item, setTitle: item_title];
            let _: () = msg_send![item_title, release];
        }
    }

    pub fn set_menu_item_callback<F>(&self, key: SystemTrayMenuItemKey, cb: F)
    where
        F: Fn() + Send + 'static,
    {
        let cb_obj = Callback::from(Box::new(cb));
        unsafe {
            let index = self.get_menu_item_index_by_key(key);
            let _: () = msg_send![self.menu.0.itemAtIndex_(index), setTarget: cb_obj];
        }
    }
}

/// Create an NSImage with an outlined rounded rectangle and text.
/// Rendered as a template image so macOS tints it automatically like other menu bar icons.
unsafe fn create_badge_image(title: &str, _is_vietnamese: bool) -> id {
    use cocoa::foundation::{NSPoint, NSRect, NSSize};

    let black: id = msg_send![class!(NSColor), blackColor];

    // Measure text to determine badge width
    let font: id = msg_send![class!(NSFont), systemFontOfSize: 9.5_f64 weight: 0.4_f64];
    let title_ns = NSString::alloc(nil).init_str(title);

    let font_key = NSString::alloc(nil).init_str("NSFont");
    let color_key = NSString::alloc(nil).init_str("NSColor");
    let keys: [id; 2] = [font_key, color_key];
    let vals: [id; 2] = [font, black];
    let attrs: id = msg_send![class!(NSDictionary), dictionaryWithObjects:vals.as_ptr() forKeys:keys.as_ptr() count:2_u64];
    let attr_str: id = msg_send![class!(NSAttributedString), alloc];
    let attr_str: id = msg_send![attr_str, initWithString:title_ns attributes:attrs];
    let text_size: NSSize = msg_send![attr_str, size];

    let padding_h = 4.0_f64;
    let padding_v = 3.5_f64;
    let natural_w = (text_size.width + padding_h * 2.0).ceil();
    let badge_w = natural_w.max(24.0);
    let badge_h = (text_size.height + padding_v * 2.0).ceil();
    let corner_radius = 4.0_f64;
    let border_width = 1.2_f64;

    // Draw at 2x resolution for Retina crispness
    let scale = 2.0_f64;
    let px_w = badge_w * scale;
    let px_h = badge_h * scale;

    let color_space_name = NSString::alloc(nil).init_str("NSCalibratedRGBColorSpace");
    let rep: id = msg_send![class!(NSBitmapImageRep), alloc];
    let planes: *mut u8 = std::ptr::null_mut();
    let rep: id = msg_send![rep,
        initWithBitmapDataPlanes: &planes
        pixelsWide: px_w as i64
        pixelsHigh: px_h as i64
        bitsPerSample: 8_i64
        samplesPerPixel: 4_i64
        hasAlpha: YES
        isPlanar: NO
        colorSpaceName: color_space_name
        bytesPerRow: 0_i64
        bitsPerPixel: 0_i64
    ];

    // Draw into the bitmap rep at 2x
    let _: () = msg_send![class!(NSGraphicsContext), saveGraphicsState];
    let gfx_ctx: id = msg_send![class!(NSGraphicsContext), graphicsContextWithBitmapImageRep: rep];
    let _: () = msg_send![class!(NSGraphicsContext), setCurrentContext: gfx_ctx];

    // Scale the graphics context so we draw in logical points
    let xform: id = msg_send![class!(NSAffineTransform), transform];
    let _: () = msg_send![xform, scaleBy: scale];
    let _: () = msg_send![xform, concat];

    // Draw rounded rect border only (no fill)
    let inset = border_width / 2.0;
    let rect = NSRect::new(
        NSPoint::new(inset, inset),
        NSSize::new(badge_w - border_width, badge_h - border_width),
    );
    let path: id = msg_send![class!(NSBezierPath), bezierPathWithRoundedRect:rect xRadius:corner_radius yRadius:corner_radius];
    let _: () = msg_send![black, setStroke];
    let _: () = msg_send![path, setLineWidth: border_width];
    let _: () = msg_send![path, stroke];

    // Draw centered text
    let text_x = (badge_w - text_size.width) / 2.0;
    let text_y = (badge_h - text_size.height) / 2.0;
    let _: () = msg_send![attr_str, drawAtPoint: NSPoint::new(text_x, text_y)];

    let _: () = msg_send![class!(NSGraphicsContext), restoreGraphicsState];

    // Create image from the bitmap rep with logical (1x) size
    let img_size = NSSize::new(badge_w, badge_h);
    let image: id = msg_send![class!(NSImage), alloc];
    let image: id = msg_send![image, initWithSize: img_size];
    let _: () = msg_send![image, addRepresentation: rep];
    let _: () = msg_send![image, setTemplate: YES];
    let _: () = msg_send![attr_str, release];

    image
}

/// Update the system tray title immediately by dispatching to the main queue.
/// This bypasses Druid's event loop, which can be slow when the window is hidden.
/// Safe to call from any thread.
pub fn dispatch_set_systray_title(title: &str, is_vietnamese: bool) {
    let Some(&item_ptr) = SYSTRAY_ITEM.get() else {
        return;
    };
    let title_owned = title.to_owned();

    struct Context {
        item: usize,
        title: String,
        is_vietnamese: bool,
    }

    unsafe extern "C" fn work(ctx: *mut c_void) {
        let ctx = Box::from_raw(ctx as *mut Context);
        let item = ctx.item as id;
        let button: id = msg_send![item, button];
        let image = create_badge_image(&ctx.title, ctx.is_vietnamese);
        let _: () = msg_send![button, setImage: image];
        let empty = NSString::alloc(nil).init_str("");
        let _: () = msg_send![button, setTitle: empty];
        let _: () = msg_send![empty, release];
    }

    let ctx = Box::new(Context {
        item: item_ptr,
        title: title_owned,
        is_vietnamese: is_vietnamese,
    });
    let ctx_ptr = Box::into_raw(ctx) as *mut c_void;

    unsafe {
        dispatch_async_f(&_dispatch_main_q, ctx_ptr, work);
    }
}

pub type Handle = CGEventTapProxy;

#[link(name = "CoreGraphics", kind = "framework")]
extern "C" {
    pub(crate) fn CGEventTapPostEvent(proxy: CGEventTapProxy, event: sys::CGEventRef);
    pub(crate) fn CGEventCreateKeyboardEvent(
        source: sys::CGEventSourceRef,
        keycode: CGKeyCode,
        keydown: bool,
    ) -> sys::CGEventRef;
    pub(crate) fn CGEventKeyboardSetUnicodeString(
        event: sys::CGEventRef,
        length: libc::c_ulong,
        string: *const u16,
    );
}

pub mod new_tap {
    use std::{
        mem::{self, ManuallyDrop},
        ptr,
    };

    use core_foundation::{
        base::TCFType,
        mach_port::{CFMachPort, CFMachPortRef},
    };
    use core_graphics::{
        event::{
            CGEvent, CGEventMask, CGEventTapCallBackFn, CGEventTapLocation, CGEventTapOptions,
            CGEventTapPlacement, CGEventTapProxy, CGEventType,
        },
        sys,
    };
    use foreign_types::ForeignType;
    use libc::c_void;

    type CGEventTapCallBackInternal = unsafe extern "C" fn(
        proxy: CGEventTapProxy,
        etype: CGEventType,
        event: sys::CGEventRef,
        user_info: *const c_void,
    ) -> sys::CGEventRef;

    #[link(name = "CoreGraphics", kind = "framework")]
    extern "C" {
        fn CGEventTapCreate(
            tap: CGEventTapLocation,
            place: CGEventTapPlacement,
            options: CGEventTapOptions,
            eventsOfInterest: CGEventMask,
            callback: CGEventTapCallBackInternal,
            userInfo: *const c_void,
        ) -> CFMachPortRef;
        fn CGEventTapEnable(tap: CFMachPortRef, enable: bool);
    }

    #[no_mangle]
    unsafe extern "C" fn cg_new_tap_callback_internal(
        _proxy: CGEventTapProxy,
        _etype: CGEventType,
        _event: sys::CGEventRef,
        _user_info: *const c_void,
    ) -> sys::CGEventRef {
        let callback = _user_info as *mut CGEventTapCallBackFn;
        let event = CGEvent::from_ptr(_event);
        let new_event = (*callback)(_proxy, _etype, &event);
        match new_event {
            Some(new_event) => ManuallyDrop::new(new_event).as_ptr(),
            None => {
                mem::forget(event);
                ptr::null_mut() as sys::CGEventRef
            }
        }
    }

    /* Generate an event mask for a single type of event. */
    macro_rules! CGEventMaskBit {
        ($eventType:expr) => {
            1 << $eventType as CGEventMask
        };
    }

    type CallbackType<'tap_life> =
        Box<dyn Fn(CGEventTapProxy, CGEventType, &CGEvent) -> Option<CGEvent> + 'tap_life>;
    pub struct CGEventTap<'tap_life> {
        pub mach_port: CFMachPort,
        pub callback_ref: CallbackType<'tap_life>,
    }

    impl<'tap_life> CGEventTap<'tap_life> {
        pub fn new<F: Fn(CGEventTapProxy, CGEventType, &CGEvent) -> Option<CGEvent> + 'tap_life>(
            tap: CGEventTapLocation,
            place: CGEventTapPlacement,
            options: CGEventTapOptions,
            events_of_interest: std::vec::Vec<CGEventType>,
            callback: F,
        ) -> Result<CGEventTap<'tap_life>, ()> {
            let event_mask: CGEventMask = events_of_interest
                .iter()
                .fold(CGEventType::Null as CGEventMask, |mask, &etype| {
                    mask | CGEventMaskBit!(etype)
                });
            let cb = Box::new(Box::new(callback) as CGEventTapCallBackFn);
            let cbr = Box::into_raw(cb);
            unsafe {
                let event_tap_ref = CGEventTapCreate(
                    tap,
                    place,
                    options,
                    event_mask,
                    cg_new_tap_callback_internal,
                    cbr as *const c_void,
                );

                if !event_tap_ref.is_null() {
                    Ok(Self {
                        mach_port: (CFMachPort::wrap_under_create_rule(event_tap_ref)),
                        callback_ref: Box::from_raw(cbr),
                    })
                } else {
                    _ = Box::from_raw(cbr);
                    Err(())
                }
            }
        }

        pub fn enable(&self) {
            unsafe { CGEventTapEnable(self.mach_port.as_concrete_TypeRef(), true) }
        }
    }
}

pub(crate) enum Callback {}
unsafe impl Message for Callback {}

pub(crate) struct CallbackState {
    cb: Box<dyn Fn()>,
}

impl Callback {
    pub(crate) fn from(cb: Box<dyn Fn()>) -> Id<Self> {
        let cbs = CallbackState { cb };
        let bcbs = Box::new(cbs);

        let ptr = Box::into_raw(bcbs);
        let ptr = ptr as *mut c_void as usize;
        let mut oid = <Callback as INSObject>::new();
        (*oid).setptr(ptr);
        oid
    }

    pub(crate) fn setptr(&mut self, uptr: usize) {
        unsafe {
            let obj = &mut *(self as *mut _ as *mut ::objc::runtime::Object);
            obj.set_ivar("_cbptr", uptr);
        }
    }
}

impl INSObject for Callback {
    fn class() -> &'static Class {
        let cname = "Callback";

        let mut klass = Class::get(cname);
        if klass.is_none() {
            let superclass = NSObject::class();
            let mut decl = ClassDecl::new(cname, superclass).unwrap();
            decl.add_ivar::<usize>("_cbptr");

            extern "C" fn sysbar_callback_call(this: &Object, _cmd: Sel) {
                unsafe {
                    let pval: usize = *this.get_ivar("_cbptr");
                    let ptr = pval as *mut c_void;
                    let ptr = ptr as *mut CallbackState;
                    let bcbs: Box<CallbackState> = Box::from_raw(ptr);
                    {
                        (*bcbs.cb)();
                    }
                    mem::forget(bcbs);
                }
            }

            unsafe {
                decl.add_method(
                    sel!(call),
                    sysbar_callback_call as extern "C" fn(&Object, Sel),
                );
            }

            decl.register();
            klass = Class::get(cname);
        }
        klass.unwrap()
    }
}

#[link(name = "ApplicationServices", kind = "framework")]
extern "C" {
    pub fn AXIsProcessTrustedWithOptions(options: CFDictionaryRef) -> bool;
    pub static kAXTrustedCheckOptionPrompt: CFStringRef;
}

#[link(name = "AppKit", kind = "framework")]
extern "C" {
    pub static NSWorkspaceDidActivateApplicationNotification: CFStringRef;
}

// dispatch_get_main_queue() is a C macro expanding to (&_dispatch_main_q)
extern "C" {
    static _dispatch_main_q: c_void;
    fn dispatch_async_f(
        queue: *const c_void,
        context: *mut c_void,
        work: unsafe extern "C" fn(*mut c_void),
    );
}

/// Open an app file picker deferred to the next run loop iteration.
/// This avoids re-entering druid's RefCell borrow during event handling.
pub fn defer_open_app_file_picker(callback: Box<dyn FnOnce(Option<String>) + Send>) {
    unsafe extern "C" fn work(ctx: *mut c_void) {
        let callback = Box::from_raw(ctx as *mut Box<dyn FnOnce(Option<String>) + Send>);
        let name = open_app_file_picker();
        callback(name);
    }

    let boxed: Box<Box<dyn FnOnce(Option<String>) + Send>> = Box::new(callback);
    let ctx_ptr = Box::into_raw(boxed) as *mut c_void;

    unsafe {
        dispatch_async_f(&_dispatch_main_q, ctx_ptr, work);
    }
}

pub fn open_app_file_picker() -> Option<String> {
    unsafe {
        let panel: id = msg_send![class!(NSOpenPanel), openPanel];
        let _: () = msg_send![panel, setCanChooseFiles: YES];
        let _: () = msg_send![panel, setCanChooseDirectories: NO];
        let _: () = msg_send![panel, setAllowsMultipleSelection: NO as BOOL];

        // Allow only .app bundles
        let app_ext = NSString::alloc(nil).init_str("app");
        let types_array: id = msg_send![class!(NSArray), arrayWithObject: app_ext];
        let _: () = msg_send![panel, setAllowedFileTypes: types_array];

        // Start in /Applications
        let apps_path = NSString::alloc(nil).init_str("/Applications");
        let dir_url: id = msg_send![class!(NSURL), fileURLWithPath: apps_path];
        let _: () = msg_send![panel, setDirectoryURL: dir_url];

        let response: i64 = msg_send![panel, runModal];
        if response == 1 {
            // NSModalResponseOK = 1
            let url: id = msg_send![panel, URL];
            let path: id = msg_send![url, path];

            let utf8: *const std::ffi::c_char = msg_send![path, UTF8String];
            if !utf8.is_null() {
                return Some(
                    std::ffi::CStr::from_ptr(utf8)
                        .to_string_lossy()
                        .into_owned(),
                );
            }
        }
        None
    }
}

pub fn open_text_file_picker() -> Option<String> {
    unsafe {
        let panel: id = msg_send![class!(NSOpenPanel), openPanel];
        let _: () = msg_send![panel, setCanChooseFiles: YES];
        let _: () = msg_send![panel, setCanChooseDirectories: NO];
        let _: () = msg_send![panel, setAllowsMultipleSelection: NO as BOOL];

        let response: i64 = msg_send![panel, runModal];
        if response == 1 {
            let url: id = msg_send![panel, URL];
            let path: id = msg_send![url, path];
            let utf8: *const std::ffi::c_char = msg_send![path, UTF8String];
            if !utf8.is_null() {
                return Some(
                    std::ffi::CStr::from_ptr(utf8)
                        .to_string_lossy()
                        .into_owned(),
                );
            }
        }
        None
    }
}

pub fn save_text_file_picker() -> Option<String> {
    unsafe {
        let panel: id = msg_send![class!(NSSavePanel), savePanel];
        let suggested_name = NSString::alloc(nil).init_str("expansions.txt");
        let _: () = msg_send![panel, setNameFieldStringValue: suggested_name];

        let response: i64 = msg_send![panel, runModal];
        if response == 1 {
            let url: id = msg_send![panel, URL];
            let path: id = msg_send![url, path];
            let utf8: *const std::ffi::c_char = msg_send![path, UTF8String];
            if !utf8.is_null() {
                return Some(
                    std::ffi::CStr::from_ptr(utf8)
                        .to_string_lossy()
                        .into_owned(),
                );
            }
        }
        None
    }
}

pub fn defer_open_text_file_picker(callback: Box<dyn FnOnce(Option<String>) + Send>) {
    unsafe extern "C" fn work(ctx: *mut c_void) {
        let callback = Box::from_raw(ctx as *mut Box<dyn FnOnce(Option<String>) + Send>);
        let path = open_text_file_picker();
        callback(path);
    }

    let boxed: Box<Box<dyn FnOnce(Option<String>) + Send>> = Box::new(callback);
    let ctx_ptr = Box::into_raw(boxed) as *mut c_void;

    unsafe {
        dispatch_async_f(&_dispatch_main_q, ctx_ptr, work);
    }
}

pub fn defer_save_text_file_picker(callback: Box<dyn FnOnce(Option<String>) + Send>) {
    unsafe extern "C" fn work(ctx: *mut c_void) {
        let callback = Box::from_raw(ctx as *mut Box<dyn FnOnce(Option<String>) + Send>);
        let path = save_text_file_picker();
        callback(path);
    }

    let boxed: Box<Box<dyn FnOnce(Option<String>) + Send>> = Box::new(callback);
    let ctx_ptr = Box::into_raw(boxed) as *mut c_void;

    unsafe {
        dispatch_async_f(&_dispatch_main_q, ctx_ptr, work);
    }
}

pub fn add_app_change_callback<F>(cb: F)
where
    F: Fn() + Send + 'static,
{
    unsafe {
        let shared_workspace: id = msg_send![class!(NSWorkspace), sharedWorkspace];
        let notification_center: id = msg_send![shared_workspace, notificationCenter];
        let cb_obj = Callback::from(Box::new(cb));

        let _: id = msg_send![notification_center,
            addObserver:cb_obj
            selector:sel!(call)
            name:NSWorkspaceDidActivateApplicationNotification
            object:nil
        ];
    }
}

pub fn add_appearance_change_callback<F>(cb: F)
where
    F: Fn() + Send + 'static,
{
    unsafe {
        use cocoa::base::nil;
        use cocoa::foundation::NSString;
        let notification_center: id =
            msg_send![class!(NSDistributedNotificationCenter), defaultCenter];
        let cb_obj = Callback::from(Box::new(cb));
        let name = NSString::alloc(nil).init_str("AppleInterfaceThemeChangedNotification");

        let _: id = msg_send![notification_center,
            addObserver:cb_obj
            selector:sel!(call)
            name:name
            object:nil
        ];
    }
}


================================================
FILE: src/platform/mod.rs
================================================
#[cfg_attr(target_os = "macos", path = "macos.rs")]
#[cfg_attr(target_os = "linux", path = "linux.rs")]
#[cfg_attr(target_os = "windows", path = "window.rs")]
mod os;

use std::fmt::Display;

use bitflags::bitflags;
pub use os::{
    add_app_change_callback, add_appearance_change_callback, defer_open_app_file_picker,
    defer_open_text_file_picker,
    defer_save_text_file_picker, dispatch_set_systray_title, ensure_accessibility_permission,
    get_active_app_name, get_app_icon_rgba, get_home_dir, get_preferred_language, is_dark_mode,
    is_in_text_selection, is_launch_on_login, run_event_listener, send_arrow_left,
    send_arrow_right, send_backspace, send_string, update_launch_on_login, Handle, SYMBOL_ALT,
    SYMBOL_CTRL, SYMBOL_SHIFT, SYMBOL_SUPER,
};

#[cfg(target_os = "macos")]
pub use os::SystemTray;
pub use os::SystemTrayMenuItemKey;

pub const RAW_KEY_GLOBE: u16 = 0xb3;
pub const RAW_ARROW_DOWN: u16 = 0x7d;
pub const RAW_ARROW_UP: u16 = 0x7e;
pub const RAW_ARROW_LEFT: u16 = 0x7b;
pub const RAW_ARROW_RIGHT: u16 = 0x7c;
pub const KEY_ENTER: char = '\x13';
pub const KEY_SPACE: char = '\u{0020}';
pub const KEY_TAB: char = '\x09';
pub const KEY_DELETE: char = '\x08';
pub const KEY_ESCAPE: char = '\x26';

bitflags! {
    pub struct KeyModifier: u32 {
        const MODIFIER_NONE     = 0b00000000;
        const MODIFIER_SHIFT    = 0b00000001;
        const MODIFIER_SUPER    = 0b00000010;
        const MODIFIER_CONTROL  = 0b00000100;
        const MODIFIER_ALT      = 0b00001000;
        const MODIFIER_CAPSLOCK = 0b00010000;
    }
}

impl Display for KeyModifier {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        if self.is_super() {
            write!(f, "super+")?;
        }
        if self.is_control() {
            write!(f, "ctrl+")?;
        }
        if self.is_alt() {
            write!(f, "alt+")?;
        }
        if self.is_shift() {
            write!(f, "shift+")?;
        }
        if self.is_capslock() {
            write!(f, "capslock+")?;
        }
        write!(f, "")
    }
}

impl KeyModifier {
    pub fn new() -> Self {
        Self { bits: 0 }
    }

    pub fn apply(
        &mut self,
        is_super: bool,
        is_ctrl: bool,
        is_alt: bool,
        is_shift: bool,
        is_capslock: bool,
    ) {
        self.set(Self::MODIFIER_SUPER, is_super);
        self.set(Self::MODIFIER_CONTROL, is_ctrl);
        self.set(Self::MODIFIER_ALT, is_alt);
        self.set(Self::MODIFIER_SHIFT, is_shift);
        self.set(Self::MODIFIER_CAPSLOCK, is_capslock);
    }

    pub fn add_shift(&mut self) {
        self.set(Self::MODIFIER_SHIFT, true);
    }

    pub fn add_super(&mut self) {
        self.set(Self::MODIFIER_SUPER, true);
    }

    pub fn add_control(&mut self) {
        self.set(Self::MODIFIER_CONTROL, true);
    }

    pub fn add_alt(&mut self) {
        self.set(Self::MODIFIER_ALT, true);
    }

    pub fn add_capslock(&mut self) {
        self.set(Self::MODIFIER_CAPSLOCK, true);
    }

    pub fn is_shift(&self) -> bool {
        self.contains(Self::MODIFIER_SHIFT)
    }

    pub fn is_super(&self) -> bool {
        self.contains(Self::MODIFIER_SUPER)
    }

    pub fn is_control(&self) -> bool {
        self.contains(Self::MODIFIER_CONTROL)
    }

    pub fn is_alt(&self) -> bool {
        self.contains(Self::MODIFIER_ALT)
    }

    pub fn is_capslock(&self) -> bool {
        self.contains(Self::MODIFIER_CAPSLOCK)
    }
}

#[derive(Debug, Copy, Clone)]
pub enum PressedKey {
    Char(char),
    Raw(u16),
}

#[derive(Debug, PartialEq, Eq)]
pub enum EventTapType {
    KeyDown,
    FlagsChanged,
    Other,
}

pub type CallbackFn = dyn Fn(os::Handle, EventTapType, Option<PressedKey>, KeyModifier) -> bool;


================================================
FILE: src/platform/windows.rs
================================================
// TODO: Implement this

use druid::{Selector, commands::CLOSE_WINDOW};

use super::CallbackFn;

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

pub fn get_home_dir() -> Option<PathBuf> {
    env::var("USERPROFILE").ok().map(PathBuf::from)
        .or_else(|| env::var("HOMEDRIVE").ok().and_then(|home_drive| {
            env::var("HOMEPATH").ok().map(|home_path| {
                PathBuf::from(format!("{}{}", home_drive, home_path))
            })
        }))
}

pub fn send_backspace(count: usize) -> Result<(), ()> {
    todo!()
}

pub fn send_string(string: &str) -> Result<(), ()> {
    todo!()
}

pub fn run_event_listener(callback: &CallbackFn) {
    todo!()
}

pub fn ensure_accessibility_permission() -> bool {
    true
}

pub fn is_in_text_selection() -> bool {
    todo!()
}

pub fn update_launch_on_login(is_enable: bool) {
    todo!()
}

pub fn is_launch_on_login() {
    todo!()
}


================================================
FILE: src/scripting/mod.rs
================================================
/// This module, `parser`, is built for the goxscript language.
/// It parses the goxscript language and returns an AST which can be used to
/// generate the corresponding vi-rs rule map.
///
/// # Example
/// The script would look like this:
///
/// ```
/// import telex
/// import vni
///
/// on s or ': add_tone(acute) end
///
/// on a or e or o or 6:
///   letter_mod(circumflex for a or e or o)
/// end
///
/// on w or 7 or 8:
///   reset_inserted_uw() or
///   letter_mod(horn or breve for u or o) or
///   insert_uw()
/// end
/// ```
///
/// # Syntax
/// The following EBNF describes the syntax of the goxscript language:
///
/// ```ebnf
/// <program> ::= <import_list>? <whitespace> <block_list>?
///
/// <import_list> ::= <import> ( <whitespace> <import_list> )?
/// <import> ::= "import" <whitespace> <identifier>
///
/// <block_list> ::= <block> ( <whitespace> <block_list> )?
/// <block> ::= "on" <whitespace> <key_list> <whitespace> ":" <whitespace> <function_call_list> <whitespace> "end"
///
/// <function_call_list> ::= <function_call> ( <whitespace> "or" <whitespace> <function_call_list> )?
/// <function_call> ::= <identifier> "(" ( <identifier_list> ( <whitespace> "for" <whitespace> <key_list> )? )? ")"
///
/// <identifier_list> ::= <identifier> ( <whitespace> "or" <whitespace> <identifier_list> )?
/// <identifier> ::= (<upper_letter> | <lower_letter> | <digit> | "_")+
///
/// <key_list> ::= <key> ( <whitespace> "or" <whitespace> <key_list> )?
/// <key> ::= <any_character>
///
/// <whitespace> ::= (" " | "\n")*
/// <any_character> ::= <upper_letter> | <lower_letter> | <digit> | <punctuation>
/// <upper_letter> ::= "A" | "B" | "C" | "D" | "E" | "F" | "G" | "H" | "I" | "J" | "K" | "L" | "M" | "N" | "O" |
///                    "P" | "Q" | "R" | "S" | "T" | "U" | "V" | "W" | "X" | "Y" | "Z"
/// <lower_letter> ::= "a" | "b" | "c" | "d" | "e" | "f" | "g" | "h" | "i" | "j" | "k" | "l" | "m" | "n" | "o" |
///                    "p" | "q" | "r" | "s" | "t" | "u" | "v" | "w" | "x" | "y" | "z"
/// <digit> ::= "0" | "1" | "2" | "3" | "4" | "5" | "6" | "7" | "8" | "9"
/// <punctuation> ::= "!" | "\"" | "#" | "$" | "%" | "&" | "'" | "(" | ")" | "*" | "+" | "," | "-" | "." | "/" |
///                   ":" | ";" | "<" | "=" | ">" | "?" | "@" | "[" | "\\" | "]" | "^" | "_" | "`" | "{" | "}" | "~"
/// ```
pub mod parser;


================================================
FILE: src/scripting/parser.rs
================================================
use nom::{
    bytes::complete::{tag, take_while1, take_while_m_n},
    character::complete::{multispace0, multispace1},
    combinator::{map, opt},
    multi::separated_list1,
    sequence::{delimited, preceded, tuple},
    IResult,
};

/// Represents a program containing a list of imports and blocks.
///
/// # Example
///
/// ```
/// let program = Program {
///     import_list: Some(vec![Import { identifier: "telex".to_string() }]),
///     block_list: Some(vec![Block {
///         key_list: vec!["a".to_string()],
///         function_call_list: vec![FunctionCall {
///             identifier: "hello".to_string(),
///             identifier_list: None,
///             key_list: None,
///         }],
///     }]),
/// };
/// println!("{:?}", program);
/// ```
#[derive(Debug, PartialEq)]
pub struct Program {
    import_list: Option<Vec<Import>>,
    block_list: Option<Vec<Block>>,
}

/// Represents an import statement with an identifier.
///
/// # Example
///
/// ```
/// let import = Import {
///     identifier: "telex".to_string(),
/// };
/// println!("{:?}", import);
/// ```
#[derive(Debug, PartialEq)]
pub struct Import {
    identifier: String,
}

/// Represents a block containing a list of keys and function calls.
///
/// # Example
///
/// ```
/// let block = Block {
///     key_list: vec!["a".to_string()],
///     function_call_list: vec![FunctionCall {
///         identifier: "hello".to_string(),
///         identifier_list: None,
///         key_list: None,
///     }],
/// };
/// println!("{:?}", block);
/// ```
#[derive(Debug, PartialEq)]
pub struct Block {
    key_list: Vec<String>,
    function_call_list: Vec<FunctionCall>,
}

/// Represents a function call with an identifier, and optional lists of identifiers and keys.
///
/// # Example
///
/// ```
/// let function_call = FunctionCall {
///     identifier: "hello".to_string(),
///     identifier_list: Some(vec!["world".to_string()]),
///     key_list: Some(vec!["a".to_string()]),
/// };
/// println!("{:?}", function_call);
/// ```
#[derive(Debug, PartialEq)]
pub struct FunctionCall {
    identifier: String,
    identifier_list: Option<Vec<String>>,
    key_list: Option<Vec<String>>,
}

/// Checks if a character is a valid key character (not whitespace).
///
/// # Example
///
/// ```
/// let result = is_key_char('a');
/// assert!(result);
/// let result = is_key_char(' ');
/// assert!(!result);
/// ```
fn is_key_char(c: char) -> bool {
    !c.is_whitespace()
}

/// Parses a key from the input string.
///
/// # Example
///
/// ```
/// let result = parse_key("a");
/// assert!(result.is_ok());
/// assert_eq!(result.unwrap().1, "a".to_string());
/// ```
fn parse_key(input: &str) -> IResult<&str, String> {
    map(take_while_m_n(1, 1, is_key_char), |s: &str| s.to_string())(input)
}

/// Parses a list of keys from the input string.
///
/// # Example
///
/// ```
/// let result = parse_key_list("a or b or c");
/// assert!(result.is_ok());
/// assert_eq!(result.unwrap().1, vec!["a".to_string(), "b".to_string(), "c".to_string()]);
/// ```
fn parse_key_list(input: &str) -> IResult<&str, Vec<String>> {
    separated_list1(delimited(multispace1, tag("or"), multispace1), parse_key)(input)
}

/// Checks if a character is a valid identifier character (alphanumeric or underscore).
///
/// # Example
///
/// ```
/// let result = is_identifier_char('a');
/// assert!(result);
/// let result = is_identifier_char('1');
/// assert!(result);
/// let result = is_identifier_char('_');
/// assert!(result);
/// let result = is_identifier_char(' ');
/// assert!(!result);
/// ```
fn is_identifier_char(c: char) -> bool {
    c.is_alphanumeric() || c == '_'
}

/// Parses an identifier from the input string.
///
/// # Example
///
/// ```
/// let result = parse_identifier("abc123");
/// assert!(result.is_ok());
/// assert_eq!(result.unwrap().1, "abc123".to_string());
/// ```
fn parse_identifier(input: &str) -> IResult<&str, String> {
    map(take_while1(is_identifier_char), |s: &str| s.to_string())(input)
}

/// Parses a list of identifiers from the input string.
///
/// # Example
///
/// ```
/// let result = parse_identifier_list("abc or def or ghi");
/// assert!(result.is_ok());
/// assert_eq!(result.unwrap().1, vec!["abc".to_string(), "def".to_string(), "ghi".to_string()]);
/// ```
fn parse_identifier_list(input: &str) -> IResult<&str, Vec<String>> {
    separated_list1(
        delimited(multispace1, tag("or"), multispace1),
        parse_identifier,
    )(input)
}

/// Parses an import statement from the input string.
///
/// # Example
///
/// ```
/// let result = parse_import("import abc");
/// assert!(result.is_ok());
/// assert_eq!(result.unwrap().1, Import { identifier: "abc".to_string() });
/// ```
fn parse_import(input: &str) -> IResult<&str, Import> {
    let (input, _) = preceded(tag("import"), multispace1)(input)?;
    let (input, identifier) = parse_identifier(input)?;
    Ok((
        input,
        Import {
            identifier: identifier.to_string(),
        },
    ))
}

/// Parses a list of import statements from the input string.
///
/// # Example
///
/// ```
/// let result = parse_import_list("import abc import def");
/// assert!(result.is_ok());
/// assert_eq!(result.unwrap().1, vec![
///     Import { identifier: "abc".to_string() },
///     Import { identifier: "def".to_string() }
/// ]);
/// ```
fn parse_import_list(input: &str) -> IResult<&str, Vec<Import>> {
    separated_list1(multispace1, parse_import)(input)
}

/// Parses a function call from the input string.
///
/// # Example
///
/// ```
/// let result = parse_function_call("hello(world)");
/// assert!(result.is_ok());
/// assert_eq!(result.unwrap().1, FunctionCall {
///     identifier: "hello".to_string(),
///     identifier_list: Some(vec!["world".to_string()]),
///     key_list: None,
/// });
/// ```
fn parse_function_call(input: &str) -> IResult<&str, FunctionCall> {
    let parse_identifier_list = opt(parse_identifier_list);
    let parse_key_list = map(
        opt(tuple((
            multispace1,
            tag("for"),
            multispace1,
            parse_key_list,
        ))),
        |x| x.map(|(_, _, _, key_list)| key_list),
    );
    let (input, (identifier, _, _, identifier_list, key_list, _, _)) = tuple((
        parse_identifier,
        tag("("),
        multispace0,
        parse_identifier_list,
        parse_key_list,
        multispace0,
        tag(")"),
    ))(input)?;
    Ok((
        input,
        FunctionCall {
            identifier: identifier.to_string(),
            identifier_list,
            key_list,
        },
    ))
}

/// Parses a list of function calls from the input string.
///
/// # Example
///
/// ```
/// let result = parse_function_call_list("hello() or world(abc)");
/// assert!(result.is_ok());
/// assert_eq!(result.unwrap().1, vec![
///     FunctionCall {
///         identifier: "hello".to_string(),
///         identifier_list: None,
///         key_list: None,
///     },
///     FunctionCall {
///         identifier: "world".to_string(),
///         identifier_list: Some(vec!["abc".to_string()]),
///         key_list: None,
///     }
/// ]);
/// ```
fn parse_function_call_list(input: &str) -> IResult<&str, Vec<FunctionCall>> {
    separated_list1(
        delimited(multispace1, tag("or"), multispace1),
        parse_function_call,
    )(input)
}

/// Parses a block from the input string.
///
/// # Example
///
/// ```
/// let result = parse_block("on a: hello() end");
/// assert!(result.is_ok());
/// assert_eq!(result.unwrap().1, Block {
///     key_list: vec!["a".to_string()],
///     function_call_list: vec![FunctionCall {
///         identifier: "hello".to_string(),
///         identifier_list: None,
///         key_list: None,
///     }],
/// });
/// ```
fn parse_block(input: &str) -> IResult<&str, Block> {
    let (input, (_, _, key_list, _, _, _, function_call_list, _, _)) = tuple((
        tag("on"),
        multispace1,
        parse_key_list,
        multispace0,
        tag(":"),
        multispace1,
        parse_function_call_list,
        multispace1,
        tag("end"),
    ))(input)?;
    Ok((
        input,
        Block {
            key_list,
            function_call_list,
        },
    ))
}

/// Parses a program from the input string.
///
/// # Example
///
/// ```
/// let result = parse_program("import telex\non a: hello() end");
/// assert!(result.is_ok());
/// assert_eq!(result.unwrap().1, Program {
///     import_list: Some(vec![Import { identifier: "telex".to_string() }]),
///     block_list: Some(vec![Block {
///         key_list: vec!["a".to_string()],
///         function_call_list: vec![FunctionCall {
///             identifier: "hello".to_string(),
///             identifier_list: None,
///             key_list: None,
///         }],
///     }]),
/// });
/// ```
pub fn parse_program(input: &str) -> IResult<&str, Program> {
    let parse_import_list = opt(parse_import_list);
    let parse_block_list = opt(separated_list1(multispace1, parse_block));
    let (input, (_, import_list, _, block_list, _)) = tuple((
        multispace0,
        parse_import_list,
        multispace0,
        parse_block_list,
        multispace0,
    ))(input)?;
    Ok((
        input,
        Program {
            import_list,
            block_list,
        },
    ))
}

#[test]
fn test_parse_key() {
    let input = "a";
    let result = parse_key(input);
    assert!(result.is_ok());
    assert!(result.unwrap().1 == "a");
}

#[test]
fn test_parse_key_should_parse_a_single_key() {
    let input = "abc";
    let result = parse_key(input);
    assert!(result.is_ok());
    assert!(result.unwrap().1 == "a");
}

#[test]
fn test_parse_key_list() {
    let input = "a or   b  or c";
    let result = parse_key_list(input);
    assert!(result.is_ok());
    println!("{result:?}");
    assert!(result.unwrap().1 == vec!["a", "b", "c"]);
}

#[test]
fn test_parse_identifier() {
    let input = "abc12_abc";
    let result = parse_identifier(input);
    assert!(result.is_ok());
    assert!(result.unwrap().1 == "abc12_abc");
}

#[test]
fn test_parse_identifier_list() {
    let input = "a or abc12 or ab_cd12";
    let result = parse_identifier_list(input);
    assert!(result.is_ok());
    assert!(result.unwrap().1 == vec!["a", "abc12", "ab_cd12"]);
}

#[test]
fn test_parse_identifier_list_single_item() {
    let input = "abc";
    let result = parse_identifier_list(input);
    assert!(result.is_ok());
    assert!(result.unwrap().1 == vec!["abc"]);
}

#[test]
fn test_parse_key_list_single() {
    let input = "a";
    let result = parse_key_list(input);
    assert!(result.is_ok());
    assert!(result.unwrap().1 == vec!["a"]);
}

#[test]
fn parse_import_fail() {
    let input = "import;";
    let result = parse_import(input);
    assert!(result.is_err());
}

#[test]
fn parse_import_fail_not_a_function() {
    let input = "import ()";
    let result = parse_import(input);
    assert!(result.is_err());
}

#[test]
fn parse_import_fail_no_module() {
    let input = "import";
    let result = parse_import(input);
    assert!(result.is_err());
}

#[test]
fn parse_import_fail_no_module_just_space() {
    let input = "import ";
    let result = parse_import(input);
    assert!(result.is_err());
}

#[test]
fn parse_import_success() {
    let input = "import abc";
    let result = parse_import(input);
    assert!(result.is_ok());
    assert!(
        result.unwrap().1
            == Import {
                identifier: "abc".to_string()
            }
    );
}

#[test]
fn parse_import_list_success_single() {
    let input = "import abc\n";
    let result = parse_import_list(input);
    assert!(result.is_ok());
    assert!(
        result.unwrap().1
            == vec![Import {
                identifier: "abc".to_string()
            }]
    );
}

#[test]
fn parse_import_list_success() {
    let input = "import abc import def";
    let result = parse_import_list(input);
    assert!(result.is_ok());
    assert!(
        result.unwrap().1
            == vec![
                Import {
                    identifier: "abc".to_string()
                },
                Import {
                    identifier: "def".to_string()
                }
            ]
    );
}

#[test]
fn parse_function_call_fail() {
    let input = "abc";
    let result = parse_function_call(input);
    assert!(result.is_err());
}

#[test]
fn parse_function_call_space_before_parens_fail() {
    let input = "abc ()";
    let result = parse_function_call(input);
    assert!(result.is_err());
}

#[test]
fn parse_function_call_success_with_no_params() {
    let input = "abc() ";
    let result = parse_function_call(input);
    assert!(result.is_ok());
    assert!(
        result.unwrap().1
            == FunctionCall {
                identifier: "abc".to_string(),
                identifier_list: None,
                key_list: None
            }
    );
}

#[test]
fn parse_function_call_success_with_no_params_with_space() {
    let input = "abc(  )";
    let result = parse_function_call(input);
    assert!(result.is_ok());
    assert!(
        result.unwrap().1
            == FunctionCall {
                identifier: "abc".to_string(),
                identifier_list: None,
                key_list: None
            }
    );
}

#[test]
fn parse_function_call_success_with_single_param() {
    let input = "abc(   hello   )";
    let result = parse_function_call(input);
    assert!(result.is_ok());
    assert!(
        result.unwrap().1
            == FunctionCall {
                identifier: "abc".to_string(),
                identifier_list: Some(vec!["hello".to_string()]),
                key_list: None
            }
    );
}

#[test]
fn parse_function_call_success_with_multiple_param() {
    let input = "say_this(   hello or word  )";
    let result = parse_function_call(input);
    assert!(result.is_ok());
    assert!(
        result.unwrap().1
            == FunctionCall {
                identifier: "say_this".to_string(),
                identifier_list: Some(vec!["hello".to_string(), "word".to_string()]),
                key_list: None
            }
    );
}

#[test]
fn parse_function_call_success_with_single_param_with_single_key() {
    let input = "say_this(   hello for a  )";
    let result = parse_function_call(input);
    assert!(result.is_ok());
    assert!(
        result.unwrap().1
            == FunctionCall {
                identifier: "say_this".to_string(),
                identifier_list: Some(vec!["hello".to_string()]),
                key_list: Some(vec!["a".to_string()])
            }
    );
}

#[test]
fn parse_function_call_success_with_single_param_with_multiple_key() {
    let input = "say_this(   hello for a or b or '  )";
    let result = parse_function_call(input);
    assert!(result.is_ok());
    assert!(
        result.unwrap().1
            == FunctionCall {
                identifier: "say_this".to_string(),
                identifier_list: Some(vec!["hello".to_string()]),
                key_list: Some(vec!["a".to_string(), "b".to_string(), "'".to_string()])
            }
    );
}

#[test]
fn parse_function_call_success_with_multiple_param_with_single_key() {
    let input = "say_this_123(   hello or world or zoo for a  )";
    let result = parse_function_call(input);
    assert!(result.is_ok());
    assert!(
        result.unwrap().1
            == FunctionCall {
                identifier: "say_this_123".to_string(),
                identifier_list: Some(vec![
                    "hello".to_string(),
                    "world".to_string(),
                    "zoo".to_string()
                ]),
                key_list: Some(vec!["a".to_string()])
            }
    );
}

#[test]
fn parse_function_call_success_with_multiple_param_with_multiple_key() {
    let input = "say_this_123(   hello or world or zoo for a or b or '  )";
    let result = parse_function_call(input);
    assert!(result.is_ok());
    assert!(
        result.unwrap().1
            == FunctionCall {
                identifier: "say_this_123".to_string(),
                identifier_list: Some(vec![
                    "hello".to_string(),
                    "world".to_string(),
                    "zoo".to_string()
                ]),
                key_list: Some(vec!["a".to_string(), "b".to_string(), "'".to_string()])
            }
    );
}

#[test]
fn parse_function_call_fail_with_multiple_param_with_no_key() {
    let input = "say_this_123(   hello or world or zoo for )";
    let result = parse_function_call(input);
    assert!(result.is_err());
}

#[test]
fn parse_function_call_fail_for_unclosed_call() {
    let input = "say_this_123(   hello or world or zoo ";
    let result = parse_function_call(input);
    assert!(result.is_err());
}

#[test]
fn parse_function_call_list_fail() {
    let input = "abc";
    let result = parse_function_call_list(input);
    assert!(result.is_err());
}

#[test]
fn parse_function_call_list_success_with_single_call() {
    let input = "abc()";
    let result = parse_function_call_list(input);
    assert!(result.is_ok());
    assert!(
        result.unwrap().1
            == vec![FunctionCall {
                identifier: "abc".to_string(),
                identifier_list: None,
                key_list: None
            }]
    );
}

#[test]
fn parse_function_call_list_success_with_multiple_call() {
    let input = "abc() or foo_bar(hello) or say_this(   hello or world or zoo for a or b or '  )";
    let result = parse_function_call_list(input);
    assert!(result.is_ok());
    assert!(
        result.unwrap().1
            == vec![
                FunctionCall {
                    identifier: "abc".to_string(),
                    identifier_list: None,
                    key_list: None
                },
                FunctionCall {
                    identifier: "foo_bar".to_string(),
                    identifier_list: Some(vec!["hello".to_string()]),
                    key_list: None
                },
                FunctionCall {
                    identifier: "say_this".to_string(),
                    identifier_list: Some(vec![
                        "hello".to_string(),
                        "world".to_string(),
                        "zoo".to_string()
                    ]),
                    key_list: Some(vec!["a".to_string(), "b".to_string(), "'".to_string()])
                }
            ]
    );
}

#[test]
fn parse_block_fail() {
    let input = "on abc: ";
    let result = parse_block(input);
    assert!(result.is_err());
}

#[test]
fn parse_block_fail_no_key() {
    let input = "on : end";
    let result = parse_block(input);
    assert!(result.is_err());
}

#[test]
fn parse_block_fail_empty_block() {
    let input = "on a: end";
    let result = parse_block(input);
    assert!(result.is_err());
}

#[test]
fn parse_block_success_single_key() {
    let input = "on a: hello() end";
    let result = parse_block(input);
    assert!(result.is_ok());
    assert!(
        result.unwrap().1
            == Block {
                key_list: Vec::from(["a".to_string()]),
                function_call_list: vec![FunctionCall {
                    identifier: "hello".to_string(),
                    identifier_list: None,
                    key_list: None
                }]
            }
    );
}

#[test]
fn parse_block_success_multiple_key() {
    let input = "on a or ' or #: hello() end";
    let result = parse_block(input);
    assert!(result.is_ok());
    assert!(
        result.unwrap().1
            == Block {
                key_list: Vec::from(["a".to_string(), "'".to_string(), "#".to_string()]),
                function_call_list: vec![FunctionCall {
                    identifier: "hello".to_string(),
                    identifier_list: None,
                    key_list: None
                }]
            }
    );
}

#[test]
fn parse_block_success_multiple_key_multiple_calls() {
    let input = "on a or ' or #: hello() or foo(abc) or foo_bar(abc or bee) or foo_foo(abc or bee for a or # or c) end";
    let result = parse_block(input);
    assert!(result.is_ok());
    assert!(
        result.unwrap().1
            == Block {
                key_list: Vec::from(["a".to_string(), "'".to_string(), "#".to_string()]),
                function_call_list: vec![
                    FunctionCall {
                        identifier: "hello".to_string(),
                        identifier_list: None,
                        key_list: None
                    },
                    FunctionCall {
                        identifier: "foo".to_string(),
                        identifier_list: Some(vec!["abc".to_string()]),
                        key_list: None
                    },
                    FunctionCall {
                        identifier: "foo_bar".to_string(),
                        identifier_list: Some(vec!["abc".to_string(), "bee".to_string()]),
                        key_list: None
                    },
                    FunctionCall {
                        identifier: "foo_foo".to_string(),
                        identifier_list: Some(vec!["abc".to_string(), "bee".to_string()]),
                        key_list: Some(vec!["a".to_string(), "#".to_string(), "c".to_string()])
                    }
                ]
            }
    );
}

#[test]
fn parse_program_single_block() {
    let input = "on a: hello() end";
    let result = parse_program(input);
    assert!(result.is_ok());
    assert!(
        result.unwrap().1
            == Program {
                import_list: None,
                block_list: Some(vec![Block {
                    key_list: Vec::from(["a".to_string()]),
                    function_call_list: vec![FunctionCall {
                        identifier: "hello".to_string(),
                        identifier_list: None,
                        key_list: None
                    }]
                }])
            }
    );
}

#[test]
fn parse_program_single_block_with_import() {
    let input = "import telex\non a: hello() end";
    let result = parse_program(input);
    assert!(result.is_ok());
    assert!(
        result.unwrap().1
            == Program {
                import_list: Some(vec![Import {
                    identifier: "telex".to_string()
                }]),
                block_list: Some(vec![Block {
                    key_list: Vec::from(["a".to_string()]),
                    function_call_list: vec![FunctionCall {
                        identifier: "hello".to_string(),
                        identifier_list: None,
                        key_list: None
                    }]
                }])
            }
    );
}

#[test]
fn parse_program_multiple_block() {
    let input = "on a: hello() end \n on b or c: foo() end\n\n\n\non d or e or f: bar() end";
    let result = parse_program(input);
    assert!(result.is_ok());
    assert!(
        result.unwrap().1
            == Program {
                import_list: None,
                block_list: Some(vec![
                    Block {
                        key_list: Vec::from(["a".to_string()]),
                        function_call_list: vec![FunctionCall {
                            identifier: "hello".to_string(),
                            identifier_list: None,
                            key_list: None
                        }]
                    },
                    Block {
                        key_list: Vec::from(["b".to_string(), "c".to_string()]),
                        function_call_list: vec![FunctionCall {
                            identifier: "foo".to_string(),
                            identifier_list: None,
                            key_list: None
                        }]
                    },
                    Block {
                        key_list: Vec::from(["d".to_string(), "e".to_string(), "f".to_string()]),
                        function_call_list: vec![FunctionCall {
                            identifier: "bar".to_string(),
                            identifier_list: None,
                            key_list: None
                        }]
                    }
                ])
            }
    );
}

#[test]
fn parse_program_multiple_block_with_multiple_import() {
    let input = "import telex\n\n\nimport vni on a: hello() end \n on b or c: foo() end\n\n\n\non d or e or f: bar() end";
    let result = parse_program(input);
    assert!(result.is_ok());
    assert!(
        result.unwrap().1
            == Program {
                import_list: Some(vec![
                    Import {
                        identifier: "telex".to_string()
                    },
                    Import {
                        identifier: "vni".to_string()
                    }
                ]),
                block_list: Some(vec![
                    Block {
                        key_list: Vec::from(["a".to_string()]),
                        function_call_list: vec![FunctionCall {
                            identifier: "hello".to_string(),
                            identifier_list: None,
                            key_list: None
                        }]
                    },
                    Block {
                        key_list: Vec::from(["b".to_string(), "c".to_string()]),
                        function_call_list: vec![FunctionCall {
                            identifier: "foo".to_string(),
                            identifier_list: None,
                            key_list: None
                        }]
                    },
                    Block {
                        key_list: Vec::from(["d".to_string(), "e".to_string(), "f".to_string()]),
                        function_call_list: vec![FunctionCall {
                            identifier: "bar".to_string(),
                            identifier_list: None,
                            key_list: None
                        }]
                    }
                ])
            }
    );
}

#[test]
fn parse_full_program_success() {
    let input = r#"
        import telex
        import vni

        on s or ': add_tone(acute) end

        on a or e or o or 6:
          letter_mod(circumflex for a or e or o)
        end

        on w or 7 or 8:
          reset_inserted_uw() or
          letter_mod(horn or breve for u or o) or
          insert_uw()
        end
        "#;
    let result = parse_program(input);
    println!("{result:?}");
    assert!(result.is_ok());
    assert!(
        result.unwrap().1
            == Program {
                import_list: Some(vec![
                    Import {
                        identifier: "telex".to_string()
                    },
                    Import {
                        identifier: "vni".to_string()
                    }
                ]),
                block_list: Some(vec![
                    Block {
                        key_list: Vec::from(["s".to_string(), "'".to_string()]),
                        function_call_list: vec![FunctionCall {
                            identifier: "add_tone".to_string(),
                            identifier_list: Some(vec!["acute".to_string()]),
                            key_list: None
                        }]
                    },
                    Block {
                        key_list: Vec::from([
                            "a".to_string(),
                            "e".to_string(),
                            "o".to_string(),
                            "6".to_string()
                        ]),
                        function_call_list: vec![FunctionCall {
                            identifier: "letter_mod".to_string(),
                            identifier_list: Some(vec!["circumflex".to_string()]),
                            key_list: Some(vec!["a".to_string(), "e".to_string(), "o".to_string()])
                        }]
                    },
                    Block {
                        key_list: Vec::from(["w".to_string(), "7".to_string(), "8".to_string()]),
                        function_call_list: vec![
                            FunctionCall {
                                identifier: "reset_inserted_uw".to_string(),
                                identifier_list: None,
                                key_list: None
                            },
                            FunctionCall {
                                identifier: "letter_mod".to_string(),
                                identifier_list: Some(vec![
                                    "horn".to_string(),
                                    "breve".to_string()
                                ]),
                                key_list: Some(vec!["u".to_string(), "o".to_string()])
                            },
                            FunctionCall {
                                identifier: "insert_uw".to_string(),
                                identifier_list: None,
                                key_list: None
                            }
                        ]
                    }
                ])
            }
    );
}


================================================
FILE: src/ui/colors.rs
================================================
use druid::{Color, Env, Key};
use std::sync::Arc;

pub const GREEN: Color = Color::rgb8(26, 138, 110);
pub const GREEN_BG: Color = Color::rgba8(26, 138, 110, 20);

#[derive(Clone, Copy, Debug)]
pub struct Theme {
    pub win_bg: Color,
    pub card_bg: Color,
    pub card_border: Color,
    pub divider: Color,
    pub text_primary: Color,
    pub text_secondary: Color,
    pub text_section: Color,
    pub badge_bg: Color,
    pub badge_border: Color,
    pub badge_text: Color,
    pub btn_reset_bg: Color,
    pub btn_reset_border: Color,
    pub btn_reset_text: Color,
    pub segmented_bg: Color,
    pub segmented_border: Color,
    pub segmented_text: Color,
    pub segmented_ring: Color,
    pub input_bg: Color,
    pub input_border: Color,
    pub input_text: Color,
    pub input_placeholder: Color,
    pub tab_border: Color,
    pub tab_inactive: Color,
    pub list_row_hover: Color,
    pub toggle_off: Color,
    pub checkbox_border: Color,
    pub tooltip_bg: Color,
}

pub static THEME: Key<Arc<Theme>> = Key::new("goxkey.theme");
pub static IS_DARK: Key<bool> = Key::new("goxkey.is_dark");

pub fn light_theme() -> Theme {
    Theme {
        win_bg: Color::rgb8(255, 255, 255),
        card_bg: Color::rgb8(245, 245, 245),
        card_border: Color::rgba8(0, 0, 0, 30),
        divider: Color::rgba8(0, 0, 0, 20),
        text_primary: Color::rgb8(17, 17, 17),
        text_secondary: Color::rgb8(102, 102, 102),
        text_section: Color::rgb8(153, 153, 153),
        badge_bg: Color::rgb8(255, 255, 255),
        badge_border: Color::rgb8(204, 204, 204),
        badge_text: Color::rgb8(85, 85, 85),
        btn_reset_bg: Color::rgb8(240, 240, 240),
        btn_reset_border: Color::rgb8(204, 204, 204),
        btn_reset_text: Color::rgb8(51, 51, 51),
        segmented_bg: Color::WHITE,
        segmented_border: Color::rgb8(221, 221, 221),
        segmented_text: Color::rgb8(136, 136, 136),
        segmented_ring: Color::rgb8(187, 187, 187),
        input_bg: Color::WHITE,
        input_border: Color::rgb8(204, 204, 204),
        input_text: Color::rgb8(17, 17, 17),
        input_placeholder: Color::rgba8(0, 0, 0, 80),
        tab_border: Color::rgb8(221, 221, 221),
        tab_inactive: Color::rgb8(153, 153, 153),
        list_row_hover: Color::rgba8(0, 0, 0, 8),
        toggle_off: Color::rgb8(187, 187, 187),
        checkbox_border: Color::rgb8(204, 204, 204),
        tooltip_bg: Color::rgb8(40, 40, 40),
    }
}

pub fn dark_theme() -> Theme {
    Theme {
        win_bg: Color::rgb8(30, 30, 30),
        card_bg: Color::rgb8(45, 45, 45),
        card_border: Color::rgba8(255, 255, 255, 20),
        divider: Color::rgba8(255, 255, 255, 15),
        text_primary: Color::rgb8(245, 245, 245),
        text_secondary: Color::rgb8(170, 170, 170),
        text_section: Color::rgb8(120, 120, 120),
        badge_bg: Color::rgb8(60, 60, 60),
        badge_border: Color::rgb8(100, 100, 100),
        badge_text: Color::rgb8(200, 200, 200),
        btn_reset_bg: Color::rgb8(60, 60, 60),
        btn_reset_border: Color::rgb8(80, 80, 80),
        btn_reset_text: Color::rgb8(220, 220, 220),
        segmented_bg: Color::rgb8(45, 45, 45),
        segmented_border: Color::rgb8(70, 70, 70),
        segmented_text: Color::rgb8(150, 150, 150),
        segmented_ring: Color::rgb8(100, 100, 100),
        input_bg: Color::rgb8(45, 45, 45),
        input_border: Color::rgb8(70, 70, 70),
        input_text: Color::rgb8(245, 245, 245),
        input_placeholder: Color::rgba8(255, 255, 255, 80),
        tab_border: Color::rgb8(70, 70, 70),
        tab_inactive: Color::rgb8(120, 120, 120),
        list_row_hover: Color::rgba8(255, 255, 255, 8),
        toggle_off: Color::rgb8(80, 80, 80),
        checkbox_border: Color::rgb8(80, 80, 80),
        tooltip_bg: Color::rgb8(60, 60, 60),
    }
}

pub fn get_theme(is_dark: bool) -> Theme {
    if is_dark {
        dark_theme()
    } else {
        light_theme()
    }
}

pub fn theme_from_env(env: &Env) -> Theme {
    get_theme(env.get(&IS_DARK))
}

pub const BADGE_VI_BG: Color = Color::rgba8(26, 138, 110, 20);
pub const BADGE_VI_BORDER: Color = Color::rgb8(26, 138, 110);
pub const BADGE_EN_BG: Color = Color::rgba8(58, 115, 199, 18);
pub const BADGE_EN_BORDER: Color = Color::rgb8(58, 115, 199);


================================================
FILE: src/ui/controllers.rs
================================================
use crate::{
    input::{rebuild_keyboard_layout_map, INPUT_STATE},
    platform::{
        defer_open_text_file_picker, defer_save_text_file_picker, update_launch_on_login,
        KeyModifier,
    },
};
use druid::{Env, Event, EventCtx, Screen, UpdateCtx, Widget, WindowDesc, WindowLevel};
use log::error;

use super::{
    add_macro_dialog_ui_builder,
    data::UIDataAdapter,
    edit_shortcut_dialog_ui_builder, format_letter_key, letter_key_to_char,
    selectors::{
        ADD_MACRO, DELETE_MACRO, DELETE_SELECTED_APP, DELETE_SELECTED_MACRO, EXPORT_MACROS_TO_FILE,
        LOAD_MACROS_FROM_FILE, RESET_DEFAULTS, SAVE_SHORTCUT, SET_EN_APP_FROM_PICKER,
        SHOW_ADD_MACRO_DIALOG, SHOW_EDIT_SHORTCUT_DIALOG, TOGGLE_APP_MODE,
    },
    ADD_MACRO_DIALOG_HEIGHT, ADD_MACRO_DIALOG_WIDTH, EDIT_SHORTCUT_DIALOG_HEIGHT,
    EDIT_SHORTCUT_DIALOG_WIDTH, SHOW_UI, UPDATE_UI,
};

pub struct UIController;

impl<W: Widget<UIDataAdapter>> druid::widget::Controller<UIDataAdapter, W> for UIController {
    fn event(
        &mut self,
        child: &mut W,
        ctx: &mut EventCtx,
        event: &Event,
        data: &mut UIDataAdapter,
        env: &Env,
    ) {
        match event {
            Event::Command(cmd) => {
                if cmd.get(UPDATE_UI).is_some() {
                    data.update();
                    rebuild_keyboard_layout_map();
                }
                if cmd.get(SHOW_UI).is_some() {
                    ctx.set_handled();
                    ctx.window().bring_to_front_and_focus();
                }
                if let Some(source) = cmd.get(DELETE_MACRO) {
                    unsafe { INPUT_STATE.delete_macro(source) };
                    data.update();
                }
                if cmd.get(ADD_MACRO).is_some()
                    && !data.new_macro_from.is_empty()
                    && !data.new_macro_to.is_empty()
                {
                    unsafe {
                        INPUT_STATE
                            .add_macro(data.new_macro_from.clone(), data.new_macro_to.clone())
                    };
                    data.new_macro_from = String::new();
                    data.new_macro_to = String::new();
                    data.update();
                }
                if cmd.get(SHOW_ADD_MACRO_DIALOG).is_some() {
                    data.new_macro_from = String::new();
                    data.new_macro_to = String::new();
                    let screen = Screen::get_display_rect();
                    let x = (screen.width() - ADD_MACRO_DIALOG_WIDTH) / 2.0;
                    let y = (screen.height() - ADD_MACRO_DIALOG_HEIGHT) / 2.0;
                    let dialog = WindowDesc::new(add_macro_dialog_ui_builder())
                        .title("Add Text Expansion")
                        .window_size((ADD_MACRO_DIALOG_WIDTH, ADD_MACRO_DIALOG_HEIGHT))
                        .resizable(false)
                        .set_position((x, y))
                        .set_level(WindowLevel::Modal(ctx.window().clone()));
                    ctx.new_window(dialog);
                    ctx.set_handled();
                }
                if cmd.get(SHOW_EDIT_SHORTCUT_DIALOG).is_some() {
                    data.pending_shortcut_display = String::new();
                    data.pending_shortcut_super = false;
                    data.pending_shortcut_ctrl = false;
                    data.pending_shortcut_alt = false;
                    data.pending_shortcut_shift = false;
                    data.pending_shortcut_letter = String::new();
                    let screen = Screen::get_display_rect();
                    let x = (screen.width() - EDIT_SHORTCUT_DIALOG_WIDTH) / 2.0;
                    let y = (screen.height() - EDIT_SHORTCUT_DIALOG_HEIGHT) / 2.0;
                    let dialog = WindowDesc::new(edit_shortcut_dialog_ui_builder())
                        .title("Edit Shortcut")
                        .window_size((EDIT_SHORTCUT_DIALOG_WIDTH, EDIT_SHORTCUT_DIALOG_HEIGHT))
                        .resizable(false)
                        .set_position((x, y))
                        .set_level(WindowLevel::Modal(ctx.window().clone()));
                    ctx.new_window(dialog);
                    ctx.set_handled();
                }
                if let Some((is_super, is_ctrl, is_alt, is_shift, letter)) = cmd.get(SAVE_SHORTCUT)
                {
                    let mut new_mod = KeyModifier::new();
                    new_mod.apply(*is_super, *is_ctrl, *is_alt, *is_shift, false);
                    let key_code = letter_key_to_char(letter);
                    unsafe {
                        INPUT_STATE.set_hotkey(&format!(
                            "{}{}",
                            new_mod,
                            match key_code {
                                Some(' ') => String::from("space"),
                                Some(c) => c.to_string(),
                                _ => String::new(),
                            }
                        ));
                    }
                    data.update();
                    ctx.set_handled();
                }
                if let Some(name) = cmd.get(SET_EN_APP_FROM_PICKER) {
                    data.new_en_app = name.clone();
                    // In the new Apps tab design, adding via picker immediately commits
                    unsafe { INPUT_STATE.add_english_app(&data.new_en_app.clone()) };
                    data.new_en_app = String::new();
                    data.update();
                }
                if let Some(app_name) = cmd.get(TOGGLE_APP_MODE) {
                    let is_vn = data.vn_apps.iter().any(|e| &e.name == app_name);
                    if is_vn {
                        unsafe {
                            INPUT_STATE.remove_vietnamese_app(app_name);
                            INPUT_STATE.add_english_app(app_name);
                        }
                    } else {
                        unsafe {
                            INPUT_STATE.remove_english_app(app_name);
                            INPUT_STATE.add_vietnamese_app(app_name);
                        }
                    }
                    data.update();
                }
                if cmd.get(DELETE_SELECTED_MACRO).is_some() {
                    let idx = data.selected_macro_index;
                    if idx >= 0 {
                        if let Some(entry) = data.macro_table.get(idx as usize) {
                            let source = entry.from.clone();
                            unsafe { INPUT_STATE.delete_macro(&source) };
                        }
                        data.selected_macro_index = -1;
                        data.update();
                    }
                }
                if cmd.get(LOAD_MACROS_FROM_FILE).is_some() {
                    ctx.set_handled();
                    let event_sink = unsafe { crate::UI_EVENT_SINK.get().cloned() };
                    defer_open_text_file_picker(Box::new(move |path| {
                        if let Some(path) = path {
                            unsafe {
                                let _ = INPUT_STATE.import_macros_from_file(&path);
                            }
                            if let Some(sink) = event_sink {
                                let _ = sink.submit_command(
                                    crate::ui::UPDATE_UI,
                                    (),
                                    druid::Target::Global,
                                );
                            }
                        }
                    }));
                }
                if cmd.get(EXPORT_MACROS_TO_FILE).is_some() {
                    ctx.set_handled();
                    let event_sink = unsafe { crate::UI_EVENT_SINK.get().cloned() };
                    defer_save_text_file_picker(Box::new(move |path| {
                        if let Some(path) = path {
                            unsafe {
                                let _ = INPUT_STATE.export_macros_to_file(&path);
                            }
                            if let Some(sink) = event_sink {
                                let _ = sink.submit_command(
                                    crate::ui::UPDATE_UI,
                                    (),
                                    druid::Target::Global,
                                );
                            }
                        }
                    }));
                }
                if cmd.get(DELETE_SELECTED_APP).is_some() {
                    let idx = data.selected_app_index;
                    if idx >= 0 {
                        let vn_len = data.vn_apps.len() as i32;
                        if idx < vn_len {
                            if let Some(entry) = data.vn_apps.get(idx as usize) {
                                let name = entry.name.clone();
                                unsafe { INPUT_STATE.remove_vietnamese_app(&name) };
                            }
                        } else {
                            let en_idx = (idx - vn_len) as usize;
                            if let Some(entry) = data.en_apps.get(en_idx) {
                                let name = entry.name.clone();
                                unsafe { INPUT_STATE.remove_english_app(&name) };
                            }
                        }
                        data.selected_app_index = -1;
                        data.update();
                    }
                }
                if cmd.get(RESET_DEFAULTS).is_some() {
                    unsafe {
                        if !INPUT_STATE.is_enabled() {
                            INPUT_STATE.toggle_vietnamese();
                        }
                        INPUT_STATE.set_method(crate::input::TypingMethod::Telex);
                        INPUT_STATE.set_hotkey("ctrl+space");
                    }
                    if let Err(err) = update_launch_on_login(true) {
                        error!("{}", err);
                    }
                    data.update();
                    ctx.set_handled();
                }
            }
            Event::WindowCloseRequested => {
                ctx.set_handled();
                ctx.window().hide();
            }
            _ => {}
        }
        child.event(ctx, event, data, env)
    }

    fn update(
        &mut self,
        child: &mut W,
        ctx: &mut UpdateCtx,
        old_data: &UIDataAdapter,
        data: &UIDataAdapter,
        env: &Env,
    ) {
        unsafe {
            if old_data.typing_method != data.typing_method {
                INPUT_STATE.set_method(data.typing_method);
            }

            if old_data.launch_on_login != data.launch_on_login {
                if let Err(err) = update_launch_on_login(data.launch_on_login) {
                    error!("{}", err);
                }
            }

            // Update hotkey
            {
                let mut new_mod = KeyModifier::new();
                new_mod.apply(
                    data.super_key,
                    data.ctrl_key,
                    data.alt_key,
                    data.shift_key,
                    data.capslock_key,
                );
                let key_code = letter_key_to_char(&data.letter_key);
                if !INPUT_STATE.get_hotkey().is_match(new_mod, key_code) {
                    INPUT_STATE.set_hotkey(&format!(
                        "{}{}",
                        new_mod,
                        match key_code {
                            Some(' ') => String::from("space"),
                            Some(c) => c.to_string(),
                            _ => String::new(),
                        }
                    ));
                }
            }

            if old_data.is_macro_enabled != data.is_macro_enabled {
                INPUT_STATE.toggle_macro_enabled();
            }

            if old_data.is_macro_autocap_enabled != data.is_macro_autocap_enabled {
                INPUT_STATE.toggle_macro_autocap();
            }

            if old_data.is_auto_toggle_enabled != data.is_auto_toggle_enabled {
                INPUT_STATE.toggle_auto_toggle();
            }

            if old_data.is_w_literal_enabled != data.is_w_literal_enabled {
                INPUT_STATE.toggle_w_literal();
            }

            if old_data.ui_language != data.ui_language {
                let lang_str = match data.ui_language {
                    1 => "vi",
                    2 => "en",
                    _ => "auto",
                };
                crate::config::CONFIG_MANAGER
                    .lock()
                    .unwrap()
                    .set_ui_language(lang_str);
                super::locale::init_lang(lang_str);
                ctx.request_paint();
                // Trigger data.update() so system tray menu text refreshes
                ctx.submit_command(super::UPDATE_UI);
            }
        }
        child.update(ctx, old_data, data, env);
    }
}

pub(super) struct LetterKeyController;

impl<W: Widget<UIDataAdapter>> druid::widget::Controller<UIDataAdapter, W> for LetterKeyController {
    fn event(
        &mut self,
        child: &mut W,
        ctx: &mut EventCtx,
        event: &Event,
        data: &mut UIDataAdapter,
        env: &Env,
    ) {
        if let &Event::MouseDown(_) = event {
            ctx.submit_command(druid::commands::SELECT_ALL);
        }
        if let &Event::KeyUp(_) = event {
            match data.letter_key.as_str() {
                "Space" => {}
                s => {
                    data.letter_key = format_letter_key(letter_key_to_char(s));
                }
            }
        }
        child.event(ctx, event, data, env)
    }
}


================================================
FILE: src/ui/data.rs
================================================
use std::sync::Arc;

use crate::{
    input::{TypingMethod, INPUT_STATE},
    platform::{is_dark_mode, is_launch_on_login, SystemTray, SystemTrayMenuItemKey},
    update_systray_title_immediately, UI_EVENT_SINK,
};
use druid::{commands::QUIT_APP, Data, Lens, Target};

use super::{format_letter_key, locale::t, SHOW_UI, UPDATE_UI};

#[derive(Clone, Data, PartialEq, Eq)]
pub(super) struct MacroEntry {
    pub(super) from: String,
    pub(super) to: String,
}

#[derive(Clone, Data, PartialEq, Eq)]
pub(super) struct AppEntry {
    pub(super) name: String,
}

#[derive(Clone, Data, Lens, PartialEq, Eq)]
pub struct UIDataAdapter {
    pub(super) is_enabled: bool,
    pub(super) typing_method: TypingMethod,
    pub(super) hotkey_display: String,
    pub(super) launch_on_login: bool,
    pub(super) is_auto_toggle_enabled: bool,
    pub(super) is_w_literal_enabled: bool,
    // Macro config
    pub(super) is_macro_enabled: bool,
    pub(super) is_macro_autocap_enabled: bool,
    pub(super) macro_table: Arc<Vec<MacroEntry>>,
    pub(super) new_macro_from: String,
    pub(super) new_macro_to: String,
    // App language settings
    pub(super) vn_apps: Arc<Vec<AppEntry>>,
    pub(super) en_apps: Arc<Vec<AppEntry>>,
    pub(super) new_en_app: String,
    // Hotkey config
    pub(super) super_key: bool,
    pub(super) ctrl_key: bool,
    pub(super) alt_key: bool,
    pub(super) shift_key: bool,
    pub(super) capslock_key: bool,
    pub(super) letter_key: String,
    // Pending shortcut capture (used by edit shortcut dialog)
    pub(super) pending_shortcut_display: String,
    pub(super) pending_shortcut_super: bool,
    pub(super) pending_shortcut_ctrl: bool,
    pub(super) pending_shortcut_alt: bool,
    pub(super) pending_shortcut_shift: bool,
    pub(super) pending_shortcut_letter: String,
    // UI language (0=Auto, 1=Vietnamese, 2=English)
    pub(super) ui_language: u32,
    // Dark mode
    pub is_dark: bool,
    // Tab navigation (0=General, 1=Apps, 2=Shortcuts, 3=Advanced)
    pub(super) active_tab: u32,
    // Apps tab selected row (combined vn+en list, -1 = none)
    pub(super) selected_app_index: i32,
    // Text Expansion tab selected row (-1 = none)
    pub(super) selected_macro_index: i32,
    // system tray
    pub(super) systray: SystemTray,
}

impl UIDataAdapter {
    pub fn new() -> Self {
        let mut ret = Self {
            is_enabled: true,
            typing_method: TypingMethod::Telex,
            hotkey_display: String::new(),
            launch_on_login: false,
            is_auto_toggle_enabled: false,
            is_w_literal_enabled: false,
            is_macro_enabled: false,
            is_macro_autocap_enabled: false,
            macro_table: Arc::new(Vec::new()),
            new_macro_from: String::new(),
            new_macro_to: String::new(),
            vn_apps: Arc::new(Vec::new()),
            en_apps: Arc::new(Vec::new()),
            new_en_app: String::new(),
            super_key: true,
            ctrl_key: true,
            alt_key: false,
            shift_key: false,
            capslock_key: false,
            letter_key: String::from("Space"),
            pending_shortcut_display: String::new(),
            pending_shortcut_super: false,
            pending_shortcut_ctrl: false,
            pending_shortcut_alt: false,
            pending_shortcut_shift: false,
            pending_shortcut_letter: String::new(),
            ui_language: 0,
            is_dark: false,
            active_tab: 0,
            selected_app_index: -1,
            selected_macro_index: -1,
            systray: SystemTray::new(),
        };
        ret.setup_system_tray_actions();
        ret.update();
        ret
    }

    pub fn update(&mut self) {
        unsafe {
            self.is_enabled = INPUT_STATE.is_enabled();
            self.typing_method = INPUT_STATE.get_method();
            self.hotkey_display = INPUT_STATE.get_hotkey().to_string();
            self.is_macro_enabled = INPUT_STATE.is_macro_enabled();
            self.is_macro_autocap_enabled = INPUT_STATE.is_macro_autocap_enabled();
            self.is_auto_toggle_enabled = INPUT_STATE.is_auto_toggle_enabled();
            self.is_w_literal_enabled = INPUT_STATE.is_w_literal_enabled();
            self.launch_on_login = is_launch_on_login();
            self.is_dark = is_dark_mode();
            let config = crate::config::CONFIG_MANAGER.lock().unwrap();
            self.ui_language = match config.get_ui_language() {
                "vi" => 1,
                "en" => 2,
                _ => 0, // "auto"
            };
            drop(config);
            self.macro_table = Arc::new(
                INPUT_STATE
                    .get_macro_table()
                    .iter()
                    .map(|(source, target)| MacroEntry {
                        from: source.to_string(),
                        to: target.to_string(),
                    })
                    .collect::<Vec<MacroEntry>>(),
            );
            self.vn_apps = Arc::new(
                INPUT_STATE
                    .get_vn_apps()
                    .into_iter()
                    .map(|name| AppEntry { name })
                    .collect(),
            );
            self.en_apps = Arc::new(
                INPUT_STATE
                    .get_en_apps()
                    .into_iter()
                    .map(|name| AppEntry { name })
                    .collect(),
            );

            let (modifiers, keycode) = INPUT_STATE.get_hotkey().inner();
            self.super_key = modifiers.is_super();
            self.ctrl_key = modifiers.is_control();
            self.alt_key = modifiers.is_alt();
            self.shift_key = modifiers.is_shift();
            self.letter_key = format_letter_key(keycode);

            match self.is_enabled {
                true => {
                    let title = if INPUT_STATE.is_gox_mode_enabled() {
                        "gõ"
                    } else {
                        "VN"
                    };
                    self.systray.set_title(title, true);
                    self.systray.set_menu_item_title(
                        SystemTrayMenuItemKey::Enable,
                        t("menu.disable_vietnamese"),
                    );
                }
                false => {
                    let title = if INPUT_STATE.is_gox_mode_enabled() {
                        match self.typing_method {
                            TypingMethod::Telex => "gox",
                            TypingMethod::VNI => "go4",
                            TypingMethod::TelexVNI => "go+",
                        }
                    } else {
                        "EN"
                    };
                    self.systray.set_title(title, false);
                    self.systray.set_menu_item_title(
                        SystemTrayMenuItemKey::Enable,
                        t("menu.enable_vietnamese"),
                    );
                }
            }
            match self.typing_method {
                TypingMethod::VNI => {
                    self.systray
                        .set_menu_item_title(SystemTrayMenuItemKey::TypingMethodTelex, "Telex");
                    self.systray
                        .set_menu_item_title(SystemTrayMenuItemKey::TypingMethodVNI, "VNI ✓");
                    self.systray.set_menu_item_title(
                        SystemTrayMenuItemKey::TypingMethodTelexVNI,
                        "Telex+VNI",
                    );
                }
                TypingMethod::Telex => {
                    self.systray
                        .set_menu_item_title(SystemTrayMenuItemKey::TypingMethodTelex, "Telex ✓");
                    self.systray
                        .set_menu_item_title(SystemTrayMenuItemKey::TypingMethodVNI, "VNI");
                    self.systray.set_menu_item_title(
                        SystemTrayMenuItemKey::TypingMethodTelexVNI,
                        "Telex+VNI",
                    );
                }
                TypingMethod::TelexVNI => {
                    self.systray
                        .set_menu_item_title(SystemTrayMenuItemKey::TypingMethodTelex, "Telex");
                    self.systray
                        .set_menu_item_title(SystemTrayMenuItemKey::TypingMethodVNI, "VNI");
                    self.systray.set_menu_item_title(
                        SystemTrayMenuItemKey::TypingMethodTelexVNI,
                        "Telex+VNI ✓",
                    );
                }
            }
            // Update localizable menu items
            self.systray
                .set_menu_item_title(SystemTrayMenuItemKey::ShowUI, t("menu.open_panel"));
            self.systray
                .set_menu_item_title(SystemTrayMenuItemKey::Exit, t("menu.quit"));
        }
    }

    fn setup_system_tray_actions(&mut self) {
        self.systray
            .set_menu_item_callback(SystemTrayMenuItemKey::ShowUI, || {
                UI_EVENT_SINK
                    .get()
                    .map(|event| Some(event.submit_command(SHOW_UI, (), Target::Auto)));
            });
        self.systray
            .set_menu_item_callback(SystemTrayMenuItemKey::Enable, || {
                unsafe {
                    INPUT_STATE.toggle_vietnamese();
                    update_systray_title_immediately();
                }
                UI_EVENT_SINK
                    .get()
                    .map(|event| Some(event.submit_command(UPDATE_UI, (), Target::Auto)));
            });
        self.systray
            .set_menu_item_callback(SystemTrayMenuItemKey::TypingMethodTelex, || {
                unsafe {
                    INPUT_STATE.set_method(TypingMethod::Telex);
                }
                UI_EVENT_SINK
                    .get()
                    .map(|event| Some(event.submit_command(UPDATE_UI, (), Target::Auto)));
            });
        self.systray
            .set_menu_item_callback(SystemTrayMenuItemKey::Typi
Download .txt
gitextract_0uwjvva_/

├── .editorconfig
├── .github/
│   ├── FUNDING.yml
│   └── workflows/
│       ├── main.yml
│       ├── pr.yml
│       └── update-cask.yml
├── .gitignore
├── CLAUDE.md
├── Cargo.toml
├── Casks/
│   └── goxkey.rb
├── DEVELOPMENT.md
├── LICENSE
├── Makefile
├── NIGHTLY_RELEASE.md
├── README.md
├── icons/
│   └── icon.icns
├── scripts/
│   ├── pre-commit
│   └── release
└── src/
    ├── config.rs
    ├── hotkey.rs
    ├── input.rs
    ├── main.rs
    ├── platform/
    │   ├── linux.rs
    │   ├── macos.rs
    │   ├── macos_ext.rs
    │   ├── mod.rs
    │   └── windows.rs
    ├── scripting/
    │   ├── mod.rs
    │   └── parser.rs
    └── ui/
        ├── colors.rs
        ├── controllers.rs
        ├── data.rs
        ├── locale.rs
        ├── mod.rs
        ├── selectors.rs
        ├── views.rs
        └── widgets.rs
Download .txt
SYMBOL INDEX (524 symbols across 18 files)

FILE: src/config.rs
  type ConfigStore (line 17) | pub struct ConfigStore {
    method get_config_path (line 57) | fn get_config_path() -> PathBuf {
    method write_config_data (line 63) | fn write_config_data(&mut self) -> Result<()> {
    method new (line 108) | pub fn new() -> Self {
    method get_hotkey (line 169) | pub fn get_hotkey(&self) -> &str {
    method set_hotkey (line 173) | pub fn set_hotkey(&mut self, hotkey: &str) {
    method get_method (line 179) | pub fn get_method(&self) -> &str {
    method set_method (line 183) | pub fn set_method(&mut self, method: &str) {
    method is_vietnamese_app (line 188) | pub fn is_vietnamese_app(&self, app_name: &str) -> bool {
    method is_english_app (line 192) | pub fn is_english_app(&self, app_name: &str) -> bool {
    method get_vn_apps (line 196) | pub fn get_vn_apps(&self) -> Vec<String> {
    method get_en_apps (line 200) | pub fn get_en_apps(&self) -> Vec<String> {
    method add_vietnamese_app (line 204) | pub fn add_vietnamese_app(&mut self, app_name: &str) {
    method add_english_app (line 214) | pub fn add_english_app(&mut self, app_name: &str) {
    method remove_vietnamese_app (line 224) | pub fn remove_vietnamese_app(&mut self, app_name: &str) {
    method remove_english_app (line 229) | pub fn remove_english_app(&mut self, app_name: &str) {
    method is_allowed_word (line 234) | pub fn is_allowed_word(&self, word: &str) -> bool {
    method is_auto_toggle_enabled (line 238) | pub fn is_auto_toggle_enabled(&self) -> bool {
    method set_auto_toggle_enabled (line 242) | pub fn set_auto_toggle_enabled(&mut self, flag: bool) {
    method is_gox_mode_enabled (line 247) | pub fn is_gox_mode_enabled(&self) -> bool {
    method set_gox_mode_enabled (line 251) | pub fn set_gox_mode_enabled(&mut self, flag: bool) {
    method is_w_literal_enabled (line 256) | pub fn is_w_literal_enabled(&self) -> bool {
    method set_w_literal_enabled (line 260) | pub fn set_w_literal_enabled(&mut self, flag: bool) {
    method get_ui_language (line 265) | pub fn get_ui_language(&self) -> &str {
    method set_ui_language (line 269) | pub fn set_ui_language(&mut self, lang: &str) {
    method is_macro_enabled (line 274) | pub fn is_macro_enabled(&self) -> bool {
    method set_macro_enabled (line 278) | pub fn set_macro_enabled(&mut self, flag: bool) {
    method is_macro_autocap_enabled (line 283) | pub fn is_macro_autocap_enabled(&self) -> bool {
    method set_macro_autocap_enabled (line 287) | pub fn set_macro_autocap_enabled(&mut self, flag: bool) {
    method get_macro_table (line 292) | pub fn get_macro_table(&self) -> &BTreeMap<String, String> {
    method add_macro (line 296) | pub fn add_macro(&mut self, from: String, to: String) {
    method delete_macro (line 301) | pub fn delete_macro(&mut self, from: &String) {
    method save (line 307) | fn save(&mut self) {
  function parse_vec_string (line 32) | fn parse_vec_string(line: String) -> Vec<String> {
  function parse_kv_string (line 39) | pub(crate) fn parse_kv_string(line: &str) -> Option<(String, String)> {
  function build_kv_string (line 48) | pub(crate) fn build_kv_string(k: &str, v: &str) -> String {
  constant HOTKEY_CONFIG_KEY (line 312) | const HOTKEY_CONFIG_KEY: &str = "hotkey";
  constant TYPING_METHOD_CONFIG_KEY (line 313) | const TYPING_METHOD_CONFIG_KEY: &str = "method";
  constant VN_APPS_CONFIG_KEY (line 314) | const VN_APPS_CONFIG_KEY: &str = "vn-apps";
  constant EN_APPS_CONFIG_KEY (line 315) | const EN_APPS_CONFIG_KEY: &str = "en-apps";
  constant MACRO_ENABLED_CONFIG_KEY (line 316) | const MACRO_ENABLED_CONFIG_KEY: &str = "is_macro_enabled";
  constant MACRO_AUTOCAP_ENABLED_CONFIG_KEY (line 317) | const MACRO_AUTOCAP_ENABLED_CONFIG_KEY: &str = "is_macro_autocap_enabled";
  constant AUTOS_TOGGLE_ENABLED_CONFIG_KEY (line 318) | const AUTOS_TOGGLE_ENABLED_CONFIG_KEY: &str = "is_auto_toggle_enabled";
  constant MACROS_CONFIG_KEY (line 319) | const MACROS_CONFIG_KEY: &str = "macros";
  constant GOX_MODE_CONFIG_KEY (line 320) | const GOX_MODE_CONFIG_KEY: &str = "is_gox_mode_enabled";
  constant W_LITERAL_CONFIG_KEY (line 321) | const W_LITERAL_CONFIG_KEY: &str = "is_w_literal_enabled";
  constant UI_LANGUAGE_CONFIG_KEY (line 322) | const UI_LANGUAGE_CONFIG_KEY: &str = "ui_language";
  constant ALLOWED_WORDS_CONFIG_KEY (line 323) | const ALLOWED_WORDS_CONFIG_KEY: &str = "allowed_words";

FILE: src/hotkey.rs
  type Hotkey (line 8) | pub struct Hotkey {
    method from_str (line 14) | pub fn from_str(input: &str) -> Self {
    method is_match (line 36) | pub fn is_match(&self, mut modifiers: KeyModifier, keycode: Option<cha...
    method inner (line 46) | pub fn inner(&self) -> (KeyModifier, Option<char>) {
  method fmt (line 52) | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
  function test_parse (line 78) | fn test_parse() {
  function test_parse_long_input (line 89) | fn test_parse_long_input() {
  function test_parse_with_named_keycode (line 102) | fn test_parse_with_named_keycode() {
  function test_can_match_with_or_without_capslock (line 113) | fn test_can_match_with_or_without_capslock() {
  function test_parse_with_just_modifiers (line 125) | fn test_parse_with_just_modifiers() {
  function test_display (line 136) | fn test_display() {

FILE: src/input.rs
  constant MAX_POSSIBLE_WORD_LENGTH (line 20) | const MAX_POSSIBLE_WORD_LENGTH: usize = 10;
  constant MAX_DUPLICATE_LENGTH (line 21) | const MAX_DUPLICATE_LENGTH: usize = 4;
  constant TONE_DUPLICATE_PATTERNS (line 22) | const TONE_DUPLICATE_PATTERNS: [&str; 17] = [
  constant PREDEFINED_CHARS (line 32) | pub const PREDEFINED_CHARS: [char; 47] = [
  constant STOP_TRACKING_WORDS (line 38) | pub const STOP_TRACKING_WORDS: [&str; 4] = [";", "'", "?", "/"];
  type CapPattern (line 43) | enum CapPattern {
  function detect_cap_pattern (line 49) | fn detect_cap_pattern(s: &str) -> CapPattern {
  function apply_cap_pattern (line 63) | fn apply_cap_pattern(s: &str, pattern: CapPattern) -> String {
  function mask_standalone_w (line 77) | fn mask_standalone_w(buffer: &str) -> String {
  function get_key_from_char (line 110) | pub fn get_key_from_char(c: char) -> rdev::Key {
  function build_keyboard_layout_map (line 166) | fn build_keyboard_layout_map(map: &mut HashMap<char, char>) {
  function rebuild_keyboard_layout_map (line 178) | pub fn rebuild_keyboard_layout_map() {
  type TypingMethod (line 196) | pub enum TypingMethod {
  type Err (line 203) | type Err = ();
  method from_str (line 205) | fn from_str(s: &str) -> Result<Self, Self::Err> {
  method fmt (line 215) | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
  function get_diff_parts (line 250) | pub fn get_diff_parts<'a>(old: &str, new: &'a str) -> (usize, &'a str) {
  type InputState (line 288) | pub struct InputState {
    method new (line 310) | pub fn new() -> Self {
    method update_active_app (line 334) | pub fn update_active_app(&mut self) -> Option<()> {
    method set_temporary_disabled (line 352) | pub fn set_temporary_disabled(&mut self) {
    method is_gox_mode_enabled (line 356) | pub fn is_gox_mode_enabled(&self) -> bool {
    method is_w_literal_enabled (line 360) | pub fn is_w_literal_enabled(&self) -> bool {
    method toggle_w_literal (line 364) | pub fn toggle_w_literal(&mut self) {
    method is_enabled (line 372) | pub fn is_enabled(&self) -> bool {
    method is_tracking (line 376) | pub fn is_tracking(&self) -> bool {
    method is_buffer_empty (line 380) | pub fn is_buffer_empty(&self) -> bool {
    method new_word (line 384) | pub fn new_word(&mut self) {
    method mark_resumable (line 397) | pub fn mark_resumable(&mut self) {
    method try_resume_previous_word (line 403) | pub fn try_resume_previous_word(&mut self) -> bool {
    method get_macro_target (line 414) | pub fn get_macro_target(&self) -> Option<String> {
    method is_macro_autocap_enabled (line 433) | pub fn is_macro_autocap_enabled(&self) -> bool {
    method toggle_macro_autocap (line 437) | pub fn toggle_macro_autocap(&mut self) {
    method get_typing_buffer (line 445) | pub fn get_typing_buffer(&self) -> &str {
    method get_displaying_word (line 449) | pub fn get_displaying_word(&self) -> &str {
    method stop_tracking (line 453) | pub fn stop_tracking(&mut self) {
    method toggle_vietnamese (line 458) | pub fn toggle_vietnamese(&mut self) {
    method add_vietnamese_app (line 470) | pub fn add_vietnamese_app(&mut self, app_name: &str) {
    method add_english_app (line 474) | pub fn add_english_app(&mut self, app_name: &str) {
    method remove_vietnamese_app (line 478) | pub fn remove_vietnamese_app(&mut self, app_name: &str) {
    method remove_english_app (line 485) | pub fn remove_english_app(&mut self, app_name: &str) {
    method get_vn_apps (line 489) | pub fn get_vn_apps(&self) -> Vec<String> {
    method get_en_apps (line 493) | pub fn get_en_apps(&self) -> Vec<String> {
    method set_method (line 497) | pub fn set_method(&mut self, method: TypingMethod) {
    method get_method (line 509) | pub fn get_method(&self) -> TypingMethod {
    method set_hotkey (line 513) | pub fn set_hotkey(&mut self, key_sequence: &str) {
    method get_hotkey (line 521) | pub fn get_hotkey(&self) -> &Hotkey {
    method is_auto_toggle_enabled (line 525) | pub fn is_auto_toggle_enabled(&self) -> bool {
    method toggle_auto_toggle (line 529) | pub fn toggle_auto_toggle(&mut self) {
    method is_macro_enabled (line 537) | pub fn is_macro_enabled(&self) -> bool {
    method toggle_macro_enabled (line 541) | pub fn toggle_macro_enabled(&mut self) {
    method get_macro_table (line 549) | pub fn get_macro_table(&self) -> &BTreeMap<String, String> {
    method delete_macro (line 553) | pub fn delete_macro(&mut self, from: &String) {
    method add_macro (line 558) | pub fn add_macro(&mut self, from: String, to: String) {
    method export_macros_to_file (line 566) | pub fn export_macros_to_file(&self, path: &str) -> std::io::Result<()> {
    method import_macros_from_file (line 577) | pub fn import_macros_from_file(&mut self, path: &str) -> std::io::Resu...
    method should_transform_keys (line 598) | pub fn should_transform_keys(&self, c: &char) -> bool {
    method transform_keys (line 602) | pub fn transform_keys(&self) -> Result<(String, TransformResult), ()> {
    method should_send_keyboard_event (line 660) | pub fn should_send_keyboard_event(&self, word: &str) -> bool {
    method should_dismiss_selection_if_needed (line 664) | pub fn should_dismiss_selection_if_needed(&self) -> bool {
    method get_backspace_count (line 669) | pub fn get_backspace_count(&self, is_delete: bool) -> usize {
    method replace (line 684) | pub fn replace(&mut self, buf: String) {
    method push (line 688) | pub fn push(&mut self, c: char) {
    method pop (line 705) | pub fn pop(&mut self) {
    method clear (line 713) | pub fn clear(&mut self) {
    method get_previous_word (line 720) | pub fn get_previous_word(&self) -> &str {
    method clear_previous_word (line 724) | pub fn clear_previous_word(&mut self) {
    method previous_word_is_stop_tracking_words (line 728) | pub fn previous_word_is_stop_tracking_words(&self) -> bool {
    method should_stop_tracking (line 732) | pub fn should_stop_tracking(&mut self) -> bool {
    method stop_tracking_if_needed (line 753) | pub fn stop_tracking_if_needed(&mut self) {
    method get_previous_modifiers (line 760) | pub fn get_previous_modifiers(&self) -> KeyModifier {
    method save_previous_modifiers (line 764) | pub fn save_previous_modifiers(&mut self, modifiers: KeyModifier) {
    method is_allowed_word (line 768) | pub fn is_allowed_word(&self, word: &str) -> bool {
  function tone_on_vowel_preserves_consonant_prefix (line 782) | fn tone_on_vowel_preserves_consonant_prefix() {
  function circumflex_application (line 790) | fn circumflex_application() {
  function multi_char_prefix_preserved (line 798) | fn multi_char_prefix_preserved() {
  function longer_common_prefix (line 806) | fn longer_common_prefix() {
  function identical_strings_no_op (line 816) | fn identical_strings_no_op() {
  function both_empty (line 825) | fn both_empty() {
  function old_empty_new_nonempty (line 832) | fn old_empty_new_nonempty() {
  function old_nonempty_new_empty (line 839) | fn old_nonempty_new_empty() {
  function new_is_prefix_of_old (line 849) | fn new_is_prefix_of_old() {
  function old_is_prefix_of_new (line 857) | fn old_is_prefix_of_new() {
  function no_common_prefix (line 866) | fn no_common_prefix() {
  function char_count_not_byte_count (line 877) | fn char_count_not_byte_count() {
  function all_multibyte_no_common_prefix (line 884) | fn all_multibyte_no_common_prefix() {
  function telex_moo_to_mo_hat (line 894) | fn telex_moo_to_mo_hat() {
  function telex_cas_to_ca_sac (line 902) | fn telex_cas_to_ca_sac() {
  function telex_viet_transform (line 910) | fn telex_viet_transform() {
  function tone_cycling_preserves_prefix (line 918) | fn tone_cycling_preserves_prefix() {
  function suffix_is_valid_utf8_slice_of_new (line 927) | fn suffix_is_valid_utf8_slice_of_new() {
  function standalone_w_is_masked (line 943) | fn standalone_w_is_masked() {
  function standalone_upper_w_is_masked (line 950) | fn standalone_upper_w_is_masked() {
  function w_after_eligible_vowel_is_not_masked (line 956) | fn w_after_eligible_vowel_is_not_masked() {
  function ww_after_eligible_vowel_not_masked (line 964) | fn ww_after_eligible_vowel_not_masked() {
  function standalone_ww_both_masked (line 973) | fn standalone_ww_both_masked() {
  function mixed_case_ww_after_eligible (line 980) | fn mixed_case_ww_after_eligible() {
  function stop_tracking_disables_tracking (line 991) | fn stop_tracking_disables_tracking() {
  function new_word_re_enables_tracking_after_stop (line 1001) | fn new_word_re_enables_tracking_after_stop() {
  function pop_to_empty_then_new_word_re_enables_tracking (line 1011) | fn pop_to_empty_then_new_word_re_enables_tracking() {
  function resume_previous_word_re_enables_tracking (line 1034) | fn resume_previous_word_re_enables_tracking() {

FILE: src/main.rs
  constant APP_VERSION (line 27) | const APP_VERSION: &str = env!("CARGO_PKG_VERSION");
  function apply_capslock_to_output (line 29) | fn apply_capslock_to_output(output: String, is_capslock: bool) -> String {
  function normalize_input_char (line 37) | fn normalize_input_char(c: char, is_shift: bool) -> char {
  function do_transform_keys (line 45) | fn do_transform_keys(handle: Handle, is_delete: bool, is_capslock: bool)...
  function do_restore_word (line 149) | fn do_restore_word(handle: Handle, is_capslock: bool) {
  function should_restore_transformed_word (line 162) | fn should_restore_transformed_word(
  function do_macro_replace (line 180) | fn do_macro_replace(handle: Handle, target: &String) {
  function update_systray_title_immediately (line 193) | pub unsafe fn update_systray_title_immediately() {
  function toggle_vietnamese (line 214) | unsafe fn toggle_vietnamese() {
  function auto_toggle_vietnamese (line 224) | unsafe fn auto_toggle_vietnamese() {
  function event_handler (line 240) | fn event_handler(
  function restore_when_invalid_and_not_allowed (line 429) | fn restore_when_invalid_and_not_allowed() {
  function no_restore_for_valid_word (line 436) | fn no_restore_for_valid_word() {
  function no_restore_for_allowed_word (line 443) | fn no_restore_for_allowed_word() {
  function no_restore_for_vni_numeric_shorthand (line 450) | fn no_restore_for_vni_numeric_shorthand() {
  function restore_for_vni_invalid_without_numeric_shorthand (line 457) | fn restore_for_vni_invalid_without_numeric_shorthand() {
  function normalize_input_char_only_depends_on_shift (line 464) | fn normalize_input_char_only_depends_on_shift() {
  function apply_capslock_to_transformed_output (line 470) | fn apply_capslock_to_transformed_output() {
  function capslock_path_keeps_telex_tone_position (line 477) | fn capslock_path_keeps_telex_tone_position() {
  function no_send_needed_for_plain_letter_with_capslock_only_case_change (line 485) | fn no_send_needed_for_plain_letter_with_capslock_only_case_change() {
  function main (line 494) | fn main() {

FILE: src/platform/linux.rs
  constant SYMBOL_SHIFT (line 7) | pub const SYMBOL_SHIFT: &str = "⇧";
  constant SYMBOL_CTRL (line 8) | pub const SYMBOL_CTRL: &str = "⌃";
  constant SYMBOL_SUPER (line 9) | pub const SYMBOL_SUPER: &str = "❖";
  constant SYMBOL_ALT (line 10) | pub const SYMBOL_ALT: &str = "⌥";
  function get_home_dir (line 12) | pub fn get_home_dir() -> Option<PathBuf> {
  function send_backspace (line 16) | pub fn send_backspace(count: usize) -> Result<(), ()> {
  function send_string (line 20) | pub fn send_string(string: &str) -> Result<(), ()> {
  function run_event_listener (line 24) | pub fn run_event_listener(callback: &CallbackFn) {
  function ensure_accessibility_permission (line 28) | pub fn ensure_accessibility_permission() -> bool {
  function is_in_text_selection (line 32) | pub fn is_in_text_selection() -> bool {
  function update_launch_on_login (line 36) | pub fn update_launch_on_login(is_enable: bool) {
  function is_launch_on_login (line 40) | pub fn is_launch_on_login() {

FILE: src/platform/macos.rs
  constant SYMBOL_SHIFT (line 48) | pub const SYMBOL_SHIFT: &str = "⇧";
  constant SYMBOL_CTRL (line 49) | pub const SYMBOL_CTRL: &str = "⌃";
  constant SYMBOL_SUPER (line 50) | pub const SYMBOL_SUPER: &str = "⌘";
  constant SYMBOL_ALT (line 51) | pub const SYMBOL_ALT: &str = "⌥";
  method from (line 54) | fn from(value: CGEventType) -> Self {
  function get_current_app_path (line 78) | fn get_current_app_path() -> String {
  function get_home_dir (line 107) | pub fn get_home_dir() -> Option<PathBuf> {
  function get_char (line 112) | fn get_char(keycode: CGKeyCode) -> Option<PressedKey> {
  function is_in_text_selection (line 172) | pub fn is_in_text_selection() -> bool {
  function send_backspace (line 197) | pub fn send_backspace(handle: Handle, count: usize) -> Result<(), ()> {
  function send_arrow_left (line 214) | pub fn send_arrow_left(handle: Handle, count: usize) -> Result<(), ()> {
  function send_arrow_right (line 235) | pub fn send_arrow_right(handle: Handle, count: usize) -> Result<(), ()> {
  function send_string (line 260) | pub fn send_string(handle: Handle, string: &str) -> Result<(), ()> {
  function add_app_change_callback (line 274) | pub fn add_app_change_callback<F>(cb: F)
  function add_appearance_change_callback (line 281) | pub fn add_appearance_change_callback<F>(cb: F)
  function run_event_listener (line 288) | pub fn run_event_listener(callback: &CallbackFn) {
  function is_process_trusted (line 365) | pub fn is_process_trusted() -> bool {
  function ensure_accessibility_permission (line 369) | pub fn ensure_accessibility_permission() -> bool {
  function get_app_icon_rgba (line 383) | pub fn get_app_icon_rgba(app_path: &str, size: u32) -> Option<(Vec<u8>, ...
  function get_preferred_language (line 454) | pub fn get_preferred_language() -> String {
  function get_active_app_name (line 465) | pub fn get_active_app_name() -> String {
  function update_launch_on_login (line 475) | pub fn update_launch_on_login(is_enable: bool) -> Result<(), auto_launch...
  function is_launch_on_login (line 482) | pub fn is_launch_on_login() -> bool {
  function is_dark_mode (line 486) | pub fn is_dark_mode() -> bool {

FILE: src/platform/macos_ext.rs
  type Wrapper (line 32) | struct Wrapper(*mut objc::runtime::Object);
  method same (line 34) | fn same(&self, _other: &Self) -> bool {
  type SystemTrayMenuItemKey (line 39) | pub enum SystemTrayMenuItemKey {
  type SystemTray (line 49) | pub struct SystemTray {
    method new (line 56) | pub fn new() -> Self {
    method set_title (line 83) | pub fn set_title(&mut self, title: &str, is_vietnamese: bool) {
    method init_menu_items (line 94) | pub fn init_menu_items(&self) {
    method add_menu_separator (line 107) | pub fn add_menu_separator(&self) {
    method add_menu_item (line 113) | pub fn add_menu_item<F>(&self, label: &str, cb: F)
    method get_menu_item_index_by_key (line 131) | pub fn get_menu_item_index_by_key(&self, key: SystemTrayMenuItemKey) -...
    method set_menu_item_title (line 142) | pub fn set_menu_item_title(&self, key: SystemTrayMenuItemKey, label: &...
    method set_menu_item_callback (line 152) | pub fn set_menu_item_callback<F>(&self, key: SystemTrayMenuItemKey, cb...
  function create_badge_image (line 166) | unsafe fn create_badge_image(title: &str, _is_vietnamese: bool) -> id {
  function dispatch_set_systray_title (line 255) | pub fn dispatch_set_systray_title(title: &str, is_vietnamese: bool) {
  type Handle (line 290) | pub type Handle = CGEventTapProxy;
  function CGEventTapPostEvent (line 294) | pub(crate) fn CGEventTapPostEvent(proxy: CGEventTapProxy, event: sys::CG...
  function CGEventCreateKeyboardEvent (line 295) | pub(crate) fn CGEventCreateKeyboardEvent(
  function CGEventKeyboardSetUnicodeString (line 300) | pub(crate) fn CGEventKeyboardSetUnicodeString(
  type CGEventTapCallBackInternal (line 327) | type CGEventTapCallBackInternal = unsafe extern "C" fn(
  function CGEventTapCreate (line 336) | fn CGEventTapCreate(
  function CGEventTapEnable (line 344) | fn CGEventTapEnable(tap: CFMachPortRef, enable: bool);
  function cg_new_tap_callback_internal (line 348) | unsafe extern "C" fn cg_new_tap_callback_internal(
  type CallbackType (line 373) | type CallbackType<'tap_life> =
  type CGEventTap (line 375) | pub struct CGEventTap<'tap_life> {
  function new (line 381) | pub fn new<F: Fn(CGEventTapProxy, CGEventType, &CGEvent) -> Option<CGEve...
  function enable (line 417) | pub fn enable(&self) {
  type Callback (line 423) | pub(crate) enum Callback {}
    method from (line 431) | pub(crate) fn from(cb: Box<dyn Fn()>) -> Id<Self> {
    method setptr (line 442) | pub(crate) fn setptr(&mut self, uptr: usize) {
  type CallbackState (line 426) | pub(crate) struct CallbackState {
  method class (line 451) | fn class() -> &'static Class {
  function AXIsProcessTrustedWithOptions (line 489) | pub fn AXIsProcessTrustedWithOptions(options: CFDictionaryRef) -> bool;
  function dispatch_async_f (line 501) | fn dispatch_async_f(
  function defer_open_app_file_picker (line 510) | pub fn defer_open_app_file_picker(callback: Box<dyn FnOnce(Option<String...
  function open_app_file_picker (line 525) | pub fn open_app_file_picker() -> Option<String> {
  function open_text_file_picker (line 561) | pub fn open_text_file_picker() -> Option<String> {
  function save_text_file_picker (line 585) | pub fn save_text_file_picker() -> Option<String> {
  function defer_open_text_file_picker (line 608) | pub fn defer_open_text_file_picker(callback: Box<dyn FnOnce(Option<Strin...
  function defer_save_text_file_picker (line 623) | pub fn defer_save_text_file_picker(callback: Box<dyn FnOnce(Option<Strin...
  function add_app_change_callback (line 638) | pub fn add_app_change_callback<F>(cb: F)
  function add_appearance_change_callback (line 656) | pub fn add_appearance_change_callback<F>(cb: F)

FILE: src/platform/mod.rs
  constant RAW_KEY_GLOBE (line 23) | pub const RAW_KEY_GLOBE: u16 = 0xb3;
  constant RAW_ARROW_DOWN (line 24) | pub const RAW_ARROW_DOWN: u16 = 0x7d;
  constant RAW_ARROW_UP (line 25) | pub const RAW_ARROW_UP: u16 = 0x7e;
  constant RAW_ARROW_LEFT (line 26) | pub const RAW_ARROW_LEFT: u16 = 0x7b;
  constant RAW_ARROW_RIGHT (line 27) | pub const RAW_ARROW_RIGHT: u16 = 0x7c;
  constant KEY_ENTER (line 28) | pub const KEY_ENTER: char = '\x13';
  constant KEY_SPACE (line 29) | pub const KEY_SPACE: char = '\u{0020}';
  constant KEY_TAB (line 30) | pub const KEY_TAB: char = '\x09';
  constant KEY_DELETE (line 31) | pub const KEY_DELETE: char = '\x08';
  constant KEY_ESCAPE (line 32) | pub const KEY_ESCAPE: char = '\x26';
  method fmt (line 46) | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
  method new (line 67) | pub fn new() -> Self {
  method apply (line 71) | pub fn apply(
  method add_shift (line 86) | pub fn add_shift(&mut self) {
  method add_super (line 90) | pub fn add_super(&mut self) {
  method add_control (line 94) | pub fn add_control(&mut self) {
  method add_alt (line 98) | pub fn add_alt(&mut self) {
  method add_capslock (line 102) | pub fn add_capslock(&mut self) {
  method is_shift (line 106) | pub fn is_shift(&self) -> bool {
  method is_super (line 110) | pub fn is_super(&self) -> bool {
  method is_control (line 114) | pub fn is_control(&self) -> bool {
  method is_alt (line 118) | pub fn is_alt(&self) -> bool {
  method is_capslock (line 122) | pub fn is_capslock(&self) -> bool {
  type PressedKey (line 128) | pub enum PressedKey {
  type EventTapType (line 134) | pub enum EventTapType {
  type CallbackFn (line 140) | pub type CallbackFn = dyn Fn(os::Handle, EventTapType, Option<PressedKey...

FILE: src/platform/windows.rs
  constant SYMBOL_SHIFT (line 7) | pub const SYMBOL_SHIFT: &str = "⇧";
  constant SYMBOL_CTRL (line 8) | pub const SYMBOL_CTRL: &str = "⌃";
  constant SYMBOL_SUPER (line 9) | pub const SYMBOL_SUPER: &str = "⊞";
  constant SYMBOL_ALT (line 10) | pub const SYMBOL_ALT: &str = "⌥";
  function get_home_dir (line 12) | pub fn get_home_dir() -> Option<PathBuf> {
  function send_backspace (line 21) | pub fn send_backspace(count: usize) -> Result<(), ()> {
  function send_string (line 25) | pub fn send_string(string: &str) -> Result<(), ()> {
  function run_event_listener (line 29) | pub fn run_event_listener(callback: &CallbackFn) {
  function ensure_accessibility_permission (line 33) | pub fn ensure_accessibility_permission() -> bool {
  function is_in_text_selection (line 37) | pub fn is_in_text_selection() -> bool {
  function update_launch_on_login (line 41) | pub fn update_launch_on_login(is_enable: bool) {
  function is_launch_on_login (line 45) | pub fn is_launch_on_login() {

FILE: src/scripting/parser.rs
  type Program (line 29) | pub struct Program {
  type Import (line 45) | pub struct Import {
  type Block (line 65) | pub struct Block {
  type FunctionCall (line 83) | pub struct FunctionCall {
  function is_key_char (line 99) | fn is_key_char(c: char) -> bool {
  function parse_key (line 112) | fn parse_key(input: &str) -> IResult<&str, String> {
  function parse_key_list (line 125) | fn parse_key_list(input: &str) -> IResult<&str, Vec<String>> {
  function is_identifier_char (line 143) | fn is_identifier_char(c: char) -> bool {
  function parse_identifier (line 156) | fn parse_identifier(input: &str) -> IResult<&str, String> {
  function parse_identifier_list (line 169) | fn parse_identifier_list(input: &str) -> IResult<&str, Vec<String>> {
  function parse_import (line 185) | fn parse_import(input: &str) -> IResult<&str, Import> {
  function parse_import_list (line 208) | fn parse_import_list(input: &str) -> IResult<&str, Vec<Import>> {
  function parse_function_call (line 225) | fn parse_function_call(input: &str) -> IResult<&str, FunctionCall> {
  function parse_function_call_list (line 275) | fn parse_function_call_list(input: &str) -> IResult<&str, Vec<FunctionCa...
  function parse_block (line 298) | fn parse_block(input: &str) -> IResult<&str, Block> {
  function parse_program (line 338) | pub fn parse_program(input: &str) -> IResult<&str, Program> {
  function test_parse_key (line 358) | fn test_parse_key() {
  function test_parse_key_should_parse_a_single_key (line 366) | fn test_parse_key_should_parse_a_single_key() {
  function test_parse_key_list (line 374) | fn test_parse_key_list() {
  function test_parse_identifier (line 383) | fn test_parse_identifier() {
  function test_parse_identifier_list (line 391) | fn test_parse_identifier_list() {
  function test_parse_identifier_list_single_item (line 399) | fn test_parse_identifier_list_single_item() {
  function test_parse_key_list_single (line 407) | fn test_parse_key_list_single() {
  function parse_import_fail (line 415) | fn parse_import_fail() {
  function parse_import_fail_not_a_function (line 422) | fn parse_import_fail_not_a_function() {
  function parse_import_fail_no_module (line 429) | fn parse_import_fail_no_module() {
  function parse_import_fail_no_module_just_space (line 436) | fn parse_import_fail_no_module_just_space() {
  function parse_import_success (line 443) | fn parse_import_success() {
  function parse_import_list_success_single (line 456) | fn parse_import_list_success_single() {
  function parse_import_list_success (line 469) | fn parse_import_list_success() {
  function parse_function_call_fail (line 487) | fn parse_function_call_fail() {
  function parse_function_call_space_before_parens_fail (line 494) | fn parse_function_call_space_before_parens_fail() {
  function parse_function_call_success_with_no_params (line 501) | fn parse_function_call_success_with_no_params() {
  function parse_function_call_success_with_no_params_with_space (line 516) | fn parse_function_call_success_with_no_params_with_space() {
  function parse_function_call_success_with_single_param (line 531) | fn parse_function_call_success_with_single_param() {
  function parse_function_call_success_with_multiple_param (line 546) | fn parse_function_call_success_with_multiple_param() {
  function parse_function_call_success_with_single_param_with_single_key (line 561) | fn parse_function_call_success_with_single_param_with_single_key() {
  function parse_function_call_success_with_single_param_with_multiple_key (line 576) | fn parse_function_call_success_with_single_param_with_multiple_key() {
  function parse_function_call_success_with_multiple_param_with_single_key (line 591) | fn parse_function_call_success_with_multiple_param_with_single_key() {
  function parse_function_call_success_with_multiple_param_with_multiple_key (line 610) | fn parse_function_call_success_with_multiple_param_with_multiple_key() {
  function parse_function_call_fail_with_multiple_param_with_no_key (line 629) | fn parse_function_call_fail_with_multiple_param_with_no_key() {
  function parse_function_call_fail_for_unclosed_call (line 636) | fn parse_function_call_fail_for_unclosed_call() {
  function parse_function_call_list_fail (line 643) | fn parse_function_call_list_fail() {
  function parse_function_call_list_success_with_single_call (line 650) | fn parse_function_call_list_success_with_single_call() {
  function parse_function_call_list_success_with_multiple_call (line 665) | fn parse_function_call_list_success_with_multiple_call() {
  function parse_block_fail (line 696) | fn parse_block_fail() {
  function parse_block_fail_no_key (line 703) | fn parse_block_fail_no_key() {
  function parse_block_fail_empty_block (line 710) | fn parse_block_fail_empty_block() {
  function parse_block_success_single_key (line 717) | fn parse_block_success_single_key() {
  function parse_block_success_multiple_key (line 735) | fn parse_block_success_multiple_key() {
  function parse_block_success_multiple_key_multiple_calls (line 753) | fn parse_block_success_multiple_key_multiple_calls() {
  function parse_program_single_block (line 788) | fn parse_program_single_block() {
  function parse_program_single_block_with_import (line 809) | fn parse_program_single_block_with_import() {
  function parse_program_multiple_block (line 832) | fn parse_program_multiple_block() {
  function parse_program_multiple_block_with_multiple_import (line 871) | fn parse_program_multiple_block_with_multiple_import() {
  function parse_full_program_success (line 917) | fn parse_full_program_success() {

FILE: src/ui/colors.rs
  constant GREEN (line 4) | pub const GREEN: Color = Color::rgb8(26, 138, 110);
  constant GREEN_BG (line 5) | pub const GREEN_BG: Color = Color::rgba8(26, 138, 110, 20);
  type Theme (line 8) | pub struct Theme {
  function light_theme (line 41) | pub fn light_theme() -> Theme {
  function dark_theme (line 73) | pub fn dark_theme() -> Theme {
  function get_theme (line 105) | pub fn get_theme(is_dark: bool) -> Theme {
  function theme_from_env (line 113) | pub fn theme_from_env(env: &Env) -> Theme {
  constant BADGE_VI_BG (line 117) | pub const BADGE_VI_BG: Color = Color::rgba8(26, 138, 110, 20);
  constant BADGE_VI_BORDER (line 118) | pub const BADGE_VI_BORDER: Color = Color::rgb8(26, 138, 110);
  constant BADGE_EN_BG (line 119) | pub const BADGE_EN_BG: Color = Color::rgba8(58, 115, 199, 18);
  constant BADGE_EN_BORDER (line 120) | pub const BADGE_EN_BORDER: Color = Color::rgb8(58, 115, 199);

FILE: src/ui/controllers.rs
  type UIController (line 24) | pub struct UIController;
    method event (line 27) | fn event(
    method update (line 227) | fn update(
  type LetterKeyController (line 306) | pub(super) struct LetterKeyController;
    method event (line 309) | fn event(

FILE: src/ui/data.rs
  type MacroEntry (line 13) | pub(super) struct MacroEntry {
  type AppEntry (line 19) | pub(super) struct AppEntry {
  type UIDataAdapter (line 24) | pub struct UIDataAdapter {
    method new (line 70) | pub fn new() -> Self {
    method update (line 110) | pub fn update(&mut self) {
    method setup_system_tray_actions (line 230) | fn setup_system_tray_actions(&mut self) {
    method toggle_vietnamese (line 282) | pub fn toggle_vietnamese(&mut self) {

FILE: src/ui/locale.rs
  constant LANG_VI (line 3) | const LANG_VI: u8 = 0;
  constant LANG_EN (line 4) | const LANG_EN: u8 = 1;
  type Lang (line 7) | pub enum Lang {
  function resolve_lang (line 15) | fn resolve_lang(config_value: &str) -> Lang {
  function init_lang (line 36) | pub fn init_lang(config_value: &str) {
  function set_lang (line 48) | pub fn set_lang(lang: Lang) {
  function current_lang (line 58) | pub fn current_lang() -> Lang {
  function t (line 66) | pub fn t(key: &'static str) -> &'static str {

FILE: src/ui/mod.rs
  constant UPDATE_UI (line 19) | pub const UPDATE_UI: Selector = Selector::new("gox-ui.update-ui");
  constant SHOW_UI (line 20) | pub const SHOW_UI: Selector = Selector::new("gox-ui.show-ui");
  constant WINDOW_WIDTH (line 21) | pub const WINDOW_WIDTH: f64 = 480.0;
  constant WINDOW_HEIGHT (line 22) | pub const WINDOW_HEIGHT: f64 = 680.0;
  function format_letter_key (line 24) | pub fn format_letter_key(c: Option<char>) -> String {
  function letter_key_to_char (line 35) | pub fn letter_key_to_char(input: &str) -> Option<char> {

FILE: src/ui/selectors.rs
  constant DELETE_MACRO (line 3) | pub(super) const DELETE_MACRO: Selector<String> = Selector::new("gox-ui....
  constant ADD_MACRO (line 4) | pub(super) const ADD_MACRO: Selector = Selector::new("gox-ui.add-macro");
  constant DELETE_SELECTED_MACRO (line 5) | pub(super) const DELETE_SELECTED_MACRO: Selector = Selector::new("gox-ui...
  constant SET_EN_APP_FROM_PICKER (line 6) | pub(super) const SET_EN_APP_FROM_PICKER: Selector<String> =
  constant DELETE_SELECTED_APP (line 8) | pub(super) const DELETE_SELECTED_APP: Selector = Selector::new("gox-ui.d...
  constant TOGGLE_APP_MODE (line 9) | pub(super) const TOGGLE_APP_MODE: Selector<String> = Selector::new("gox-...
  constant SHOW_ADD_MACRO_DIALOG (line 10) | pub(super) const SHOW_ADD_MACRO_DIALOG: Selector = Selector::new("gox-ui...
  constant SHOW_EDIT_SHORTCUT_DIALOG (line 11) | pub(super) const SHOW_EDIT_SHORTCUT_DIALOG: Selector =
  constant SAVE_SHORTCUT (line 13) | pub(super) const SAVE_SHORTCUT: Selector<(bool, bool, bool, bool, String...
  constant RESET_DEFAULTS (line 15) | pub(super) const RESET_DEFAULTS: Selector = Selector::new("gox-ui.reset-...
  constant LOAD_MACROS_FROM_FILE (line 16) | pub(super) const LOAD_MACROS_FROM_FILE: Selector = Selector::new("gox-ui...
  constant EXPORT_MACROS_TO_FILE (line 17) | pub(super) const EXPORT_MACROS_TO_FILE: Selector = Selector::new("gox-ui...

FILE: src/ui/views.rs
  function text_label (line 32) | fn text_label(
  function title_label (line 48) | fn title_label(key: &'static str) -> impl Widget<UIDataAdapter> {
  function subtitle_label (line 52) | fn subtitle_label(key: &'static str) -> impl Widget<UIDataAdapter> {
  function title_subtitle_column (line 56) | fn title_subtitle_column(
  function centered_btn (line 66) | fn centered_btn(
  function make_text_layout (line 87) | fn make_text_layout(
  function draw_centered_text (line 101) | fn draw_centered_text(ctx: &mut druid::PaintCtx, text: &str, font_size: ...
  function symbol_btn (line 113) | fn symbol_btn(symbol: &'static str) -> impl Widget<UIDataAdapter> {
  function remove_btn (line 121) | fn remove_btn(is_enabled_fn: fn(&UIDataAdapter) -> bool) -> impl Widget<...
  function toolbar_btn (line 139) | fn toolbar_btn(key: &'static str, divider: Option<&'static str>) -> impl...
  function h_divider (line 159) | fn h_divider() -> impl Widget<UIDataAdapter> {
  function section_label (line 169) | fn section_label(key: &'static str) -> impl Widget<UIDataAdapter> {
  function card_divider (line 181) | fn card_divider() -> impl Widget<UIDataAdapter> {
  function settings_row (line 191) | fn settings_row<TW: Widget<UIDataAdapter> + 'static>(
  function settings_card (line 206) | fn settings_card<TW: Widget<UIDataAdapter> + 'static>(inner: TW) -> impl...
  function tab_body (line 218) | fn tab_body() -> Flex<UIDataAdapter> {
  constant TAB_PADDING (line 222) | const TAB_PADDING: (f64, f64, f64, f64) = (24.0, 20.0, 24.0, 24.0);
  function option_group (line 224) | fn option_group<SW: Widget<UIDataAdapter> + 'static>(
  function v_scroll (line 239) | fn v_scroll<W: Widget<UIDataAdapter> + 'static>(inner: W) -> Scroll<UIDa...
  function list_card (line 246) | fn list_card(
  function action_btn (line 266) | fn action_btn(
  function dialog_buttons (line 284) | fn dialog_buttons(
  function cancel_btn (line 296) | fn cancel_btn() -> impl Widget<UIDataAdapter> {
  function general_tab (line 307) | fn general_tab() -> impl Widget<UIDataAdapter> {
  function apps_tab (line 442) | fn apps_tab() -> impl Widget<UIDataAdapter> {
  function advanced_tab (line 521) | fn advanced_tab() -> impl Widget<UIDataAdapter> {
  function macro_row_item (line 583) | fn macro_row_item() -> impl Widget<MacroEntry> {
  function main_ui_builder (line 609) | pub fn main_ui_builder() -> impl Widget<UIDataAdapter> {
  function permission_request_ui_builder (line 643) | pub fn permission_request_ui_builder() -> impl Widget<()> {
  function macro_editor_ui_builder (line 688) | pub fn macro_editor_ui_builder() -> impl Widget<UIDataAdapter> {
  function center_window_position (line 725) | pub fn center_window_position() -> (f64, f64) {
  constant ADD_MACRO_DIALOG_WIDTH (line 732) | pub const ADD_MACRO_DIALOG_WIDTH: f64 = 340.0;
  constant ADD_MACRO_DIALOG_HEIGHT (line 733) | pub const ADD_MACRO_DIALOG_HEIGHT: f64 = 208.0;
  constant EDIT_SHORTCUT_DIALOG_WIDTH (line 735) | pub const EDIT_SHORTCUT_DIALOG_WIDTH: f64 = 340.0;
  constant EDIT_SHORTCUT_DIALOG_HEIGHT (line 736) | pub const EDIT_SHORTCUT_DIALOG_HEIGHT: f64 = 200.0;
  function styled_text_input (line 738) | fn styled_text_input(placeholder: &'static str) -> impl Widget<String> {
  function add_macro_dialog_ui_builder (line 758) | pub fn add_macro_dialog_ui_builder() -> impl Widget<UIDataAdapter> {
  function edit_shortcut_dialog_ui_builder (line 809) | pub fn edit_shortcut_dialog_ui_builder() -> impl Widget<UIDataAdapter> {

FILE: src/ui/widgets.rs
  type ToggleSwitch (line 20) | pub(super) struct ToggleSwitch;
    method event (line 23) | fn event(&mut self, ctx: &mut EventCtx, event: &Event, data: &mut bool...
    method lifecycle (line 40) | fn lifecycle(&mut self, _ctx: &mut LifeCycleCtx, _event: &LifeCycle, _...
    method update (line 43) | fn update(&mut self, ctx: &mut UpdateCtx, old_data: &bool, data: &bool...
    method layout (line 49) | fn layout(
    method paint (line 59) | fn paint(&mut self, ctx: &mut PaintCtx, data: &bool, env: &Env) {
  type StyledCheckbox (line 76) | pub(super) struct StyledCheckbox;
    method event (line 79) | fn event(&mut self, ctx: &mut EventCtx, event: &Event, data: &mut bool...
    method lifecycle (line 96) | fn lifecycle(&mut self, _ctx: &mut LifeCycleCtx, _event: &LifeCycle, _...
    method update (line 99) | fn update(&mut self, ctx: &mut UpdateCtx, old_data: &bool, data: &bool...
    method layout (line 105) | fn layout(
    method paint (line 115) | fn paint(&mut self, ctx: &mut PaintCtx, data: &bool, env: &Env) {
  type InfoTooltip (line 132) | pub(super) struct InfoTooltip {
    method new (line 138) | pub fn new(text: &'static str) -> Self {
    method event (line 147) | fn event(&mut self, _ctx: &mut EventCtx, _event: &Event, _data: &mut T...
    method lifecycle (line 149) | fn lifecycle(&mut self, ctx: &mut LifeCycleCtx, event: &LifeCycle, _da...
    method update (line 156) | fn update(&mut self, _ctx: &mut UpdateCtx, _old_data: &T, _data: &T, _...
    method layout (line 158) | fn layout(&mut self, ctx: &mut LayoutCtx, _bc: &BoxConstraints, _data:...
    method paint (line 163) | fn paint(&mut self, ctx: &mut PaintCtx, _data: &T, env: &Env) {
  type SegmentedControl (line 211) | pub(super) struct SegmentedControl {
    method new (line 217) | pub(super) fn new(options: Vec<(&str, TypingMethod)>) -> Self {
    method event (line 229) | fn event(&mut self, ctx: &mut EventCtx, event: &Event, data: &mut Typi...
    method lifecycle (line 241) | fn lifecycle(
    method update (line 250) | fn update(
    method layout (line 262) | fn layout(
    method paint (line 283) | fn paint(&mut self, ctx: &mut PaintCtx, data: &TypingMethod, env: &Env) {
  type U32SegmentedControl (line 329) | pub(super) struct U32SegmentedControl {
    method new (line 335) | pub(super) fn new(options: Vec<(&'static str, u32)>) -> Self {
    method event (line 344) | fn event(&mut self, ctx: &mut EventCtx, event: &Event, data: &mut u32,...
    method lifecycle (line 356) | fn lifecycle(&mut self, _ctx: &mut LifeCycleCtx, _event: &LifeCycle, _...
    method update (line 358) | fn update(&mut self, ctx: &mut UpdateCtx, old_data: &u32, data: &u32, ...
    method layout (line 364) | fn layout(
    method paint (line 385) | fn paint(&mut self, ctx: &mut PaintCtx, data: &u32, env: &Env) {
  type TabBar (line 431) | pub(super) struct TabBar {
    method new (line 436) | pub(super) fn new() -> Self {
    method draw_icon_general (line 442) | fn draw_icon_general(ctx: &mut PaintCtx, cx: f64, cy: f64, color: &Col...
    method draw_icon_apps (line 455) | fn draw_icon_apps(ctx: &mut PaintCtx, cx: f64, cy: f64, color: &Color) {
    method draw_icon_text_expansion (line 462) | fn draw_icon_text_expansion(ctx: &mut PaintCtx, cx: f64, cy: f64, colo...
    method event (line 492) | fn event(&mut self, ctx: &mut EventCtx, event: &Event, data: &mut u32,...
    method lifecycle (line 504) | fn lifecycle(&mut self, _ctx: &mut LifeCycleCtx, _event: &LifeCycle, _...
    method update (line 506) | fn update(&mut self, ctx: &mut UpdateCtx, old_data: &u32, data: &u32, ...
    method layout (line 512) | fn layout(
    method paint (line 528) | fn paint(&mut self, ctx: &mut PaintCtx, data: &u32, env: &Env) {
  type KeyBadge (line 572) | pub(super) struct KeyBadge {
    method new (line 577) | pub(super) fn new(label: impl Into<String>) -> Self {
    method event (line 585) | fn event(&mut self, _ctx: &mut EventCtx, _event: &Event, _data: &mut (...
    method lifecycle (line 586) | fn lifecycle(&mut self, _ctx: &mut LifeCycleCtx, _event: &LifeCycle, _...
    method update (line 587) | fn update(&mut self, _ctx: &mut UpdateCtx, _old: &(), _data: &(), _env...
    method layout (line 589) | fn layout(
    method paint (line 600) | fn paint(&mut self, ctx: &mut PaintCtx, _data: &(), env: &Env) {
  type HotkeyBadgesWidget (line 620) | pub(super) struct HotkeyBadgesWidget {
    method new (line 628) | pub(super) fn new() -> Self {
    method rebuild_badges (line 637) | fn rebuild_badges(&mut self, display: &str) {
    method event (line 647) | fn event(&mut self, ctx: &mut EventCtx, event: &Event, data: &mut UIDa...
    method lifecycle (line 732) | fn lifecycle(
    method update (line 747) | fn update(
    method layout (line 764) | fn layout(
    method paint (line 787) | fn paint(&mut self, ctx: &mut PaintCtx, data: &UIDataAdapter, env: &En...
  type MacroListWidget (line 823) | pub(super) struct MacroListWidget {
    method new (line 831) | pub(super) fn new() -> Self {
    method event (line 839) | fn event(&mut self, ctx: &mut EventCtx, event: &Event, data: &mut UIDa...
    method lifecycle (line 851) | fn lifecycle(
    method update (line 860) | fn update(
    method layout (line 874) | fn layout(
    method paint (line 895) | fn paint(&mut self, ctx: &mut PaintCtx, data: &UIDataAdapter, env: &En...
  constant MACRO_ROW_HEIGHT (line 827) | const MACRO_ROW_HEIGHT: f64 = 44.0;
  constant MACRO_HEADER_HEIGHT (line 828) | const MACRO_HEADER_HEIGHT: f64 = 30.0;
  type CombinedAppEntry (line 1007) | pub(super) struct CombinedAppEntry {
  type AppsListWidget (line 1013) | pub(super) struct AppsListWidget {
    method new (line 1023) | pub(super) fn new() -> Self {
    method build_entries (line 1038) | pub(super) fn build_entries(data: &UIDataAdapter) -> Vec<CombinedAppEn...
    method initials (line 1062) | fn initials(name: &str) -> String {
    method event (line 1073) | fn event(&mut self, ctx: &mut EventCtx, event: &Event, data: &mut UIDa...
    method lifecycle (line 1105) | fn lifecycle(
    method update (line 1114) | fn update(
    method layout (line 1128) | fn layout(
    method paint (line 1166) | fn paint(&mut self, ctx: &mut PaintCtx, data: &UIDataAdapter, env: &En...
  constant ROW_HEIGHT (line 1020) | const ROW_HEIGHT: f64 = 52.0;
  type ShortcutCaptureWidget (line 1313) | pub(super) struct ShortcutCaptureWidget {
    method new (line 1322) | pub(super) fn new() -> Self {
    method event (line 1334) | fn event(&mut self, ctx: &mut EventCtx, event: &Event, data: &mut UIDa...
    method lifecycle (line 1412) | fn lifecycle(
    method update (line 1432) | fn update(
    method layout (line 1442) | fn layout(
    method paint (line 1452) | fn paint(&mut self, ctx: &mut PaintCtx, data: &UIDataAdapter, env: &En...
Condensed preview — 36 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (301K chars).
[
  {
    "path": ".editorconfig",
    "chars": 57,
    "preview": "[*.rs]\nindent_style = space\nindent_size = 4\ntab_width = 4"
  },
  {
    "path": ".github/FUNDING.yml",
    "chars": 81,
    "preview": "# These are supported funding model platforms\n\ngithub: huytd\nko_fi: thefullsnack\n"
  },
  {
    "path": ".github/workflows/main.yml",
    "chars": 1063,
    "preview": "on:\n  push:\n    branches:\n      - 'main'\n\nname: Stable\n\njobs:\n  test:\n    name: Test project\n    runs-on: macos-11 # add"
  },
  {
    "path": ".github/workflows/pr.yml",
    "chars": 516,
    "preview": "on:\n  push:\n    branches-ignore:\n      - 'main'\n\nname: Pull request\n\njobs:\n  test:\n    name: Test project\n    runs-on: m"
  },
  {
    "path": ".github/workflows/update-cask.yml",
    "chars": 1383,
    "preview": "on:\n  release:\n    types: [published]\n\nname: Update Homebrew Cask\n\njobs:\n  update-cask:\n    name: Update Casks/goxkey.rb"
  },
  {
    "path": ".gitignore",
    "chars": 23,
    "preview": "/target\n.DS_Store\n.idea"
  },
  {
    "path": "CLAUDE.md",
    "chars": 3412,
    "preview": "## Project Overview\n\nGõkey is a Vietnamese input method editor (IME) for macOS, written in Rust. It\nintercepts keyboard "
  },
  {
    "path": "Cargo.toml",
    "chars": 953,
    "preview": "[package]\ndescription = \"Bộ gõ tiếng Việt mã nguồn mở đa hệ điều hành Gõ Key\"\nedition = \"2021\"\nname = \"goxkey\"\nversion ="
  },
  {
    "path": "Casks/goxkey.rb",
    "chars": 632,
    "preview": "cask \"goxkey\" do\n  version \"0.3.0\"\n  sha256 \"e747009b9c78d2ea3d72ed5419c24090553cbc1c7095dc63145de89467c7649e\"\n\n  url \"h"
  },
  {
    "path": "DEVELOPMENT.md",
    "chars": 3074,
    "preview": "## Development\n\nCurrently, only macOS is supported. Windows and Linux could also be supported as well but it's not our p"
  },
  {
    "path": "LICENSE",
    "chars": 1495,
    "preview": "BSD 3-Clause License\n\nCopyright (c) 2023, Huy Tran\n\nRedistribution and use in source and binary forms, with or without\nm"
  },
  {
    "path": "Makefile",
    "chars": 1196,
    "preview": "VERSION := $(shell grep '^version' Cargo.toml | head -1 | sed 's/.*= *\"\\(.*\\)\"/\\1/')\n\nrun:\n\tcargo r\n\nbundle:\n\tcargo bund"
  },
  {
    "path": "NIGHTLY_RELEASE.md",
    "chars": 1423,
    "preview": "🍎 Bản build này được release tự động sau mỗi lần update code từ nhóm phát triển. Hiện tại chỉ mới hỗ trợ macOS.\n\n🖥 Để cà"
  },
  {
    "path": "README.md",
    "chars": 2811,
    "preview": "<p align=\"center\">\n\t<img src=\"./icons/icon.png\" width=\"90px\">\n</p>\n\n\n<img width=\"1585\" height=\"797\" alt=\"screenshots\" sr"
  },
  {
    "path": "scripts/pre-commit",
    "chars": 20,
    "preview": "cargo fmt\ngit add .\n"
  },
  {
    "path": "scripts/release",
    "chars": 408,
    "preview": "codesign -s \"Developer ID Application: Huy Tran\" --timestamp --options=runtime target/release/bundle/osx/GoKey.app\nditto"
  },
  {
    "path": "src/config.rs",
    "chars": 10010,
    "preview": "use std::collections::BTreeMap;\nuse std::io::BufRead;\nuse std::{\n    fs::File,\n    io,\n    io::{Result, Write},\n    path"
  },
  {
    "path": "src/hotkey.rs",
    "chars": 4932,
    "preview": "use std::fmt::Display;\n\nuse crate::platform::{\n    KeyModifier, KEY_DELETE, KEY_ENTER, KEY_ESCAPE, KEY_SPACE, KEY_TAB, S"
  },
  {
    "path": "src/input.rs",
    "chars": 33405,
    "preview": "use std::collections::BTreeMap;\nuse std::{collections::HashMap, fmt::Display, str::FromStr};\n\nuse druid::{Data, Target};"
  },
  {
    "path": "src/main.rs",
    "chars": 23375,
    "preview": "mod config;\nmod hotkey;\nmod input;\nmod platform;\nmod scripting;\nmod ui;\n\nuse std::thread;\n\nuse druid::{AppLauncher, ExtE"
  },
  {
    "path": "src/platform/linux.rs",
    "chars": 761,
    "preview": "// TODO: Implement this\n\nuse druid::{commands::CLOSE_WINDOW, Selector};\n\nuse super::CallbackFn;\n\npub const SYMBOL_SHIFT:"
  },
  {
    "path": "src/platform/macos.rs",
    "chars": 18101,
    "preview": "use std::env::current_exe;\nuse std::path::Path;\nuse std::{env, path::PathBuf, ptr};\n\nmod macos_ext;\nuse auto_launch::{Au"
  },
  {
    "path": "src/platform/macos_ext.rs",
    "chars": 22870,
    "preview": "use cocoa::appkit::{\n    NSApp, NSApplication, NSButton, NSMenu, NSMenuItem, NSStatusBar, NSStatusItem,\n};\nuse cocoa::ba"
  },
  {
    "path": "src/platform/mod.rs",
    "chars": 3739,
    "preview": "#[cfg_attr(target_os = \"macos\", path = \"macos.rs\")]\n#[cfg_attr(target_os = \"linux\", path = \"linux.rs\")]\n#[cfg_attr(targe"
  },
  {
    "path": "src/platform/windows.rs",
    "chars": 992,
    "preview": "// TODO: Implement this\n\nuse druid::{Selector, commands::CLOSE_WINDOW};\n\nuse super::CallbackFn;\n\npub const SYMBOL_SHIFT:"
  },
  {
    "path": "src/scripting/mod.rs",
    "chars": 2349,
    "preview": "/// This module, `parser`, is built for the goxscript language.\n/// It parses the goxscript language and returns an AST "
  },
  {
    "path": "src/scripting/parser.rs",
    "chars": 29126,
    "preview": "use nom::{\n    bytes::complete::{tag, take_while1, take_while_m_n},\n    character::complete::{multispace0, multispace1},"
  },
  {
    "path": "src/ui/colors.rs",
    "chars": 4287,
    "preview": "use druid::{Color, Env, Key};\nuse std::sync::Arc;\n\npub const GREEN: Color = Color::rgb8(26, 138, 110);\npub const GREEN_B"
  },
  {
    "path": "src/ui/controllers.rs",
    "chars": 13791,
    "preview": "use crate::{\n    input::{rebuild_keyboard_layout_map, INPUT_STATE},\n    platform::{\n        defer_open_text_file_picker,"
  },
  {
    "path": "src/ui/data.rs",
    "chars": 11083,
    "preview": "use std::sync::Arc;\n\nuse crate::{\n    input::{TypingMethod, INPUT_STATE},\n    platform::{is_dark_mode, is_launch_on_logi"
  },
  {
    "path": "src/ui/locale.rs",
    "chars": 8987,
    "preview": "use std::sync::atomic::{AtomicU8, Ordering};\n\nconst LANG_VI: u8 = 0;\nconst LANG_EN: u8 = 1;\n\n#[derive(Clone, Copy, Parti"
  },
  {
    "path": "src/ui/mod.rs",
    "chars": 1196,
    "preview": "mod colors;\nmod controllers;\nmod data;\npub(crate) mod locale;\nmod selectors;\nmod views;\nmod widgets;\n\nuse druid::Selecto"
  },
  {
    "path": "src/ui/selectors.rs",
    "chars": 1190,
    "preview": "use druid::Selector;\n\npub(super) const DELETE_MACRO: Selector<String> = Selector::new(\"gox-ui.delete-macro\");\npub(super)"
  },
  {
    "path": "src/ui/views.rs",
    "chars": 29129,
    "preview": "use crate::{input::TypingMethod, platform::defer_open_app_file_picker, UI_EVENT_SINK};\nuse druid::{\n    kurbo::RoundedRe"
  },
  {
    "path": "src/ui/widgets.rs",
    "chars": 48079,
    "preview": "use std::collections::HashMap;\n\nuse crate::input::TypingMethod;\nuse druid::{\n    kurbo::{BezPath, Circle, RoundedRect},\n"
  }
]

// ... and 1 more files (download for full content)

About this extraction

This page contains the full source code of the huytd/goxkey GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 36 files (279.2 KB), approximately 70.4k tokens, and a symbol index with 524 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.

Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.

Copied to clipboard!