├── 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 |

ChatKey Logo

2 | 3 |

4 | 5 | License: MIT 6 | 7 | 8 | Latest Release 9 | 10 | 11 | Total Downloads 12 | 13 | 14 | Powered By: AutoHokey 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 | Screenshot 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 | [![ChatGPT & Zapier: The Future of AI Automation, Maybe](http://img.youtube.com/vi/4mraUhvVrOc/0.jpg)](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 | } --------------------------------------------------------------------------------