Upload 12 files
Browse files- .gitignore +26 -0
- Cargo.lock +173 -0
- Cargo.toml +23 -0
- LICENSE +21 -0
- README.md +144 -0
- STACKS.md +71 -0
- src/hook.rs +193 -0
- src/main.rs +59 -0
- src/replacer.rs +111 -0
- src/spellcheck.rs +272 -0
- src/tray.rs +221 -0
- words.txt +0 -0
.gitignore
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Rust build artifacts
|
| 2 |
+
target/
|
| 3 |
+
|
| 4 |
+
# Compiled executables
|
| 5 |
+
*.exe
|
| 6 |
+
|
| 7 |
+
# Editor configs
|
| 8 |
+
.vscode/
|
| 9 |
+
*.code-workspace
|
| 10 |
+
|
| 11 |
+
# OS files
|
| 12 |
+
Thumbs.db
|
| 13 |
+
desktop.ini
|
| 14 |
+
|
| 15 |
+
# AutoHotkey backups
|
| 16 |
+
*.ahk~
|
| 17 |
+
|
| 18 |
+
# Runtime user data
|
| 19 |
+
data/user_words.txt
|
| 20 |
+
|
| 21 |
+
# Large generated data (misspelling-generator output)
|
| 22 |
+
data/misspellings.txt
|
| 23 |
+
|
| 24 |
+
# Legacy reference modules
|
| 25 |
+
lib/
|
| 26 |
+
misspelling-generator/
|
Cargo.lock
ADDED
|
@@ -0,0 +1,173 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# This file is automatically @generated by Cargo.
|
| 2 |
+
# It is not intended for manual editing.
|
| 3 |
+
version = 4
|
| 4 |
+
|
| 5 |
+
[[package]]
|
| 6 |
+
name = "autocorrect-type"
|
| 7 |
+
version = "1.0.0"
|
| 8 |
+
dependencies = [
|
| 9 |
+
"windows",
|
| 10 |
+
]
|
| 11 |
+
|
| 12 |
+
[[package]]
|
| 13 |
+
name = "proc-macro2"
|
| 14 |
+
version = "1.0.106"
|
| 15 |
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
| 16 |
+
checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934"
|
| 17 |
+
dependencies = [
|
| 18 |
+
"unicode-ident",
|
| 19 |
+
]
|
| 20 |
+
|
| 21 |
+
[[package]]
|
| 22 |
+
name = "quote"
|
| 23 |
+
version = "1.0.44"
|
| 24 |
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
| 25 |
+
checksum = "21b2ebcf727b7760c461f091f9f0f539b77b8e87f2fd88131e7f1b433b3cece4"
|
| 26 |
+
dependencies = [
|
| 27 |
+
"proc-macro2",
|
| 28 |
+
]
|
| 29 |
+
|
| 30 |
+
[[package]]
|
| 31 |
+
name = "syn"
|
| 32 |
+
version = "2.0.117"
|
| 33 |
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
| 34 |
+
checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99"
|
| 35 |
+
dependencies = [
|
| 36 |
+
"proc-macro2",
|
| 37 |
+
"quote",
|
| 38 |
+
"unicode-ident",
|
| 39 |
+
]
|
| 40 |
+
|
| 41 |
+
[[package]]
|
| 42 |
+
name = "unicode-ident"
|
| 43 |
+
version = "1.0.24"
|
| 44 |
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
| 45 |
+
checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75"
|
| 46 |
+
|
| 47 |
+
[[package]]
|
| 48 |
+
name = "windows"
|
| 49 |
+
version = "0.58.0"
|
| 50 |
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
| 51 |
+
checksum = "dd04d41d93c4992d421894c18c8b43496aa748dd4c081bac0dc93eb0489272b6"
|
| 52 |
+
dependencies = [
|
| 53 |
+
"windows-core",
|
| 54 |
+
"windows-targets",
|
| 55 |
+
]
|
| 56 |
+
|
| 57 |
+
[[package]]
|
| 58 |
+
name = "windows-core"
|
| 59 |
+
version = "0.58.0"
|
| 60 |
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
| 61 |
+
checksum = "6ba6d44ec8c2591c134257ce647b7ea6b20335bf6379a27dac5f1641fcf59f99"
|
| 62 |
+
dependencies = [
|
| 63 |
+
"windows-implement",
|
| 64 |
+
"windows-interface",
|
| 65 |
+
"windows-result",
|
| 66 |
+
"windows-strings",
|
| 67 |
+
"windows-targets",
|
| 68 |
+
]
|
| 69 |
+
|
| 70 |
+
[[package]]
|
| 71 |
+
name = "windows-implement"
|
| 72 |
+
version = "0.58.0"
|
| 73 |
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
| 74 |
+
checksum = "2bbd5b46c938e506ecbce286b6628a02171d56153ba733b6c741fc627ec9579b"
|
| 75 |
+
dependencies = [
|
| 76 |
+
"proc-macro2",
|
| 77 |
+
"quote",
|
| 78 |
+
"syn",
|
| 79 |
+
]
|
| 80 |
+
|
| 81 |
+
[[package]]
|
| 82 |
+
name = "windows-interface"
|
| 83 |
+
version = "0.58.0"
|
| 84 |
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
| 85 |
+
checksum = "053c4c462dc91d3b1504c6fe5a726dd15e216ba718e84a0e46a88fbe5ded3515"
|
| 86 |
+
dependencies = [
|
| 87 |
+
"proc-macro2",
|
| 88 |
+
"quote",
|
| 89 |
+
"syn",
|
| 90 |
+
]
|
| 91 |
+
|
| 92 |
+
[[package]]
|
| 93 |
+
name = "windows-result"
|
| 94 |
+
version = "0.2.0"
|
| 95 |
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
| 96 |
+
checksum = "1d1043d8214f791817bab27572aaa8af63732e11bf84aa21a45a78d6c317ae0e"
|
| 97 |
+
dependencies = [
|
| 98 |
+
"windows-targets",
|
| 99 |
+
]
|
| 100 |
+
|
| 101 |
+
[[package]]
|
| 102 |
+
name = "windows-strings"
|
| 103 |
+
version = "0.1.0"
|
| 104 |
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
| 105 |
+
checksum = "4cd9b125c486025df0eabcb585e62173c6c9eddcec5d117d3b6e8c30e2ee4d10"
|
| 106 |
+
dependencies = [
|
| 107 |
+
"windows-result",
|
| 108 |
+
"windows-targets",
|
| 109 |
+
]
|
| 110 |
+
|
| 111 |
+
[[package]]
|
| 112 |
+
name = "windows-targets"
|
| 113 |
+
version = "0.52.6"
|
| 114 |
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
| 115 |
+
checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973"
|
| 116 |
+
dependencies = [
|
| 117 |
+
"windows_aarch64_gnullvm",
|
| 118 |
+
"windows_aarch64_msvc",
|
| 119 |
+
"windows_i686_gnu",
|
| 120 |
+
"windows_i686_gnullvm",
|
| 121 |
+
"windows_i686_msvc",
|
| 122 |
+
"windows_x86_64_gnu",
|
| 123 |
+
"windows_x86_64_gnullvm",
|
| 124 |
+
"windows_x86_64_msvc",
|
| 125 |
+
]
|
| 126 |
+
|
| 127 |
+
[[package]]
|
| 128 |
+
name = "windows_aarch64_gnullvm"
|
| 129 |
+
version = "0.52.6"
|
| 130 |
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
| 131 |
+
checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3"
|
| 132 |
+
|
| 133 |
+
[[package]]
|
| 134 |
+
name = "windows_aarch64_msvc"
|
| 135 |
+
version = "0.52.6"
|
| 136 |
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
| 137 |
+
checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469"
|
| 138 |
+
|
| 139 |
+
[[package]]
|
| 140 |
+
name = "windows_i686_gnu"
|
| 141 |
+
version = "0.52.6"
|
| 142 |
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
| 143 |
+
checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b"
|
| 144 |
+
|
| 145 |
+
[[package]]
|
| 146 |
+
name = "windows_i686_gnullvm"
|
| 147 |
+
version = "0.52.6"
|
| 148 |
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
| 149 |
+
checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66"
|
| 150 |
+
|
| 151 |
+
[[package]]
|
| 152 |
+
name = "windows_i686_msvc"
|
| 153 |
+
version = "0.52.6"
|
| 154 |
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
| 155 |
+
checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66"
|
| 156 |
+
|
| 157 |
+
[[package]]
|
| 158 |
+
name = "windows_x86_64_gnu"
|
| 159 |
+
version = "0.52.6"
|
| 160 |
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
| 161 |
+
checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78"
|
| 162 |
+
|
| 163 |
+
[[package]]
|
| 164 |
+
name = "windows_x86_64_gnullvm"
|
| 165 |
+
version = "0.52.6"
|
| 166 |
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
| 167 |
+
checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d"
|
| 168 |
+
|
| 169 |
+
[[package]]
|
| 170 |
+
name = "windows_x86_64_msvc"
|
| 171 |
+
version = "0.52.6"
|
| 172 |
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
| 173 |
+
checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"
|
Cargo.toml
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
[package]
|
| 2 |
+
name = "autocorrect-type"
|
| 3 |
+
version = "1.0.0"
|
| 4 |
+
edition = "2021"
|
| 5 |
+
authors = ["Rembrant Oyangoren Albeos"]
|
| 6 |
+
description = "System-wide autocorrect engine for Windows"
|
| 7 |
+
license = "MIT"
|
| 8 |
+
|
| 9 |
+
[dependencies]
|
| 10 |
+
windows = { version = "0.58", features = [
|
| 11 |
+
"Win32_Foundation",
|
| 12 |
+
"Win32_Graphics_Gdi",
|
| 13 |
+
"Win32_System_LibraryLoader",
|
| 14 |
+
"Win32_UI_WindowsAndMessaging",
|
| 15 |
+
"Win32_UI_Input_KeyboardAndMouse",
|
| 16 |
+
"Win32_UI_Shell",
|
| 17 |
+
] }
|
| 18 |
+
|
| 19 |
+
[profile.release]
|
| 20 |
+
opt-level = 3
|
| 21 |
+
lto = true
|
| 22 |
+
strip = true
|
| 23 |
+
panic = "abort"
|
LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
MIT License
|
| 2 |
+
|
| 3 |
+
Copyright (c) 2026 Rembrant Oyangoren Albeos
|
| 4 |
+
|
| 5 |
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
| 6 |
+
of this software and associated documentation files (the "Software"), to deal
|
| 7 |
+
in the Software without restriction, including without limitation the rights
|
| 8 |
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
| 9 |
+
copies of the Software, and to permit persons to whom the Software is
|
| 10 |
+
furnished to do so, subject to the following conditions:
|
| 11 |
+
|
| 12 |
+
The above copyright notice and this permission notice shall be included in all
|
| 13 |
+
copies or substantial portions of the Software.
|
| 14 |
+
|
| 15 |
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
| 16 |
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
| 17 |
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
| 18 |
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
| 19 |
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
| 20 |
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
| 21 |
+
SOFTWARE.
|
README.md
ADDED
|
@@ -0,0 +1,144 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Autocorrect-Type
|
| 2 |
+
|
| 3 |
+

|
| 4 |
+

|
| 5 |
+

|
| 6 |
+

|
| 7 |
+

|
| 8 |
+

|
| 9 |
+
|
| 10 |
+
System-wide autocorrect for Windows. Runs silently in the background and fixes misspelled words as you type -- in any application (terminal, browser, Discord, search bar, etc.).
|
| 11 |
+
|
| 12 |
+
Built with Rust + Windows API. No external APIs, no cloud dependencies, no AI -- fully offline and native.
|
| 13 |
+
|
| 14 |
+
Handles 466k English words sourced from [english-words](https://github.com/dwyl/english-words).
|
| 15 |
+
You can download the misspelling datasets [here](https://huggingface.co/datasets/algorembrant/generated-misspelling-words) and make sure to rename the file into `misspellings.txt` and place it on `*\data\` folder path.
|
| 16 |
+
|
| 17 |
+
## Features
|
| 18 |
+
|
| 19 |
+
- **System-wide** -- works in any text input across your entire system
|
| 20 |
+
- **500+ common misspellings** -- instant, high-confidence corrections (e.g. `teh` -> `the`)
|
| 21 |
+
- **Smart spell checking** -- edit-distance algorithm catches unknown typos against a 466k-word dictionary
|
| 22 |
+
- **Clipboard-based replacement** -- corrections happen instantly, no visible cursor movement
|
| 23 |
+
- **Case-preserving** -- respects your capitalization (`TEH` -> `THE`, `Teh` -> `The`)
|
| 24 |
+
- **System tray** -- toggle on/off, runs silently in background
|
| 25 |
+
- **Lightweight** -- single `.exe`, no runtime dependencies, tiny memory footprint
|
| 26 |
+
|
| 27 |
+
## System Overview
|
| 28 |
+
|
| 29 |
+
```mermaid
|
| 30 |
+
graph TD
|
| 31 |
+
A[Keyboard Input] -->|WH_KEYBOARD_LL| B[hook.rs]
|
| 32 |
+
B -->|Word buffer| C{Trigger key?}
|
| 33 |
+
C -->|No| B
|
| 34 |
+
C -->|Yes| D[spellcheck.rs]
|
| 35 |
+
D -->|Known misspelling| E[Misspellings DB]
|
| 36 |
+
D -->|Unknown word| F[Edit-Distance Engine]
|
| 37 |
+
F -->|Match found| G[Dictionary - 466k words]
|
| 38 |
+
E --> H[replacer.rs]
|
| 39 |
+
G --> H
|
| 40 |
+
H -->|Clipboard swap| I[Corrected Text]
|
| 41 |
+
J[tray.rs] -->|Toggle / Exit| B
|
| 42 |
+
K[main.rs] -->|Init| B
|
| 43 |
+
K -->|Init| D
|
| 44 |
+
K -->|Init| J
|
| 45 |
+
```
|
| 46 |
+
|
| 47 |
+
## Requirements
|
| 48 |
+
|
| 49 |
+
### To run the compiled `.exe`
|
| 50 |
+
|
| 51 |
+
- Windows 10/11
|
| 52 |
+
|
| 53 |
+
### To build from source
|
| 54 |
+
|
| 55 |
+
- [Rust toolchain](https://rustup.rs/) (stable)
|
| 56 |
+
|
| 57 |
+
## Build
|
| 58 |
+
|
| 59 |
+
```bash
|
| 60 |
+
cargo build --release
|
| 61 |
+
```
|
| 62 |
+
|
| 63 |
+
The binary will be at `target/release/autocorrect-type.exe`.
|
| 64 |
+
|
| 65 |
+
## Usage
|
| 66 |
+
|
| 67 |
+
### Run
|
| 68 |
+
|
| 69 |
+
Double-click `autocorrect-type.exe` or run from terminal. The app appears in the system tray.
|
| 70 |
+
|
| 71 |
+
```bash
|
| 72 |
+
# if it didnt open then
|
| 73 |
+
cargo run --release
|
| 74 |
+
```
|
| 75 |
+
|
| 76 |
+
### Controls
|
| 77 |
+
|
| 78 |
+
| Action | How |
|
| 79 |
+
|---|---|
|
| 80 |
+
| Toggle ON/OFF | `Ctrl + Alt + A` or right-click tray icon |
|
| 81 |
+
| Exit | Right-click tray -> "Exit" |
|
| 82 |
+
|
| 83 |
+
### How it works
|
| 84 |
+
|
| 85 |
+
1. Type normally in any application
|
| 86 |
+
2. When you press **Space**, **Enter**, **Tab**, or punctuation, the app checks the word you just typed
|
| 87 |
+
3. If it is a known misspelling or a close match to a dictionary word, it is instantly replaced via the clipboard (no cursor flicker)
|
| 88 |
+
4. A small balloon notification shows the correction briefly
|
| 89 |
+
|
| 90 |
+
## How the correction works (no cursor flicker)
|
| 91 |
+
|
| 92 |
+
Unlike typical autocorrect tools that backspace and retype characters visibly, this engine uses a clipboard-based approach:
|
| 93 |
+
|
| 94 |
+
1. Saves current clipboard contents
|
| 95 |
+
2. Sends backspaces to delete the misspelled word
|
| 96 |
+
3. Copies the correction to the clipboard
|
| 97 |
+
4. Simulates `Ctrl+V` to paste instantly
|
| 98 |
+
5. Restores the original clipboard
|
| 99 |
+
|
| 100 |
+
This makes corrections appear instantaneous with no visible cursor movement.
|
| 101 |
+
|
| 102 |
+
## Customization
|
| 103 |
+
|
| 104 |
+
### Add words to the dictionary
|
| 105 |
+
|
| 106 |
+
Add words to `data/user_words.txt` (one per line, auto-created next to the `.exe`). Restart the app after editing.
|
| 107 |
+
|
| 108 |
+
### Expand the misspellings database
|
| 109 |
+
|
| 110 |
+
Edit `data/misspellings.txt`. Format: `misspelling=correction`, one per line. Lines starting with `#` are comments. The file is embedded at compile time.
|
| 111 |
+
|
| 112 |
+
## Project Structure
|
| 113 |
+
|
| 114 |
+
```text
|
| 115 |
+
autocorrect-type/
|
| 116 |
+
├── Cargo.toml
|
| 117 |
+
├── Cargo.lock
|
| 118 |
+
├── LICENSE
|
| 119 |
+
├── README.md
|
| 120 |
+
├── .gitignore
|
| 121 |
+
├── src/
|
| 122 |
+
| ├── main.rs
|
| 123 |
+
| ├── hook.rs
|
| 124 |
+
| ├── spellcheck.rs
|
| 125 |
+
| ├── replacer.rs
|
| 126 |
+
| └── tray.rs
|
| 127 |
+
└── data/
|
| 128 |
+
├── words.txt
|
| 129 |
+
└── misspellings.txt
|
| 130 |
+
```
|
| 131 |
+
|
| 132 |
+
## Citation
|
| 133 |
+
|
| 134 |
+
```bibtex
|
| 135 |
+
@software{autocorrect_type,
|
| 136 |
+
author = {Rembrant Oyangoren Albeos},
|
| 137 |
+
title = {Autocorrect-Type},
|
| 138 |
+
year = {2026},
|
| 139 |
+
publisher = {Hugging Face},
|
| 140 |
+
url = {https://huggingface.co/algorembrant/autocorrect-type}
|
| 141 |
+
}
|
| 142 |
+
```
|
| 143 |
+
|
| 144 |
+
|
STACKS.md
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
## Description
|
| 2 |
+
|
| 3 |
+
Autocorrect-Type is a system-wide, offline autocorrect engine for Windows built entirely in Rust with the Windows API. It intercepts keyboard input at the OS level via a low-level keyboard hook (`WH_KEYBOARD_LL`), buffers typed characters into words, and upon encountering a trigger key (Space, Enter, Tab, or punctuation) checks the completed word against a 466k-entry English dictionary and a curated misspellings database. Corrections are applied instantly using a clipboard-based swap strategy that avoids visible cursor movement or flicker. The application runs silently in the system tray with hotkey toggling, requires no external APIs or cloud services, and ships as a single lightweight `.exe`.
|
| 4 |
+
|
| 5 |
+
## System Overview
|
| 6 |
+
|
| 7 |
+
```mermaid
|
| 8 |
+
graph TD
|
| 9 |
+
A[Keyboard Input] -->|WH_KEYBOARD_LL| B[hook.rs]
|
| 10 |
+
B -->|Word buffer| C{Trigger key?}
|
| 11 |
+
C -->|No| B
|
| 12 |
+
C -->|Yes| D[spellcheck.rs]
|
| 13 |
+
D -->|Known misspelling| E[Misspellings DB]
|
| 14 |
+
D -->|Unknown word| F[Edit-Distance Engine]
|
| 15 |
+
F -->|Match found| G[Dictionary - 466k words]
|
| 16 |
+
E --> H[replacer.rs]
|
| 17 |
+
G --> H
|
| 18 |
+
H -->|Clipboard swap| I[Corrected Text]
|
| 19 |
+
J[tray.rs] -->|Toggle / Exit| B
|
| 20 |
+
K[main.rs] -->|Init| B
|
| 21 |
+
K -->|Init| D
|
| 22 |
+
K -->|Init| J
|
| 23 |
+
```
|
| 24 |
+
|
| 25 |
+
## Project Structure
|
| 26 |
+
|
| 27 |
+
```text
|
| 28 |
+
autocorrect-type/
|
| 29 |
+
├── Cargo.toml
|
| 30 |
+
├── Cargo.lock
|
| 31 |
+
├── LICENSE
|
| 32 |
+
├── README.md
|
| 33 |
+
├── STACKS.md
|
| 34 |
+
├── .gitignore
|
| 35 |
+
├── src/
|
| 36 |
+
| ├── main.rs
|
| 37 |
+
| ├── hook.rs
|
| 38 |
+
| ├── spellcheck.rs
|
| 39 |
+
| ├── replacer.rs
|
| 40 |
+
| └── tray.rs
|
| 41 |
+
└── data/
|
| 42 |
+
├── words.txt
|
| 43 |
+
└── misspellings.txt
|
| 44 |
+
```
|
| 45 |
+
|
| 46 |
+
## Techstack
|
| 47 |
+
|
| 48 |
+
Audit of project files (excluding build artifacts and legacy references):
|
| 49 |
+
|
| 50 |
+
| File Type | Count | Size (KB) |
|
| 51 |
+
| :--- | :--- | :--- |
|
| 52 |
+
| Rust (.rs) | 5 | 26.3 |
|
| 53 |
+
| Text (.txt) | 2 | 571,065.0 |
|
| 54 |
+
| Markdown (.md) | 3 | 12.5 |
|
| 55 |
+
| TOML (.toml) | 1 | 0.5 |
|
| 56 |
+
| Lock (.lock) | 1 | 4.6 |
|
| 57 |
+
| License | 1 | 1.1 |
|
| 58 |
+
| Gitignore (.gitignore) | 1 | 0.2 |
|
| 59 |
+
|
| 60 |
+
**Total Files**: 14
|
| 61 |
+
|
| 62 |
+
## Dependencies
|
| 63 |
+
|
| 64 |
+
- **Rust**:
|
| 65 |
+
- `windows` (0.58): Win32 Foundation, Graphics GDI, System LibraryLoader, UI WindowsAndMessaging, UI Input KeyboardAndMouse, UI Shell
|
| 66 |
+
|
| 67 |
+
## Applications
|
| 68 |
+
|
| 69 |
+
- Google Antigravity
|
| 70 |
+
- Visual Studio Code
|
| 71 |
+
- Windows PowerShell
|
src/hook.rs
ADDED
|
@@ -0,0 +1,193 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
// ---------------------------------------------------------------------------
|
| 2 |
+
// hook.rs — Low-level keyboard hook (WH_KEYBOARD_LL)
|
| 3 |
+
// ---------------------------------------------------------------------------
|
| 4 |
+
|
| 5 |
+
use crate::{CHECKER, CORRECTING, ENABLED, PENDING, WORD_BUFFER, TriggerKind};
|
| 6 |
+
use std::sync::atomic::Ordering;
|
| 7 |
+
use windows::Win32::Foundation::*;
|
| 8 |
+
use windows::Win32::System::LibraryLoader::GetModuleHandleW;
|
| 9 |
+
use windows::Win32::UI::Input::KeyboardAndMouse::*;
|
| 10 |
+
use windows::Win32::UI::WindowsAndMessaging::*;
|
| 11 |
+
|
| 12 |
+
/// Custom message posted when a correction is ready
|
| 13 |
+
pub const WM_DO_CORRECTION: u32 = WM_APP + 1;
|
| 14 |
+
|
| 15 |
+
static mut HOOK: HHOOK = HHOOK(std::ptr::null_mut());
|
| 16 |
+
|
| 17 |
+
// ---- public API -----------------------------------------------------------
|
| 18 |
+
|
| 19 |
+
/// Install the global keyboard hook on the current thread.
|
| 20 |
+
pub fn install() {
|
| 21 |
+
unsafe {
|
| 22 |
+
let hmod = GetModuleHandleW(None).unwrap_or_default();
|
| 23 |
+
HOOK = SetWindowsHookExW(WH_KEYBOARD_LL, Some(ll_keyboard_proc), hmod, 0)
|
| 24 |
+
.expect("SetWindowsHookExW failed");
|
| 25 |
+
}
|
| 26 |
+
}
|
| 27 |
+
|
| 28 |
+
/// Pump the Windows message loop (blocks until WM_QUIT).
|
| 29 |
+
pub fn message_loop(_hwnd: HWND) {
|
| 30 |
+
let mut msg = MSG::default();
|
| 31 |
+
unsafe {
|
| 32 |
+
while GetMessageW(&mut msg, HWND::default(), 0, 0).as_bool() {
|
| 33 |
+
let _ = TranslateMessage(&msg);
|
| 34 |
+
DispatchMessageW(&msg);
|
| 35 |
+
}
|
| 36 |
+
}
|
| 37 |
+
}
|
| 38 |
+
|
| 39 |
+
// ---- hook callback --------------------------------------------------------
|
| 40 |
+
|
| 41 |
+
unsafe extern "system" fn ll_keyboard_proc(
|
| 42 |
+
code: i32,
|
| 43 |
+
wparam: WPARAM,
|
| 44 |
+
lparam: LPARAM,
|
| 45 |
+
) -> LRESULT {
|
| 46 |
+
if code < 0 {
|
| 47 |
+
return CallNextHookEx(HOOK, code, wparam, lparam);
|
| 48 |
+
}
|
| 49 |
+
|
| 50 |
+
// Only key-down
|
| 51 |
+
let msg = wparam.0 as u32;
|
| 52 |
+
if msg != WM_KEYDOWN && msg != WM_SYSKEYDOWN {
|
| 53 |
+
return CallNextHookEx(HOOK, code, wparam, lparam);
|
| 54 |
+
}
|
| 55 |
+
|
| 56 |
+
// Skip while we are injecting a correction
|
| 57 |
+
if CORRECTING.load(Ordering::SeqCst) {
|
| 58 |
+
return CallNextHookEx(HOOK, code, wparam, lparam);
|
| 59 |
+
}
|
| 60 |
+
|
| 61 |
+
// Skip if autocorrect is paused
|
| 62 |
+
if !ENABLED.load(Ordering::SeqCst) {
|
| 63 |
+
return CallNextHookEx(HOOK, code, wparam, lparam);
|
| 64 |
+
}
|
| 65 |
+
|
| 66 |
+
let kb = *(lparam.0 as *const KBDLLHOOKSTRUCT);
|
| 67 |
+
|
| 68 |
+
// Ignore injected input (our own SendInput calls)
|
| 69 |
+
if kb.flags.0 & LLKHF_INJECTED.0 != 0 {
|
| 70 |
+
return CallNextHookEx(HOOK, code, wparam, lparam);
|
| 71 |
+
}
|
| 72 |
+
|
| 73 |
+
let vk = kb.vkCode;
|
| 74 |
+
|
| 75 |
+
// --- Letters A-Z → append to buffer ---
|
| 76 |
+
if (0x41..=0x5A).contains(&vk) {
|
| 77 |
+
let ch = vk_to_char(vk);
|
| 78 |
+
if let Ok(mut buf) = WORD_BUFFER.lock() {
|
| 79 |
+
buf.push(ch);
|
| 80 |
+
}
|
| 81 |
+
return CallNextHookEx(HOOK, code, wparam, lparam);
|
| 82 |
+
}
|
| 83 |
+
|
| 84 |
+
// --- Apostrophe (unshifted OEM_7 on US layout) ---
|
| 85 |
+
if vk == VK_OEM_7.0 as u32 {
|
| 86 |
+
let shift = GetAsyncKeyState(VK_SHIFT.0 as i32) as u16 & 0x8000 != 0;
|
| 87 |
+
if !shift {
|
| 88 |
+
if let Ok(mut buf) = WORD_BUFFER.lock() {
|
| 89 |
+
buf.push('\'');
|
| 90 |
+
}
|
| 91 |
+
return CallNextHookEx(HOOK, code, wparam, lparam);
|
| 92 |
+
}
|
| 93 |
+
}
|
| 94 |
+
|
| 95 |
+
// --- Word boundary keys → trigger correction check ---
|
| 96 |
+
let trigger = match vk {
|
| 97 |
+
v if v == VK_SPACE.0 as u32 => Some(TriggerKind::Space),
|
| 98 |
+
v if v == VK_RETURN.0 as u32 => Some(TriggerKind::Enter),
|
| 99 |
+
v if v == VK_TAB.0 as u32 => Some(TriggerKind::Tab),
|
| 100 |
+
v if v == VK_OEM_PERIOD.0 as u32 => Some(TriggerKind::Char('.')),
|
| 101 |
+
v if v == VK_OEM_COMMA.0 as u32 => Some(TriggerKind::Char(',')),
|
| 102 |
+
v if v == VK_OEM_1.0 as u32 => Some(TriggerKind::Char(';')),
|
| 103 |
+
v if v == VK_OEM_2.0 as u32 => Some(TriggerKind::Char('/')),
|
| 104 |
+
_ => None,
|
| 105 |
+
};
|
| 106 |
+
|
| 107 |
+
if let Some(trig) = trigger {
|
| 108 |
+
check_and_schedule(trig);
|
| 109 |
+
return CallNextHookEx(HOOK, code, wparam, lparam);
|
| 110 |
+
}
|
| 111 |
+
|
| 112 |
+
// --- Backspace → trim buffer ---
|
| 113 |
+
if vk == VK_BACK.0 as u32 {
|
| 114 |
+
if let Ok(mut buf) = WORD_BUFFER.lock() {
|
| 115 |
+
buf.pop();
|
| 116 |
+
}
|
| 117 |
+
return CallNextHookEx(HOOK, code, wparam, lparam);
|
| 118 |
+
}
|
| 119 |
+
|
| 120 |
+
// --- Navigation / Delete / Escape → clear buffer ---
|
| 121 |
+
const NAV: &[VIRTUAL_KEY] = &[
|
| 122 |
+
VK_LEFT, VK_RIGHT, VK_UP, VK_DOWN,
|
| 123 |
+
VK_HOME, VK_END, VK_PRIOR, VK_NEXT,
|
| 124 |
+
VK_DELETE, VK_ESCAPE,
|
| 125 |
+
];
|
| 126 |
+
if NAV.iter().any(|k| k.0 as u32 == vk) {
|
| 127 |
+
if let Ok(mut buf) = WORD_BUFFER.lock() {
|
| 128 |
+
buf.clear();
|
| 129 |
+
}
|
| 130 |
+
}
|
| 131 |
+
|
| 132 |
+
CallNextHookEx(HOOK, code, wparam, lparam)
|
| 133 |
+
}
|
| 134 |
+
|
| 135 |
+
// ---- helpers --------------------------------------------------------------
|
| 136 |
+
|
| 137 |
+
/// Convert a VK code (A-Z range) to the character the user actually typed.
|
| 138 |
+
fn vk_to_char(vk: u32) -> char {
|
| 139 |
+
let shift = unsafe { GetAsyncKeyState(VK_SHIFT.0 as i32) } as u16 & 0x8000 != 0;
|
| 140 |
+
let caps = unsafe { GetKeyState(VK_CAPITAL.0 as i32) } & 1 != 0;
|
| 141 |
+
let upper = shift ^ caps;
|
| 142 |
+
let base = (vk - 0x41) as u8;
|
| 143 |
+
if upper { (b'A' + base) as char } else { (b'a' + base) as char }
|
| 144 |
+
}
|
| 145 |
+
|
| 146 |
+
/// Take the current word buffer, check it, and post a correction message.
|
| 147 |
+
fn check_and_schedule(trigger: TriggerKind) {
|
| 148 |
+
let word = {
|
| 149 |
+
let mut buf = WORD_BUFFER.lock().unwrap();
|
| 150 |
+
let w = buf.clone();
|
| 151 |
+
buf.clear();
|
| 152 |
+
w
|
| 153 |
+
};
|
| 154 |
+
|
| 155 |
+
if word.len() < 2 {
|
| 156 |
+
return;
|
| 157 |
+
}
|
| 158 |
+
|
| 159 |
+
let checker = CHECKER.get().expect("checker not initialised");
|
| 160 |
+
if let Some(correction) = checker.get_correction(&word) {
|
| 161 |
+
if correction.eq_ignore_ascii_case(&word) {
|
| 162 |
+
return;
|
| 163 |
+
}
|
| 164 |
+
|
| 165 |
+
let corrected = preserve_case(&word, &correction);
|
| 166 |
+
|
| 167 |
+
// Store pending correction
|
| 168 |
+
if let Ok(mut p) = PENDING.lock() {
|
| 169 |
+
*p = Some((word, corrected, trigger));
|
| 170 |
+
}
|
| 171 |
+
|
| 172 |
+
// Ask the tray window to perform the replacement
|
| 173 |
+
unsafe {
|
| 174 |
+
let hwnd = crate::tray::hwnd();
|
| 175 |
+
let _ = PostMessageW(hwnd, WM_DO_CORRECTION, WPARAM(0), LPARAM(0));
|
| 176 |
+
}
|
| 177 |
+
}
|
| 178 |
+
}
|
| 179 |
+
|
| 180 |
+
/// Mirror the original word's capitalisation onto the correction.
|
| 181 |
+
fn preserve_case(original: &str, correction: &str) -> String {
|
| 182 |
+
if original.chars().all(|c| c.is_uppercase() || !c.is_alphabetic()) {
|
| 183 |
+
return correction.to_uppercase();
|
| 184 |
+
}
|
| 185 |
+
if original.chars().next().map_or(false, |c| c.is_uppercase()) {
|
| 186 |
+
let mut it = correction.chars();
|
| 187 |
+
return match it.next() {
|
| 188 |
+
Some(f) => f.to_uppercase().to_string() + it.as_str(),
|
| 189 |
+
None => String::new(),
|
| 190 |
+
};
|
| 191 |
+
}
|
| 192 |
+
correction.to_lowercase()
|
| 193 |
+
}
|
src/main.rs
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#![windows_subsystem = "windows"]
|
| 2 |
+
|
| 3 |
+
mod hook;
|
| 4 |
+
mod replacer;
|
| 5 |
+
mod spellcheck;
|
| 6 |
+
mod tray;
|
| 7 |
+
|
| 8 |
+
use std::sync::atomic::AtomicBool;
|
| 9 |
+
use std::sync::{Mutex, OnceLock};
|
| 10 |
+
|
| 11 |
+
// ---------------------------------------------------------------------------
|
| 12 |
+
// Global application state (all accessed from the main thread only)
|
| 13 |
+
// ---------------------------------------------------------------------------
|
| 14 |
+
|
| 15 |
+
/// Whether autocorrect is active
|
| 16 |
+
pub static ENABLED: AtomicBool = AtomicBool::new(true);
|
| 17 |
+
|
| 18 |
+
/// Whether we are currently performing a correction (ignore our own keys)
|
| 19 |
+
pub static CORRECTING: AtomicBool = AtomicBool::new(false);
|
| 20 |
+
|
| 21 |
+
/// Accumulated word buffer
|
| 22 |
+
pub static WORD_BUFFER: Mutex<String> = Mutex::new(String::new());
|
| 23 |
+
|
| 24 |
+
/// Pending correction: (original_word, corrected_word, trigger)
|
| 25 |
+
pub static PENDING: Mutex<Option<(String, String, TriggerKind)>> = Mutex::new(None);
|
| 26 |
+
|
| 27 |
+
/// Spellcheck engine (initialized once at startup)
|
| 28 |
+
pub static CHECKER: OnceLock<spellcheck::SpellChecker> = OnceLock::new();
|
| 29 |
+
|
| 30 |
+
/// How the user terminated the word
|
| 31 |
+
#[derive(Clone, Debug)]
|
| 32 |
+
pub enum TriggerKind {
|
| 33 |
+
Space,
|
| 34 |
+
Enter,
|
| 35 |
+
Tab,
|
| 36 |
+
Char(char),
|
| 37 |
+
}
|
| 38 |
+
|
| 39 |
+
// ---------------------------------------------------------------------------
|
| 40 |
+
// Entry point
|
| 41 |
+
// ---------------------------------------------------------------------------
|
| 42 |
+
|
| 43 |
+
fn main() {
|
| 44 |
+
// 1. Initialize the spellcheck engine
|
| 45 |
+
let checker = spellcheck::SpellChecker::new();
|
| 46 |
+
CHECKER.set(checker).expect("spellchecker already initialized");
|
| 47 |
+
|
| 48 |
+
// 2. Create the hidden tray window (also registers the hotkey)
|
| 49 |
+
let hwnd = tray::setup();
|
| 50 |
+
|
| 51 |
+
// 3. Install the global keyboard hook
|
| 52 |
+
hook::install();
|
| 53 |
+
|
| 54 |
+
// 4. Show startup balloon
|
| 55 |
+
tray::notify("Autocorrect-Type", "Running. Press Ctrl+Alt+A to toggle.");
|
| 56 |
+
|
| 57 |
+
// 5. Run the Windows message loop (blocks forever)
|
| 58 |
+
hook::message_loop(hwnd);
|
| 59 |
+
}
|
src/replacer.rs
ADDED
|
@@ -0,0 +1,111 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
// ---------------------------------------------------------------------------
|
| 2 |
+
// replacer.rs — Direct-typing text replacement (no clipboard interaction)
|
| 3 |
+
// ---------------------------------------------------------------------------
|
| 4 |
+
|
| 5 |
+
use crate::TriggerKind;
|
| 6 |
+
use std::thread;
|
| 7 |
+
use std::time::Duration;
|
| 8 |
+
use windows::Win32::UI::Input::KeyboardAndMouse::*;
|
| 9 |
+
|
| 10 |
+
/// Replace `original` with `corrected` in the currently focused application.
|
| 11 |
+
///
|
| 12 |
+
/// Strategy:
|
| 13 |
+
/// 1. Delete the misspelled word + trigger character with backspaces
|
| 14 |
+
/// 2. Type the corrected word character-by-character
|
| 15 |
+
/// 3. Re-type the trigger character
|
| 16 |
+
///
|
| 17 |
+
/// This approach never touches the clipboard, so Ctrl+C contents are
|
| 18 |
+
/// always preserved and never interfere with corrections.
|
| 19 |
+
pub fn replace(original: &str, corrected: &str, trigger: &TriggerKind) {
|
| 20 |
+
// Small delay to let the trigger key be processed first
|
| 21 |
+
thread::sleep(Duration::from_millis(40));
|
| 22 |
+
|
| 23 |
+
// 1. Backspace to delete the word + the trigger character
|
| 24 |
+
let bs_count = original.len() + 1;
|
| 25 |
+
for _ in 0..bs_count {
|
| 26 |
+
send_key(VK_BACK);
|
| 27 |
+
}
|
| 28 |
+
thread::sleep(Duration::from_millis(30));
|
| 29 |
+
|
| 30 |
+
// 2. Type the corrected word directly (no clipboard)
|
| 31 |
+
for ch in corrected.chars() {
|
| 32 |
+
send_char(ch);
|
| 33 |
+
}
|
| 34 |
+
|
| 35 |
+
// 3. Re-type the trigger character
|
| 36 |
+
match trigger {
|
| 37 |
+
TriggerKind::Space => send_key(VK_SPACE),
|
| 38 |
+
TriggerKind::Enter => send_key(VK_RETURN),
|
| 39 |
+
TriggerKind::Tab => send_key(VK_TAB),
|
| 40 |
+
TriggerKind::Char(ch) => send_char(*ch),
|
| 41 |
+
}
|
| 42 |
+
}
|
| 43 |
+
|
| 44 |
+
// ---------------------------------------------------------------------------
|
| 45 |
+
// Input simulation helpers
|
| 46 |
+
// ---------------------------------------------------------------------------
|
| 47 |
+
|
| 48 |
+
fn send_key(vk: VIRTUAL_KEY) {
|
| 49 |
+
let inputs = [
|
| 50 |
+
INPUT {
|
| 51 |
+
r#type: INPUT_KEYBOARD,
|
| 52 |
+
Anonymous: INPUT_0 {
|
| 53 |
+
ki: KEYBDINPUT {
|
| 54 |
+
wVk: vk,
|
| 55 |
+
wScan: 0,
|
| 56 |
+
dwFlags: KEYBD_EVENT_FLAGS(0),
|
| 57 |
+
time: 0,
|
| 58 |
+
dwExtraInfo: 0,
|
| 59 |
+
},
|
| 60 |
+
},
|
| 61 |
+
},
|
| 62 |
+
INPUT {
|
| 63 |
+
r#type: INPUT_KEYBOARD,
|
| 64 |
+
Anonymous: INPUT_0 {
|
| 65 |
+
ki: KEYBDINPUT {
|
| 66 |
+
wVk: vk,
|
| 67 |
+
wScan: 0,
|
| 68 |
+
dwFlags: KEYEVENTF_KEYUP,
|
| 69 |
+
time: 0,
|
| 70 |
+
dwExtraInfo: 0,
|
| 71 |
+
},
|
| 72 |
+
},
|
| 73 |
+
},
|
| 74 |
+
];
|
| 75 |
+
unsafe {
|
| 76 |
+
SendInput(&inputs, std::mem::size_of::<INPUT>() as i32);
|
| 77 |
+
}
|
| 78 |
+
}
|
| 79 |
+
|
| 80 |
+
fn send_char(ch: char) {
|
| 81 |
+
let code = ch as u16;
|
| 82 |
+
let inputs = [
|
| 83 |
+
INPUT {
|
| 84 |
+
r#type: INPUT_KEYBOARD,
|
| 85 |
+
Anonymous: INPUT_0 {
|
| 86 |
+
ki: KEYBDINPUT {
|
| 87 |
+
wVk: VIRTUAL_KEY(0),
|
| 88 |
+
wScan: code,
|
| 89 |
+
dwFlags: KEYEVENTF_UNICODE,
|
| 90 |
+
time: 0,
|
| 91 |
+
dwExtraInfo: 0,
|
| 92 |
+
},
|
| 93 |
+
},
|
| 94 |
+
},
|
| 95 |
+
INPUT {
|
| 96 |
+
r#type: INPUT_KEYBOARD,
|
| 97 |
+
Anonymous: INPUT_0 {
|
| 98 |
+
ki: KEYBDINPUT {
|
| 99 |
+
wVk: VIRTUAL_KEY(0),
|
| 100 |
+
wScan: code,
|
| 101 |
+
dwFlags: KEYEVENTF_UNICODE | KEYEVENTF_KEYUP,
|
| 102 |
+
time: 0,
|
| 103 |
+
dwExtraInfo: 0,
|
| 104 |
+
},
|
| 105 |
+
},
|
| 106 |
+
},
|
| 107 |
+
];
|
| 108 |
+
unsafe {
|
| 109 |
+
SendInput(&inputs, std::mem::size_of::<INPUT>() as i32);
|
| 110 |
+
}
|
| 111 |
+
}
|
src/spellcheck.rs
ADDED
|
@@ -0,0 +1,272 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
// ---------------------------------------------------------------------------
|
| 2 |
+
// spellcheck.rs — Dictionary lookup + edit-distance spell checking
|
| 3 |
+
// ---------------------------------------------------------------------------
|
| 4 |
+
|
| 5 |
+
use std::collections::HashSet;
|
| 6 |
+
use std::fs::File;
|
| 7 |
+
use std::io::{BufRead, BufReader, Seek, SeekFrom};
|
| 8 |
+
use std::path::PathBuf;
|
| 9 |
+
|
| 10 |
+
#[derive(Debug)]
|
| 11 |
+
pub struct SpellChecker {
|
| 12 |
+
/// Valid English words (lowercased)
|
| 13 |
+
words: HashSet<String>,
|
| 14 |
+
/// User-added words (lowercased)
|
| 15 |
+
user_words: HashSet<String>,
|
| 16 |
+
/// Path to misspellings file for on-disk binary search
|
| 17 |
+
misspellings_path: PathBuf,
|
| 18 |
+
}
|
| 19 |
+
|
| 20 |
+
impl SpellChecker {
|
| 21 |
+
pub fn new() -> Self {
|
| 22 |
+
let mut sc = Self {
|
| 23 |
+
words: HashSet::with_capacity(500_000),
|
| 24 |
+
user_words: HashSet::new(),
|
| 25 |
+
misspellings_path: data_path("misspellings.txt"),
|
| 26 |
+
};
|
| 27 |
+
sc.load_dictionary();
|
| 28 |
+
sc.load_user_words();
|
| 29 |
+
sc
|
| 30 |
+
}
|
| 31 |
+
|
| 32 |
+
// --- loading -----------------------------------------------------------
|
| 33 |
+
|
| 34 |
+
fn load_dictionary(&mut self) {
|
| 35 |
+
let path = data_path("words.txt");
|
| 36 |
+
if let Ok(file) = File::open(&path) {
|
| 37 |
+
let reader = BufReader::new(file);
|
| 38 |
+
for line in reader.lines() {
|
| 39 |
+
if let Ok(w) = line {
|
| 40 |
+
let w = w.trim();
|
| 41 |
+
if !w.is_empty() {
|
| 42 |
+
self.words.insert(w.to_lowercase());
|
| 43 |
+
}
|
| 44 |
+
}
|
| 45 |
+
}
|
| 46 |
+
}
|
| 47 |
+
}
|
| 48 |
+
|
| 49 |
+
fn load_user_words(&mut self) {
|
| 50 |
+
let path = user_words_path();
|
| 51 |
+
if let Ok(data) = std::fs::read_to_string(&path) {
|
| 52 |
+
for line in data.lines() {
|
| 53 |
+
let w = line.trim();
|
| 54 |
+
if !w.is_empty() {
|
| 55 |
+
self.user_words.insert(w.to_lowercase());
|
| 56 |
+
}
|
| 57 |
+
}
|
| 58 |
+
}
|
| 59 |
+
}
|
| 60 |
+
|
| 61 |
+
// --- public API --------------------------------------------------------
|
| 62 |
+
|
| 63 |
+
/// Returns `Some(correction)` if the word is misspelled, else `None`.
|
| 64 |
+
pub fn get_correction(&self, word: &str) -> Option<String> {
|
| 65 |
+
if word.len() <= 1 {
|
| 66 |
+
return None;
|
| 67 |
+
}
|
| 68 |
+
|
| 69 |
+
let lower = word.to_lowercase();
|
| 70 |
+
|
| 71 |
+
// 1) Already a valid word → no correction (Fast check)
|
| 72 |
+
if self.is_word(&lower) {
|
| 73 |
+
return None;
|
| 74 |
+
}
|
| 75 |
+
|
| 76 |
+
// 2) Known misspelling → Binary search on disk (O(log N))
|
| 77 |
+
// This avoids keeping 590MB of strings in RAM.
|
| 78 |
+
if let Some(fix) = self.find_misspelling_on_disk(&lower) {
|
| 79 |
+
return Some(fix);
|
| 80 |
+
}
|
| 81 |
+
|
| 82 |
+
// 3) Skip very short words for dynamic checking (too many false positives)
|
| 83 |
+
if lower.len() <= 2 {
|
| 84 |
+
return None;
|
| 85 |
+
}
|
| 86 |
+
|
| 87 |
+
// 4) Edit-distance-1 candidates
|
| 88 |
+
for candidate in edits1(&lower) {
|
| 89 |
+
if self.is_word(&candidate) {
|
| 90 |
+
return Some(candidate);
|
| 91 |
+
}
|
| 92 |
+
}
|
| 93 |
+
|
| 94 |
+
// 5) Edit-distance-2 for longer words (≥5 chars)
|
| 95 |
+
if lower.len() >= 5 {
|
| 96 |
+
let mut found = 0;
|
| 97 |
+
for e1 in edits1(&lower) {
|
| 98 |
+
for e2 in edits1(&e1) {
|
| 99 |
+
if self.is_word(&e2) {
|
| 100 |
+
return Some(e2);
|
| 101 |
+
}
|
| 102 |
+
found += 1;
|
| 103 |
+
if found >= 50 {
|
| 104 |
+
// Bail early to keep latency low
|
| 105 |
+
return None;
|
| 106 |
+
}
|
| 107 |
+
}
|
| 108 |
+
}
|
| 109 |
+
}
|
| 110 |
+
|
| 111 |
+
None
|
| 112 |
+
}
|
| 113 |
+
|
| 114 |
+
/// Binary search for `target` in the misspellings file.
|
| 115 |
+
/// Format: `misspelling=correction` sorted by misspelling.
|
| 116 |
+
fn find_misspelling_on_disk(&self, target: &str) -> Option<String> {
|
| 117 |
+
let mut file = File::open(&self.misspellings_path).ok()?;
|
| 118 |
+
let size = file.metadata().ok()?.len();
|
| 119 |
+
if size == 0 { return None; }
|
| 120 |
+
|
| 121 |
+
let mut low = 0;
|
| 122 |
+
let mut high = size;
|
| 123 |
+
|
| 124 |
+
while low < high {
|
| 125 |
+
let mid = low + (high - low) / 2;
|
| 126 |
+
file.seek(SeekFrom::Start(mid)).ok()?;
|
| 127 |
+
|
| 128 |
+
// Find the start of the next full line
|
| 129 |
+
let mut line = String::new();
|
| 130 |
+
let mut br = BufReader::new(&file);
|
| 131 |
+
if mid != 0 {
|
| 132 |
+
let _ = br.read_line(&mut line); // skip partial line
|
| 133 |
+
line.clear();
|
| 134 |
+
let _ = br.read_line(&mut line); // read full line
|
| 135 |
+
} else {
|
| 136 |
+
let _ = br.read_line(&mut line);
|
| 137 |
+
}
|
| 138 |
+
|
| 139 |
+
if line.is_empty() {
|
| 140 |
+
high = mid;
|
| 141 |
+
continue;
|
| 142 |
+
}
|
| 143 |
+
|
| 144 |
+
let trimmed = line.trim();
|
| 145 |
+
if trimmed.is_empty() || trimmed.starts_with('#') {
|
| 146 |
+
low = mid + 1;
|
| 147 |
+
continue;
|
| 148 |
+
}
|
| 149 |
+
|
| 150 |
+
if let Some((bad, good)) = trimmed.split_once('=') {
|
| 151 |
+
let bad = bad.trim();
|
| 152 |
+
let cmp = bad.cmp(target);
|
| 153 |
+
if cmp == std::cmp::Ordering::Equal {
|
| 154 |
+
return Some(good.trim().to_string());
|
| 155 |
+
} else if cmp == std::cmp::Ordering::Less {
|
| 156 |
+
low = mid + 1;
|
| 157 |
+
} else {
|
| 158 |
+
high = mid;
|
| 159 |
+
}
|
| 160 |
+
} else {
|
| 161 |
+
low = mid + 1;
|
| 162 |
+
}
|
| 163 |
+
}
|
| 164 |
+
None
|
| 165 |
+
}
|
| 166 |
+
|
| 167 |
+
/// Add a word to the user dictionary and persist it.
|
| 168 |
+
pub fn add_user_word(&mut self, word: &str) {
|
| 169 |
+
let lower = word.to_lowercase();
|
| 170 |
+
self.user_words.insert(lower.clone());
|
| 171 |
+
let path = user_words_path();
|
| 172 |
+
if let Some(parent) = path.parent() {
|
| 173 |
+
let _ = std::fs::create_dir_all(parent);
|
| 174 |
+
}
|
| 175 |
+
let _ = std::fs::OpenOptions::new()
|
| 176 |
+
.create(true)
|
| 177 |
+
.append(true)
|
| 178 |
+
.open(&path)
|
| 179 |
+
.and_then(|mut f| {
|
| 180 |
+
use std::io::Write;
|
| 181 |
+
writeln!(f, "{}", lower)
|
| 182 |
+
});
|
| 183 |
+
}
|
| 184 |
+
|
| 185 |
+
fn is_word(&self, w: &str) -> bool {
|
| 186 |
+
self.words.contains(w) || self.user_words.contains(w)
|
| 187 |
+
}
|
| 188 |
+
}
|
| 189 |
+
|
| 190 |
+
// ---------------------------------------------------------------------------
|
| 191 |
+
// Helpers
|
| 192 |
+
// ---------------------------------------------------------------------------
|
| 193 |
+
|
| 194 |
+
fn data_path(filename: &str) -> PathBuf {
|
| 195 |
+
let mut p = std::env::current_exe().unwrap_or_else(|_| PathBuf::from("."));
|
| 196 |
+
p.pop(); // remove exe name
|
| 197 |
+
p.push("data");
|
| 198 |
+
p.push(filename);
|
| 199 |
+
|
| 200 |
+
// If data folder doesn't exist next to exe, try root (for dev)
|
| 201 |
+
if !p.exists() {
|
| 202 |
+
let mut p2 = PathBuf::from(".");
|
| 203 |
+
p2.push("data");
|
| 204 |
+
p2.push(filename);
|
| 205 |
+
if p2.exists() { return p2; }
|
| 206 |
+
}
|
| 207 |
+
p
|
| 208 |
+
}
|
| 209 |
+
|
| 210 |
+
/// Path to the user-words file next to the executable.
|
| 211 |
+
fn user_words_path() -> PathBuf {
|
| 212 |
+
data_path("user_words.txt")
|
| 213 |
+
}
|
| 214 |
+
|
| 215 |
+
// ---------------------------------------------------------------------------
|
| 216 |
+
// Edit-distance generator
|
| 217 |
+
// ---------------------------------------------------------------------------
|
| 218 |
+
|
| 219 |
+
/// Generate all strings within edit-distance 1 of `word`.
|
| 220 |
+
fn edits1(word: &str) -> Vec<String> {
|
| 221 |
+
let chars: Vec<char> = word.chars().collect();
|
| 222 |
+
let n = chars.len();
|
| 223 |
+
let mut results = Vec::with_capacity(n * 54 + 26);
|
| 224 |
+
|
| 225 |
+
// Deletions
|
| 226 |
+
for i in 0..n {
|
| 227 |
+
let mut s = String::with_capacity(n - 1);
|
| 228 |
+
for (j, &c) in chars.iter().enumerate() {
|
| 229 |
+
if j != i {
|
| 230 |
+
s.push(c);
|
| 231 |
+
}
|
| 232 |
+
}
|
| 233 |
+
results.push(s);
|
| 234 |
+
}
|
| 235 |
+
|
| 236 |
+
// Transpositions
|
| 237 |
+
for i in 0..n.saturating_sub(1) {
|
| 238 |
+
let mut v = chars.clone();
|
| 239 |
+
v.swap(i, i + 1);
|
| 240 |
+
results.push(v.into_iter().collect());
|
| 241 |
+
}
|
| 242 |
+
|
| 243 |
+
// Replacements
|
| 244 |
+
for i in 0..n {
|
| 245 |
+
for c in 'a'..='z' {
|
| 246 |
+
if c != chars[i] {
|
| 247 |
+
let mut v = chars.clone();
|
| 248 |
+
v[i] = c;
|
| 249 |
+
results.push(v.into_iter().collect());
|
| 250 |
+
}
|
| 251 |
+
}
|
| 252 |
+
}
|
| 253 |
+
|
| 254 |
+
// Insertions
|
| 255 |
+
for i in 0..=n {
|
| 256 |
+
for c in 'a'..='z' {
|
| 257 |
+
let mut s = String::with_capacity(n + 1);
|
| 258 |
+
for (j, &ch) in chars.iter().enumerate() {
|
| 259 |
+
if j == i {
|
| 260 |
+
s.push(c);
|
| 261 |
+
}
|
| 262 |
+
s.push(ch);
|
| 263 |
+
}
|
| 264 |
+
if i == n {
|
| 265 |
+
s.push(c);
|
| 266 |
+
}
|
| 267 |
+
results.push(s);
|
| 268 |
+
}
|
| 269 |
+
}
|
| 270 |
+
|
| 271 |
+
results
|
| 272 |
+
}
|
src/tray.rs
ADDED
|
@@ -0,0 +1,221 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
// ---------------------------------------------------------------------------
|
| 2 |
+
// tray.rs — System tray icon, context menu, hotkey, and correction handler
|
| 3 |
+
// ---------------------------------------------------------------------------
|
| 4 |
+
|
| 5 |
+
use crate::hook::WM_DO_CORRECTION;
|
| 6 |
+
use crate::{CORRECTING, ENABLED, PENDING};
|
| 7 |
+
use std::sync::atomic::Ordering;
|
| 8 |
+
use windows::core::*;
|
| 9 |
+
use windows::Win32::Foundation::*;
|
| 10 |
+
use windows::Win32::UI::Input::KeyboardAndMouse::*;
|
| 11 |
+
use windows::Win32::UI::Shell::*;
|
| 12 |
+
use windows::Win32::UI::WindowsAndMessaging::*;
|
| 13 |
+
|
| 14 |
+
const WM_TRAYICON: u32 = WM_APP + 100;
|
| 15 |
+
const IDM_TOGGLE: u32 = 2001;
|
| 16 |
+
const IDM_EXIT: u32 = 2002;
|
| 17 |
+
const HOTKEY_ID: i32 = 1;
|
| 18 |
+
|
| 19 |
+
static mut HWND_TRAY: HWND = HWND(std::ptr::null_mut());
|
| 20 |
+
|
| 21 |
+
/// Get the tray window handle.
|
| 22 |
+
pub fn hwnd() -> HWND {
|
| 23 |
+
unsafe { HWND_TRAY }
|
| 24 |
+
}
|
| 25 |
+
|
| 26 |
+
/// Create the hidden message window, tray icon, and hotkey.
|
| 27 |
+
pub fn setup() -> HWND {
|
| 28 |
+
unsafe {
|
| 29 |
+
let class = w!("AutocorrectTypeTray");
|
| 30 |
+
let wc = WNDCLASSEXW {
|
| 31 |
+
cbSize: std::mem::size_of::<WNDCLASSEXW>() as u32,
|
| 32 |
+
lpfnWndProc: Some(wnd_proc),
|
| 33 |
+
lpszClassName: class,
|
| 34 |
+
..Default::default()
|
| 35 |
+
};
|
| 36 |
+
RegisterClassExW(&wc);
|
| 37 |
+
|
| 38 |
+
let h = CreateWindowExW(
|
| 39 |
+
WINDOW_EX_STYLE::default(),
|
| 40 |
+
class,
|
| 41 |
+
w!("Autocorrect-Type"),
|
| 42 |
+
WINDOW_STYLE::default(),
|
| 43 |
+
0, 0, 0, 0,
|
| 44 |
+
HWND_MESSAGE,
|
| 45 |
+
None,
|
| 46 |
+
None,
|
| 47 |
+
None,
|
| 48 |
+
)
|
| 49 |
+
.expect("CreateWindowExW failed");
|
| 50 |
+
|
| 51 |
+
HWND_TRAY = h;
|
| 52 |
+
|
| 53 |
+
// Tray icon
|
| 54 |
+
add_tray_icon(h);
|
| 55 |
+
|
| 56 |
+
// Ctrl+Alt+A hotkey
|
| 57 |
+
let _ = RegisterHotKey(h, HOTKEY_ID, MOD_CONTROL | MOD_ALT, 0x41); // 'A'
|
| 58 |
+
|
| 59 |
+
h
|
| 60 |
+
}
|
| 61 |
+
}
|
| 62 |
+
|
| 63 |
+
/// Show a balloon notification from the tray icon.
|
| 64 |
+
pub fn notify(title: &str, msg: &str) {
|
| 65 |
+
unsafe {
|
| 66 |
+
let mut nid = base_nid();
|
| 67 |
+
nid.uFlags = NIF_INFO;
|
| 68 |
+
copy_wide(title, &mut nid.szInfoTitle);
|
| 69 |
+
copy_wide(msg, &mut nid.szInfo);
|
| 70 |
+
nid.Anonymous.uTimeout = 2000;
|
| 71 |
+
let _ = Shell_NotifyIconW(NIM_MODIFY, &nid);
|
| 72 |
+
}
|
| 73 |
+
}
|
| 74 |
+
|
| 75 |
+
// ---------------------------------------------------------------------------
|
| 76 |
+
// internals
|
| 77 |
+
// ---------------------------------------------------------------------------
|
| 78 |
+
|
| 79 |
+
unsafe fn add_tray_icon(_hwnd: HWND) {
|
| 80 |
+
let mut nid = base_nid();
|
| 81 |
+
nid.uFlags = NIF_MESSAGE | NIF_ICON | NIF_TIP;
|
| 82 |
+
nid.uCallbackMessage = WM_TRAYICON;
|
| 83 |
+
nid.hIcon = LoadIconW(None, IDI_APPLICATION).unwrap_or_default();
|
| 84 |
+
copy_wide("Autocorrect-Type (Active)", &mut nid.szTip);
|
| 85 |
+
let _ = Shell_NotifyIconW(NIM_ADD, &nid);
|
| 86 |
+
}
|
| 87 |
+
|
| 88 |
+
unsafe fn remove_tray_icon() {
|
| 89 |
+
let nid = base_nid();
|
| 90 |
+
let _ = Shell_NotifyIconW(NIM_DELETE, &nid);
|
| 91 |
+
}
|
| 92 |
+
|
| 93 |
+
unsafe fn update_tip(text: &str) {
|
| 94 |
+
let mut nid = base_nid();
|
| 95 |
+
nid.uFlags = NIF_TIP;
|
| 96 |
+
copy_wide(text, &mut nid.szTip);
|
| 97 |
+
let _ = Shell_NotifyIconW(NIM_MODIFY, &nid);
|
| 98 |
+
}
|
| 99 |
+
|
| 100 |
+
unsafe fn base_nid() -> NOTIFYICONDATAW {
|
| 101 |
+
let mut nid = NOTIFYICONDATAW::default();
|
| 102 |
+
nid.cbSize = std::mem::size_of::<NOTIFYICONDATAW>() as u32;
|
| 103 |
+
nid.hWnd = HWND_TRAY;
|
| 104 |
+
nid.uID = 1;
|
| 105 |
+
nid
|
| 106 |
+
}
|
| 107 |
+
|
| 108 |
+
fn copy_wide(src: &str, dst: &mut [u16]) {
|
| 109 |
+
for (i, ch) in src.encode_utf16().enumerate() {
|
| 110 |
+
if i >= dst.len() - 1 {
|
| 111 |
+
break;
|
| 112 |
+
}
|
| 113 |
+
dst[i] = ch;
|
| 114 |
+
}
|
| 115 |
+
}
|
| 116 |
+
|
| 117 |
+
unsafe fn show_context_menu(hwnd: HWND) {
|
| 118 |
+
let menu = CreatePopupMenu().unwrap();
|
| 119 |
+
let enabled = ENABLED.load(Ordering::SeqCst);
|
| 120 |
+
let label = if enabled {
|
| 121 |
+
w!("Pause Autocorrect")
|
| 122 |
+
} else {
|
| 123 |
+
w!("Resume Autocorrect")
|
| 124 |
+
};
|
| 125 |
+
let _ = AppendMenuW(menu, MF_STRING, IDM_TOGGLE as usize, label);
|
| 126 |
+
let _ = AppendMenuW(menu, MF_SEPARATOR, 0, None);
|
| 127 |
+
let _ = AppendMenuW(menu, MF_STRING, IDM_EXIT as usize, w!("Exit"));
|
| 128 |
+
|
| 129 |
+
let mut pt = Default::default();
|
| 130 |
+
let _ = GetCursorPos(&mut pt);
|
| 131 |
+
let _ = SetForegroundWindow(hwnd);
|
| 132 |
+
let _ = TrackPopupMenu(menu, TPM_BOTTOMALIGN | TPM_LEFTALIGN, pt.x, pt.y, 0, hwnd, None);
|
| 133 |
+
let _ = PostMessageW(hwnd, WM_NULL, WPARAM(0), LPARAM(0));
|
| 134 |
+
let _ = DestroyMenu(menu);
|
| 135 |
+
}
|
| 136 |
+
|
| 137 |
+
// ---------------------------------------------------------------------------
|
| 138 |
+
// Window procedure
|
| 139 |
+
// ---------------------------------------------------------------------------
|
| 140 |
+
|
| 141 |
+
unsafe extern "system" fn wnd_proc(
|
| 142 |
+
hwnd: HWND,
|
| 143 |
+
msg: u32,
|
| 144 |
+
wparam: WPARAM,
|
| 145 |
+
lparam: LPARAM,
|
| 146 |
+
) -> LRESULT {
|
| 147 |
+
match msg {
|
| 148 |
+
// --- Tray icon interaction ---
|
| 149 |
+
WM_TRAYICON => {
|
| 150 |
+
let event = (lparam.0 & 0xFFFF) as u32;
|
| 151 |
+
if event == WM_RBUTTONUP || event == WM_CONTEXTMENU {
|
| 152 |
+
show_context_menu(hwnd);
|
| 153 |
+
}
|
| 154 |
+
LRESULT(0)
|
| 155 |
+
}
|
| 156 |
+
|
| 157 |
+
// --- Menu commands ---
|
| 158 |
+
WM_COMMAND => {
|
| 159 |
+
let id = (wparam.0 & 0xFFFF) as u32;
|
| 160 |
+
match id {
|
| 161 |
+
IDM_TOGGLE => toggle(),
|
| 162 |
+
IDM_EXIT => {
|
| 163 |
+
remove_tray_icon();
|
| 164 |
+
PostQuitMessage(0);
|
| 165 |
+
}
|
| 166 |
+
_ => {}
|
| 167 |
+
}
|
| 168 |
+
LRESULT(0)
|
| 169 |
+
}
|
| 170 |
+
|
| 171 |
+
// --- Hotkey (Ctrl+Alt+A) ---
|
| 172 |
+
WM_HOTKEY => {
|
| 173 |
+
if wparam.0 as i32 == HOTKEY_ID {
|
| 174 |
+
toggle();
|
| 175 |
+
}
|
| 176 |
+
LRESULT(0)
|
| 177 |
+
}
|
| 178 |
+
|
| 179 |
+
// --- Perform a scheduled correction ---
|
| 180 |
+
m if m == WM_DO_CORRECTION => {
|
| 181 |
+
do_correction();
|
| 182 |
+
LRESULT(0)
|
| 183 |
+
}
|
| 184 |
+
|
| 185 |
+
// --- Cleanup ---
|
| 186 |
+
WM_DESTROY => {
|
| 187 |
+
remove_tray_icon();
|
| 188 |
+
PostQuitMessage(0);
|
| 189 |
+
LRESULT(0)
|
| 190 |
+
}
|
| 191 |
+
|
| 192 |
+
_ => DefWindowProcW(hwnd, msg, wparam, lparam),
|
| 193 |
+
}
|
| 194 |
+
}
|
| 195 |
+
|
| 196 |
+
fn toggle() {
|
| 197 |
+
let was = ENABLED.load(Ordering::SeqCst);
|
| 198 |
+
ENABLED.store(!was, Ordering::SeqCst);
|
| 199 |
+
let (tip, msg) = if was {
|
| 200 |
+
("Autocorrect-Type (Paused)", "Autocorrect paused")
|
| 201 |
+
} else {
|
| 202 |
+
("Autocorrect-Type (Active)", "Autocorrect enabled")
|
| 203 |
+
};
|
| 204 |
+
unsafe { update_tip(tip) };
|
| 205 |
+
notify("Autocorrect-Type", msg);
|
| 206 |
+
}
|
| 207 |
+
|
| 208 |
+
fn do_correction() {
|
| 209 |
+
let data = {
|
| 210 |
+
let mut p = PENDING.lock().unwrap();
|
| 211 |
+
p.take()
|
| 212 |
+
};
|
| 213 |
+
if let Some((original, corrected, trigger)) = data {
|
| 214 |
+
CORRECTING.store(true, Ordering::SeqCst);
|
| 215 |
+
crate::replacer::replace(&original, &corrected, &trigger);
|
| 216 |
+
CORRECTING.store(false, Ordering::SeqCst);
|
| 217 |
+
|
| 218 |
+
// Show a tooltip-like notification
|
| 219 |
+
notify("Autocorrect-Type", &format!("{original} → {corrected}"));
|
| 220 |
+
}
|
| 221 |
+
}
|
words.txt
ADDED
|
The diff for this file is too large to render.
See raw diff
|
|
|