├── ChatKey.ahk
├── LICENSE
├── README.md
├── assets
├── app.ico
├── enter.ico
├── enter.png
├── logo.png
└── screenshot.png
├── config.ini
└── libs
├── HBitmapFromResource.ahk
└── JSON.ahk
/ChatKey.ahk:
--------------------------------------------------------------------------------
1 | ;@Ahk2Exe-Base %A_AhkPath%
2 |
3 | ;@Ahk2Exe-SetName ChatKey
4 | ;@Ahk2Exe-SetVersion 2.0.0
5 | ;@Ahk2Exe-SetDescription ChatKey is small tool that enables you to use your own ChatGPT/GPT-4 prompts in any application that supports text input.
6 | ;@Ahk2Exe-SetCopyright github.com/overflowy
7 | ;@Ahk2Exe-AddResource %A_ScriptDir%\assets\app.ico
8 | ;@Ahk2Exe-AddResource %A_ScriptDir%\assets\enter.png
9 |
10 | #NoEnv ; Recommended for performance and compatibility with future AutoHotkey releases.
11 | ; #Warn ; Enable warnings to assist with detecting common errors.
12 | SendMode Input ; Recommended for new scripts due to its superior speed and reliability.
13 | SetWorkingDir %A_ScriptDir% ; Ensures a consistent starting directory.
14 | #Persistent
15 | #Include libs\JSON.ahk
16 | #Include libs\HBitmapFromResource.ahk
17 |
18 | ; Set the tray icon
19 | ;@Ahk2Exe-IgnoreBegin
20 | Menu, Tray, Icon, %A_ScriptDir%\assets\app.ico
21 | ;@Ahk2Exe-IgnoreEnd
22 | /*@Ahk2Exe-Keep
23 | Menu, Tray, Icon, %A_ScriptName%
24 | */
25 |
26 | ; Use OPENAI_TOKEN environment variable
27 | EnvGet, API_KEY, OPENAI_TOKEN
28 |
29 | ; If the API key is not set, show an error message and exit
30 | if (API_KEY = "") {
31 | MsgBox, 0x30, Missing API Key, Please set the OPENAI_TOKEN environment variable to your OpenAI API key.
32 | ExitApp
33 | }
34 |
35 | ; If the config file is missing, show an error message and exit
36 | if not (FileExist("config.ini")) {
37 | MsgBox, 0x10, Missing Config File, Please make sure the config file is in the same directory as the script.
38 | ExitApp
39 | }
40 |
41 | prevClipboard := ""
42 |
43 | CopyText() {
44 | ; Save the previous clipboard contents
45 | global prevClipboard := clipboard
46 |
47 | ; Copy the selected text
48 | clipboard := ""
49 | SendInput, ^c
50 | ClipWait, 1
51 |
52 | ; Check if the clipboard is empty
53 | if (clipboard == "") {
54 | MsgBox, 0x30, No Text Selected, Please select some text before running the script.
55 | clipboard := prevClipboard
56 | return
57 | }
58 |
59 | ; Check if the selected text is too long
60 | IniRead, max_input_length, config.ini, settings, max_input_length, "0"
61 | if (max_input_length != "0") {
62 | if (StrLen(clipboard) > max_input_length) {
63 | MsgBox, 0x30, Input Too Long, The selected text is too long. Please select a shorter text.
64 | clipboard := prevClipboard
65 | return
66 | }
67 | }
68 |
69 | clipboard := RTrim(clipboard, "`n")
70 | return clipboard
71 | }
72 |
73 | PasteText(text) {
74 | global prevClipboard
75 |
76 | IniRead, replace_text, config.ini, settings, replace_text, "1"
77 |
78 | if (replace_text == "1") {
79 | clipboard := text
80 | }
81 | else {
82 | newText = %clipboard%`n`n%text%
83 | clipboard := newText
84 | }
85 |
86 | SendInput, ^v
87 |
88 | ; Restore the previous clipboard contents
89 | Sleep, 500
90 | clipboard := prevClipboard
91 | }
92 |
93 | PrepareRequestBody(section) {
94 | ; Copy the selected text]
95 | text := CopyText()
96 |
97 | Gui, Destroy ; Destroy previous GUI
98 |
99 | if (text == "") {
100 | return
101 | }
102 |
103 | ; Read config parameters
104 | IniRead, model, config.ini, % section, model, gpt-3.5-turbo
105 | IniRead, temperature, config.ini, % section, temperature, 0.7
106 |
107 | IniRead, top_p, config.ini, % section, top_p
108 | IniRead, max_tokens, config.ini, % section, max_tokens
109 | IniRead, presence_penalty, config.ini, % section, presence_penalty
110 | IniRead, frequency_penalty, config.ini, % section, frequency_penalty
111 |
112 | IniRead, system_content, config.ini, % section, system_content
113 |
114 | ; Make sure system_content param is defined
115 | if (system_content == "ERROR") {
116 | MsgBox, 0x30, Missing System Prompt, Please set the system_content parameter in the config file.
117 | return
118 | }
119 |
120 | ; Prepare the request body
121 | requestBody := {}
122 | requestBody.model := model
123 | requestBody.temperature := temperature + 0
124 |
125 | if (top_p != "ERROR") {
126 | requestBody.top_p := top_p + 0
127 | }
128 | if (max_tokens != "ERROR") {
129 | requestBody.max_tokens := max_tokens + 0
130 | }
131 | if (presence_penalty != "ERROR") {
132 | requestBody.presence_penalty := presence_penalty + 0
133 | }
134 | if (frequency_penalty != "ERROR") {
135 | requestBody.frequency_penalty := frequency_penalty + 0
136 | }
137 |
138 | requestBody.messages := [{"role": "system", "content": system_content}, {"role": "user", "content": text}]
139 |
140 | return requestBody
141 | }
142 |
143 | SendRequest(requestBody) {
144 | global API_KEY
145 |
146 | ; Convert the request body to valid JSON
147 | requestBodyJson := Json.Dump(requestBody)
148 |
149 | ; Send the request
150 | http := ComObjCreate("Msxml2.ServerXMLHTTP")
151 | http.Open("POST", "https://api.openai.com/v1/chat/completions", false)
152 | http.SetRequestHeader("Authorization", "Bearer " . API_KEY)
153 | http.SetRequestHeader("Content-Type", "application/json")
154 | http.Send(requestBodyJson)
155 |
156 | ; Parse the response
157 | response := http.ResponseText
158 | jsonResponse := Json.Load(response)
159 |
160 | ; Check for errors
161 | err := jsonResponse.error.message
162 | if (err != "") {
163 | MsgBox, 0x30, Response Error, % err
164 | return
165 | }
166 |
167 | ; Return the message content
168 | return jsonResponse.choices[1].message.content
169 | }
170 |
171 | HideTrayTip() {
172 | TrayTip ; Attempt to hide it the normal way
173 | if SubStr(A_OSVersion,1,3) = "10." {
174 | Menu Tray, NoIcon
175 | Sleep 200
176 | Menu Tray, Icon
177 | }
178 | }
179 |
180 | ; Show the popup menu
181 | ShowMenu() {
182 | try {
183 | Menu, popupMenu, DeleteAll
184 | }
185 | catch e {
186 | ; Do nothing
187 | }
188 |
189 | IniRead, menu_items, config.ini, popup_menu
190 | prompts := {}
191 |
192 | Loop, Parse, menu_items, `n
193 | {
194 | section := A_LoopField
195 | if (section == "---") { ; Separator
196 | Menu, popupMenu, Add
197 | }
198 | else {
199 | IniRead, name, config.ini, % section, name
200 | IniRead, shortcut, config.ini, % section, shortcut
201 |
202 | label = &%shortcut% %name%
203 | prompts[label] := section
204 | }
205 | Menu, popupMenu, Add, % label, MenuHandler
206 | }
207 | Menu, popupMenu, Show
208 | return
209 |
210 | MenuHandler:
211 | IniRead, show_notification, config.ini, settings, show_notification, "1"
212 | ; Show a tray tip while waiting for the response if show_notifications is enabled
213 | if (show_notification == "1") {
214 | TrayTip,, Waiting..., 5, 1
215 | }
216 |
217 | section := prompts[A_ThisMenuItem]
218 |
219 | ; Prepare the request body
220 | requestBody := PrepareRequestBody(section)
221 | if (requestBody == "") {
222 | return
223 | }
224 |
225 | ; Send the request and get the response
226 | responseText := SendRequest(requestBody)
227 |
228 | if (show_notification == "1") {
229 | HideTrayTip()
230 | }
231 |
232 | if (responseText == "") {
233 | return
234 | }
235 |
236 | DllCall("User32\SetThreadDpiAwarenessContext", "UInt" , -1) ; Disable DPI scaling
237 | Gui, -Caption +AlwaysOnTop -DPIScale
238 | Gui, Margin, 0, 0
239 | Gui, Color, c1d1d1d, c1d1d1d
240 | Gui, Add, Progress, x-1 y-1 w400 h32 Backgroundb5614b Disabled
241 | Gui, Font, s13 c1d1d1d, Segoe UI
242 | Gui, Add, Text, x0 y0 w400 h30 BackgroundTrans Center 0x200 gGuiMove vCaption, ChatKey
243 | Gui, Font, s12 c1d1d1d, Consolas
244 | Gui, Add, Edit, vMainEdit cb4b4b4 x6 y+14 w410 r20 -E0x200 ; Add the edit box
245 | GuiControl,, MainEdit, % responseText ; Set the text
246 |
247 | ;@Ahk2Exe-IgnoreBegin
248 | Gui, Add, Picture, x350 y+25 w42 h42 gConfirm, %A_ScriptDir%\assets\enter.ico
249 | ;@Ahk2Exe-IgnoreEnd
250 | /*@Ahk2Exe-Keep
251 | handler := HBitmapFromResource("enter.png")
252 | Gui, Add, Picture, x350 y+25 gConfirm, % "HBITMAP:" . handler
253 | */
254 |
255 | Gui, Add, Button, y+10 Default gConfirm, Confirm ; Add a hidden button so we can use Enter to confirm
256 | Gui, +LastFound ; Make the GUI window the last found window for the next WinSet
257 | WinSet, Region, 0-0 w400 h508 r6-6 ; Round the corners
258 | Gui, Show, w400
259 | SendInput, {End} ; Move the cursor to the end of the text
260 | GuiControl, Focus, Confirm ; Focus the hidden button
261 | return
262 |
263 | Confirm:
264 | Gui, Submit, NoHide
265 | GuiControlGet, text,, MainEdit
266 | Gui, Destroy
267 |
268 | PasteText(text)
269 | return
270 |
271 | GuiMove:
272 | PostMessage, 0xA1, 2
273 | return
274 |
275 | GuiEscape:
276 | Gui, Destroy
277 | return
278 | }
279 |
280 | ; Init the popup menu hotkey
281 | IniRead, popup_menu_hotkey, config.ini, settings, popup_menu_hotkey, !.
282 | Hotkey, % popup_menu_hotkey, ShowMenu
283 |
284 | TrayTip,, Ready to use, 3, 1
285 |
286 | Menu, Tray, NoStandard
287 | Menu, Tray, Add, Edit Config, EditConfig
288 | Menu, Tray, Add, Reload, Reload_
289 | Menu, Tray, Add, Update ChatKey, Update
290 | Menu, Tray, Add, About
291 | Menu, Tray, Add, Exit
292 | return
293 |
294 | EditConfig:
295 | Run, %A_ScriptDir%\config.ini
296 | return
297 |
298 | Reload_:
299 | Reload
300 | return
301 |
302 | Update:
303 | Run, https://github.com/overflowy/chat-key/releases/latest/download/ChatKey.zip
304 | return
305 |
306 | About:
307 | Run, https://github.com/overflowy/chat-key
308 | return
309 |
310 | Exit:
311 | ExitApp
312 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2023 overflowy@riseup.net
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.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |

2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 | ## About
19 |
20 | ChatKey is small tool that enables you to use your own ChatGPT/GPT-4 prompts in any application that supports text input.
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 | ## Usage
29 |
30 | 1. Please ensure that you have configured the OPENAI_TOKEN environment variable with your API key
31 | 2. Download the [latest release](https://github.com/overflowy/chat-key/releases/latest)
32 | 3. Extract all files from the zip
33 | 4. Run `ChatKey.exe`
34 | 5. Start typing in any application that supports text input
35 | 6. Select the text to use as input for the prompt
36 | 7. Press the hotkey to show the popup menu (default: `Alt + .`).
37 | 8. Select the prompt from the popup menu
38 | 9. Wait for the response to be generated
39 | 10. Review the generated response and press `Enter`
40 |
41 | [I versus AI](https://www.youtube.com/@IversusAI) has done an incredible video covering most topics:
42 |
43 | [](https://www.youtube.com/watch?v=4mraUhvVrOc&t=628s)
44 |
45 | ## Configuration
46 |
47 | To configure ChatKey, you can edit the [`config.ini`](config.ini) file provided.
48 |
49 | ### General settings
50 |
51 | | Key | Description | Default |
52 | | ------------------- | --------------------------------------------------------- | --------- |
53 | | `popup_menu_hotkey` | The hotkey to show the popup menu | `Alt + .` |
54 | | `replace_text` | Whether to replace the selected text with the response | `0` |
55 | | `show_notification` | Whether to show a notification when generating a response | `1` |
56 | | `max_input_length` | The maximum length of input text (0 = unlimited) | `0` |
57 |
58 | ### Adding prompts
59 |
60 | To add new prompts, you must include a new section in the [`config.ini`](config.ini) file. For instance, if you wish to include a prompt for translating text to French, you can achieve this by appending the following section to the configuration file:
61 |
62 | ```ini
63 | [prompt_translate_to_french]
64 | name = Translate to French
65 | shortcut = t
66 | system_prompt = "I want you to act as a French translator. I will say something in any language and you will translate it to French. The first thing I want you to translate is:"
67 | temperature = 0.2
68 | model = gpt-3.5-turbo
69 | ```
70 |
71 | To ensure that the newly added prompt is available in the popup menu, it must be included in the `[popup_menu]` section. Additionally, if you have already configured multiple prompts, you can tidy up the popup menu by utilizing `---` as a separator.
72 |
73 | ```ini
74 | [popup_menu]
75 | ---
76 | prompt_translate_to_french
77 | ```
78 |
79 | The changes will be applied automatically, there's no need to restart ChatKey (the only exception to this rule is the global `popup_menu_hotkey`).
80 |
81 | ### Prompt settings
82 |
83 | You can individually configure the parameters of each prompt. If keys with default values are omitted, the default values will be used instead.
84 |
85 | | Key | Description | Default |
86 | | ------------------- | -------------------------------------------------------------------------------------------------------- | --------- |
87 | | `name` | The name of the prompt that will be displayed in the popup menu | |
88 | | `shortcut` | The shortcut key to select the prompt from the popup menu | |
89 | | `system_content` | The prompt that will be used to generate the response (required) | |
90 | | `model` | The model to use when generating the response, more info [here](https://platform.openai.com/docs/models) | `gpt-3.5` |
91 | | `temperature` | The temperature to use when generating the response (0.0 - 2.0) | `0.7` |
92 | | `top_p` | The top_p to use when generating the response (0.0 - 1.0) | |
93 | | `presence_penalty` | Increase the model's likelihood to talk about new topics (-2.0 - 2.0) | |
94 | | `frequency_penalty` | Decrease the model's likelihood to repeat the same line verbatim (-2.0 - 2.0) | |
95 |
96 | ## Acknowledgements
97 |
98 | - [I versus AI](https://www.youtube.com/@IversusAI) for the awesome tutorial video
99 | - [cocobelgica](https://github.com/cocobelgica) for the JSON lib
100 | - [teadrinker](https://www.autohotkey.com/boards/viewtopic.php?t=113529) for the HBitmapFromResource lib
101 |
102 |
103 | ## License
104 |
105 | The code in this repository is licensed under the MIT License. See [LICENSE](LICENSE) for more information.
106 |
--------------------------------------------------------------------------------
/assets/app.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/overflowy/chat-key/97c52aaffed855cbb50c4e9e1bf1e9b04512e154/assets/app.ico
--------------------------------------------------------------------------------
/assets/enter.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/overflowy/chat-key/97c52aaffed855cbb50c4e9e1bf1e9b04512e154/assets/enter.ico
--------------------------------------------------------------------------------
/assets/enter.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/overflowy/chat-key/97c52aaffed855cbb50c4e9e1bf1e9b04512e154/assets/enter.png
--------------------------------------------------------------------------------
/assets/logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/overflowy/chat-key/97c52aaffed855cbb50c4e9e1bf1e9b04512e154/assets/logo.png
--------------------------------------------------------------------------------
/assets/screenshot.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/overflowy/chat-key/97c52aaffed855cbb50c4e9e1bf1e9b04512e154/assets/screenshot.png
--------------------------------------------------------------------------------
/config.ini:
--------------------------------------------------------------------------------
1 | [settings]
2 | # CTRL = ^, ALT = !, SHIFT = +
3 | popup_menu_hotkey = !.
4 | replace_text = 1
5 | show_notification = 1
6 | max_input_length = 0
7 |
8 | [popup_menu]
9 | prompt_follow
10 | ---
11 | prompt_better
12 | prompt_autocomplete
13 | prompt_explain
14 | prompt_translate_en
15 | prompt_critique
16 | prompt_summarize
17 | prompt_key_points
18 |
19 | [prompt_follow]
20 | name = Follow the instructions
21 | shortcut = .
22 | system_content = "Follow the following intructions:"
23 |
24 | [prompt_better]
25 | name = Make it sound better
26 | shortcut = 1
27 | system_content = "Make the following sound better:"
28 |
29 | [prompt_autocomplete]
30 | name = Autocomplete
31 | shortcut = 2
32 | system_content = "Please complete the following:"
33 |
34 | [prompt_explain]
35 | name = Explain in simple terms
36 | shortcut = 3
37 | system_content = "Explain in simple terms the following:"
38 |
39 | [prompt_translate_en]
40 | name = Translate to English
41 | shortcut = 4
42 | system_content = "I want you to act as an English translator, spelling corrector and improver. I will speak to you in any language and you will detect the language, translate it and answer in the corrected and improved version of my text, in English. I want you to replace my simplified A0-level words and sentences with more beautiful and elegant, upper level English words and sentences. Keep the meaning same, but make them more literary. I want you to only reply the correction, the improvements and nothing else, do not write explanations. My first sentence is:"
43 | temperature = 0.2
44 |
45 | [prompt_critique]
46 | name = Critique writing
47 | shortcut = 5
48 | system_content = "I want you to act as a writing critic. Critique the following text and convince me why it is not good. Let's think about the problems of the text step by step, then rewrite the text and improve it based on your criticism."
49 |
50 | [prompt_summarize]
51 | name = Summarize
52 | shortcut = 6
53 | system_content = "Summarize the following text:"
54 |
55 | [prompt_key_points]
56 | name = List key points
57 | shortcut = 7
58 | system_content = "List the key points of the following text as a bullet list:"
59 |
--------------------------------------------------------------------------------
/libs/HBitmapFromResource.ahk:
--------------------------------------------------------------------------------
1 | HBitmapFromResource(resName) {
2 | hMod := DllCall("GetModuleHandle", "Ptr", 0, "Ptr")
3 | hRes := DllCall("FindResource", "Ptr", hMod, "Str", resName, "UInt", RT_RCDATA := 10, "Ptr")
4 | resSize := DllCall("SizeofResource", "Ptr", hMod, "Ptr", hRes)
5 | hResData := DllCall("LoadResource", "Ptr", hMod, "Ptr", hRes, "Ptr")
6 | pBuff := DllCall("LockResource", "Ptr", hResData, "Ptr")
7 | pStream := DllCall("Shlwapi\SHCreateMemStream", "Ptr", pBuff, "UInt", resSize, "Ptr")
8 |
9 | Gdip := new GDIplus
10 | pBitmap := Gdip.CreateBitmapFromStream(pStream)
11 | hBitmap := Gdip.CreateHBITMAPFromBitmap(pBitmap)
12 | Gdip.DisposeImage(pBitmap)
13 | ObjRelease(pStream)
14 | Return hBitmap
15 | }
16 |
17 | class GDIplus {
18 | __New() {
19 | if !DllCall("GetModuleHandle", "Str", "gdiplus", "Ptr")
20 | DllCall("LoadLibrary", "Str", "gdiplus")
21 | VarSetCapacity(si, A_PtrSize = 8 ? 24 : 16, 0), si := Chr(1)
22 | DllCall("gdiplus\GdiplusStartup", "UPtrP", pToken, "Ptr", &si, "Ptr", 0)
23 | this.token := pToken
24 | }
25 | __Delete() {
26 | DllCall("gdiplus\GdiplusShutdown", "Ptr", this.token)
27 | if hModule := DllCall("GetModuleHandle", "Str", "gdiplus", "Ptr")
28 | DllCall("FreeLibrary", "Ptr", hModule)
29 | }
30 | CreateBitmapFromStream(pStream) {
31 | DllCall("gdiplus\GdipCreateBitmapFromStream", "Ptr", pStream, "PtrP", pBitmap)
32 | Return pBitmap
33 | }
34 | CreateHBITMAPFromBitmap(pBitmap, background := 0xffffffff) {
35 | DllCall("gdiplus\GdipCreateHBITMAPFromBitmap", "Ptr", pBitmap, "PtrP", hbm, "UInt", background)
36 | return hbm
37 | }
38 | DisposeImage(pBitmap) {
39 | return DllCall("gdiplus\GdipDisposeImage", "Ptr", pBitmap)
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/libs/JSON.ahk:
--------------------------------------------------------------------------------
1 | /**
2 | * Lib: JSON.ahk
3 | * JSON lib for AutoHotkey.
4 | * Version:
5 | * v2.1.3 [updated 04/18/2016 (MM/DD/YYYY)]
6 | * License:
7 | * WTFPL [http://wtfpl.net/]
8 | * Requirements:
9 | * Latest version of AutoHotkey (v1.1+ or v2.0-a+)
10 | * Installation:
11 | * Use #Include JSON.ahk or copy into a function library folder and then
12 | * use #Include
13 | * Links:
14 | * GitHub: - https://github.com/cocobelgica/AutoHotkey-JSON
15 | * Forum Topic - http://goo.gl/r0zI8t
16 | * Email: - cocobelgica gmail com
17 | */
18 |
19 |
20 | /**
21 | * Class: JSON
22 | * The JSON object contains methods for parsing JSON and converting values
23 | * to JSON. Callable - NO; Instantiable - YES; Subclassable - YES;
24 | * Nestable(via #Include) - NO.
25 | * Methods:
26 | * Load() - see relevant documentation before method definition header
27 | * Dump() - see relevant documentation before method definition header
28 | */
29 | class JSON
30 | {
31 | /**
32 | * Method: Load
33 | * Parses a JSON string into an AHK value
34 | * Syntax:
35 | * value := JSON.Load( text [, reviver ] )
36 | * Parameter(s):
37 | * value [retval] - parsed value
38 | * text [in, ByRef] - JSON formatted string
39 | * reviver [in, opt] - function object, similar to JavaScript's
40 | * JSON.parse() 'reviver' parameter
41 | */
42 | class Load extends JSON.Functor
43 | {
44 | Call(self, ByRef text, reviver:="")
45 | {
46 | this.rev := IsObject(reviver) ? reviver : false
47 | ; Object keys(and array indices) are temporarily stored in arrays so that
48 | ; we can enumerate them in the order they appear in the document/text instead
49 | ; of alphabetically. Skip if no reviver function is specified.
50 | this.keys := this.rev ? {} : false
51 |
52 | static quot := Chr(34), bashq := "\" . quot
53 | , json_value := quot . "{[01234567890-tfn"
54 | , json_value_or_array_closing := quot . "{[]01234567890-tfn"
55 | , object_key_or_object_closing := quot . "}"
56 |
57 | key := ""
58 | is_key := false
59 | root := {}
60 | stack := [root]
61 | next := json_value
62 | pos := 0
63 |
64 | while ((ch := SubStr(text, ++pos, 1)) != "") {
65 | if InStr(" `t`r`n", ch)
66 | continue
67 | if !InStr(next, ch, 1)
68 | this.ParseError(next, text, pos)
69 |
70 | holder := stack[1]
71 | is_array := holder.IsArray
72 |
73 | if InStr(",:", ch) {
74 | next := (is_key := !is_array && ch == ",") ? quot : json_value
75 |
76 | } else if InStr("}]", ch) {
77 | ObjRemoveAt(stack, 1)
78 | next := stack[1]==root ? "" : stack[1].IsArray ? ",]" : ",}"
79 |
80 | } else {
81 | if InStr("{[", ch) {
82 | ; Check if Array() is overridden and if its return value has
83 | ; the 'IsArray' property. If so, Array() will be called normally,
84 | ; otherwise, use a custom base object for arrays
85 | static json_array := Func("Array").IsBuiltIn || ![].IsArray ? {IsArray: true} : 0
86 |
87 | ; sacrifice readability for minor(actually negligible) performance gain
88 | (ch == "{")
89 | ? ( is_key := true
90 | , value := {}
91 | , next := object_key_or_object_closing )
92 | ; ch == "["
93 | : ( value := json_array ? new json_array : []
94 | , next := json_value_or_array_closing )
95 |
96 | ObjInsertAt(stack, 1, value)
97 |
98 | if (this.keys)
99 | this.keys[value] := []
100 |
101 | } else {
102 | if (ch == quot) {
103 | i := pos
104 | while (i := InStr(text, quot,, i+1)) {
105 | value := StrReplace(SubStr(text, pos+1, i-pos-1), "\\", "\u005c")
106 |
107 | static tail := A_AhkVersion<"2" ? 0 : -1
108 | if (SubStr(value, tail) != "\")
109 | break
110 | }
111 |
112 | if (!i)
113 | this.ParseError("'", text, pos)
114 |
115 | value := StrReplace(value, "\/", "/")
116 | , value := StrReplace(value, bashq, quot)
117 | , value := StrReplace(value, "\b", "`b")
118 | , value := StrReplace(value, "\f", "`f")
119 | , value := StrReplace(value, "\n", "`n")
120 | , value := StrReplace(value, "\r", "`r")
121 | , value := StrReplace(value, "\t", "`t")
122 |
123 | pos := i ; update pos
124 |
125 | i := 0
126 | while (i := InStr(value, "\",, i+1)) {
127 | if !(SubStr(value, i+1, 1) == "u")
128 | this.ParseError("\", text, pos - StrLen(SubStr(value, i+1)))
129 |
130 | uffff := Abs("0x" . SubStr(value, i+2, 4))
131 | if (A_IsUnicode || uffff < 0x100)
132 | value := SubStr(value, 1, i-1) . Chr(uffff) . SubStr(value, i+6)
133 | }
134 |
135 | if (is_key) {
136 | key := value, next := ":"
137 | continue
138 | }
139 |
140 | } else {
141 | value := SubStr(text, pos, i := RegExMatch(text, "[\]\},\s]|$",, pos)-pos)
142 |
143 | static number := "number", integer :="integer"
144 | if value is %number%
145 | {
146 | if value is %integer%
147 | value += 0
148 | }
149 | else if (value == "true" || value == "false")
150 | value := %value% + 0
151 | else if (value == "null")
152 | value := ""
153 | else
154 | ; we can do more here to pinpoint the actual culprit
155 | ; but that's just too much extra work.
156 | this.ParseError(next, text, pos, i)
157 |
158 | pos += i-1
159 | }
160 |
161 | next := holder==root ? "" : is_array ? ",]" : ",}"
162 | } ; If InStr("{[", ch) { ... } else
163 |
164 | is_array? key := ObjPush(holder, value) : holder[key] := value
165 |
166 | if (this.keys && this.keys.HasKey(holder))
167 | this.keys[holder].Push(key)
168 | }
169 |
170 | } ; while ( ... )
171 |
172 | return this.rev ? this.Walk(root, "") : root[""]
173 | }
174 |
175 | ParseError(expect, ByRef text, pos, len:=1)
176 | {
177 | static quot := Chr(34), qurly := quot . "}"
178 |
179 | line := StrSplit(SubStr(text, 1, pos), "`n", "`r").Length()
180 | col := pos - InStr(text, "`n",, -(StrLen(text)-pos+1))
181 | msg := Format("{1}`n`nLine:`t{2}`nCol:`t{3}`nChar:`t{4}"
182 | , (expect == "") ? "Extra data"
183 | : (expect == "'") ? "Unterminated string starting at"
184 | : (expect == "\") ? "Invalid \escape"
185 | : (expect == ":") ? "Expecting ':' delimiter"
186 | : (expect == quot) ? "Expecting object key enclosed in double quotes"
187 | : (expect == qurly) ? "Expecting object key enclosed in double quotes or object closing '}'"
188 | : (expect == ",}") ? "Expecting ',' delimiter or object closing '}'"
189 | : (expect == ",]") ? "Expecting ',' delimiter or array closing ']'"
190 | : InStr(expect, "]") ? "Expecting JSON value or array closing ']'"
191 | : "Expecting JSON value(string, number, true, false, null, object or array)"
192 | , line, col, pos)
193 |
194 | static offset := A_AhkVersion<"2" ? -3 : -4
195 | throw Exception(msg, offset, SubStr(text, pos, len))
196 | }
197 |
198 | Walk(holder, key)
199 | {
200 | value := holder[key]
201 | if IsObject(value) {
202 | for i, k in this.keys[value] {
203 | ; check if ObjHasKey(value, k) ??
204 | v := this.Walk(value, k)
205 | if (v != JSON.Undefined)
206 | value[k] := v
207 | else
208 | ObjDelete(value, k)
209 | }
210 | }
211 |
212 | return this.rev.Call(holder, key, value)
213 | }
214 | }
215 |
216 | /**
217 | * Method: Dump
218 | * Converts an AHK value into a JSON string
219 | * Syntax:
220 | * str := JSON.Dump( value [, replacer, space ] )
221 | * Parameter(s):
222 | * str [retval] - JSON representation of an AHK value
223 | * value [in] - any value(object, string, number)
224 | * replacer [in, opt] - function object, similar to JavaScript's
225 | * JSON.stringify() 'replacer' parameter
226 | * space [in, opt] - similar to JavaScript's JSON.stringify()
227 | * 'space' parameter
228 | */
229 | class Dump extends JSON.Functor
230 | {
231 | Call(self, value, replacer:="", space:="")
232 | {
233 | this.rep := IsObject(replacer) ? replacer : ""
234 |
235 | this.gap := ""
236 | if (space) {
237 | static integer := "integer"
238 | if space is %integer%
239 | Loop, % ((n := Abs(space))>10 ? 10 : n)
240 | this.gap .= " "
241 | else
242 | this.gap := SubStr(space, 1, 10)
243 |
244 | this.indent := "`n"
245 | }
246 |
247 | return this.Str({"": value}, "")
248 | }
249 |
250 | Str(holder, key)
251 | {
252 | value := holder[key]
253 |
254 | if (this.rep)
255 | value := this.rep.Call(holder, key, ObjHasKey(holder, key) ? value : JSON.Undefined)
256 |
257 | if IsObject(value) {
258 | ; Check object type, skip serialization for other object types such as
259 | ; ComObject, Func, BoundFunc, FileObject, RegExMatchObject, Property, etc.
260 | static type := A_AhkVersion<"2" ? "" : Func("Type")
261 | if (type ? type.Call(value) == "Object" : ObjGetCapacity(value) != "") {
262 | if (this.gap) {
263 | stepback := this.indent
264 | this.indent .= this.gap
265 | }
266 |
267 | is_array := value.IsArray
268 | ; Array() is not overridden, rollback to old method of
269 | ; identifying array-like objects. Due to the use of a for-loop
270 | ; sparse arrays such as '[1,,3]' are detected as objects({}).
271 | if (!is_array) {
272 | for i in value
273 | is_array := i == A_Index
274 | until !is_array
275 | }
276 |
277 | str := ""
278 | if (is_array) {
279 | Loop, % value.Length() {
280 | if (this.gap)
281 | str .= this.indent
282 |
283 | v := this.Str(value, A_Index)
284 | str .= (v != "") ? v . "," : "null,"
285 | }
286 | } else {
287 | colon := this.gap ? ": " : ":"
288 | for k in value {
289 | v := this.Str(value, k)
290 | if (v != "") {
291 | if (this.gap)
292 | str .= this.indent
293 |
294 | str .= this.Quote(k) . colon . v . ","
295 | }
296 | }
297 | }
298 |
299 | if (str != "") {
300 | str := RTrim(str, ",")
301 | if (this.gap)
302 | str .= stepback
303 | }
304 |
305 | if (this.gap)
306 | this.indent := stepback
307 |
308 | return is_array ? "[" . str . "]" : "{" . str . "}"
309 | }
310 |
311 | } else ; is_number ? value : "value"
312 | return ObjGetCapacity([value], 1)=="" ? value : this.Quote(value)
313 | }
314 |
315 | Quote(string)
316 | {
317 | static quot := Chr(34), bashq := "\" . quot
318 |
319 | if (string != "") {
320 | string := StrReplace(string, "\", "\\")
321 | ; , string := StrReplace(string, "/", "\/") ; optional in ECMAScript
322 | , string := StrReplace(string, quot, bashq)
323 | , string := StrReplace(string, "`b", "\b")
324 | , string := StrReplace(string, "`f", "\f")
325 | , string := StrReplace(string, "`n", "\n")
326 | , string := StrReplace(string, "`r", "\r")
327 | , string := StrReplace(string, "`t", "\t")
328 |
329 | static rx_escapable := A_AhkVersion<"2" ? "O)[^\x20-\x7e]" : "[^\x20-\x7e]"
330 | while RegExMatch(string, rx_escapable, m)
331 | string := StrReplace(string, m.Value, Format("\u{1:04x}", Ord(m.Value)))
332 | }
333 |
334 | return quot . string . quot
335 | }
336 | }
337 |
338 | /**
339 | * Property: Undefined
340 | * Proxy for 'undefined' type
341 | * Syntax:
342 | * undefined := JSON.Undefined
343 | * Remarks:
344 | * For use with reviver and replacer functions since AutoHotkey does not
345 | * have an 'undefined' type. Returning blank("") or 0 won't work since these
346 | * can't be distnguished from actual JSON values. This leaves us with objects.
347 | * Replacer() - the caller may return a non-serializable AHK objects such as
348 | * ComObject, Func, BoundFunc, FileObject, RegExMatchObject, and Property to
349 | * mimic the behavior of returning 'undefined' in JavaScript but for the sake
350 | * of code readability and convenience, it's better to do 'return JSON.Undefined'.
351 | * Internally, the property returns a ComObject with the variant type of VT_EMPTY.
352 | */
353 | Undefined[]
354 | {
355 | get {
356 | static empty := {}, vt_empty := ComObject(0, &empty, 1)
357 | return vt_empty
358 | }
359 | }
360 |
361 | class Functor
362 | {
363 | __Call(method, ByRef arg, args*)
364 | {
365 | ; When casting to Call(), use a new instance of the "function object"
366 | ; so as to avoid directly storing the properties(used across sub-methods)
367 | ; into the "function object" itself.
368 | if IsObject(method)
369 | return (new this).Call(method, arg, args*)
370 | else if (method == "")
371 | return (new this).Call(arg, args*)
372 | }
373 | }
374 | }
--------------------------------------------------------------------------------