algorembrant commited on
Commit
725a1f8
·
verified ·
1 Parent(s): 7bf1067

Upload 12 files

Browse files
Files changed (12) hide show
  1. .gitignore +26 -0
  2. Cargo.lock +173 -0
  3. Cargo.toml +23 -0
  4. LICENSE +21 -0
  5. README.md +144 -0
  6. STACKS.md +71 -0
  7. src/hook.rs +193 -0
  8. src/main.rs +59 -0
  9. src/replacer.rs +111 -0
  10. src/spellcheck.rs +272 -0
  11. src/tray.rs +221 -0
  12. 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
+ ![License](https://img.shields.io/github/license/not-algorembrant/autocorrect-type)
4
+ ![Repo Size](https://img.shields.io/github/repo-size/not-algorembrant/autocorrect-type)
5
+ ![Last Commit](https://img.shields.io/github/last-commit/not-algorembrant/autocorrect-type)
6
+ ![Rust](https://img.shields.io/badge/Rust-5-dea584)
7
+ ![Markdown](https://img.shields.io/badge/Markdown-2-083fa1)
8
+ ![TOML](https://img.shields.io/badge/TOML-1-9c4121)
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