├── .gitignore ├── _build_msvc19.bat ├── _build_msvc8.bat ├── license ├── readme.md ├── show.c ├── textbox.h └── textbox.rc /.gitignore: -------------------------------------------------------------------------------- 1 | *.obj 2 | *.exe 3 | *.res 4 | *.o 5 | *.pdb 6 | sign.bat 7 | -------------------------------------------------------------------------------- /_build_msvc19.bat: -------------------------------------------------------------------------------- 1 | @echo off 2 | call "C:\Program Files (x86)\Microsoft Visual Studio\2019\Community\VC\Auxiliary\Build\vcvars32.bat">nul 3 | rc /nologo textbox.rc 4 | cl /nologo /GS- /GF /Os /O1 /DNDEBUG /DTXT_NOBEEPS /Tp show.c textbox.res advapi32.lib comdlg32.lib kernel32.lib user32.lib /link /nodefaultlib /entry:main 5 | pause 6 | -------------------------------------------------------------------------------- /_build_msvc8.bat: -------------------------------------------------------------------------------- 1 | @echo off 2 | call "C:\Program Files (x86)\Microsoft Visual Studio 9.0\VC\vcvarsall.bat" 3 | rc textbox.rc 4 | cl /nologo /GS- /GF /Os /O1 /DNDEBUG /DTXT_NOBEEPS /Tp show.c textbox.res advapi32.lib comdlg32.lib kernel32.lib user32.lib /link /nodefaultlib /entry:main 5 | pause 6 | -------------------------------------------------------------------------------- /license: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Sam Tupy 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 | # pipe2textbox 2 | [download binary](https://github.com/samtupy/pipe2textbox/releases/latest/download/show.exe) 3 | 4 | Now it's command|show, no more command|clip / run > notepad / paste! 5 | 6 | ## What is this thing? 7 | If you are an NVDA screen reader user who also employs the command line, no doubt you will have run into the infuriating issue where only the very tail portion of a command's output is easily accessible to you. How many times have you had to type "command --help > help.txt" or "command --help | clip" before pasting into notepad, all because NVDA can't scroll up in the CLI output history? You can use control+A then control+c in windows cmd, but you still must sacrifice or back up your clipboard contents for this not to mention opening notepad to review the data. Seriously if you wanted to read more than the last 30ish lines of terminal output, until now it always somehow had to involve opening notepad! 8 | 9 | While this little program I've made by no means improves NVDA's scrolling functionality itself, it does attempt to make the entire situation just a little less painful by allowing you to pipe the output of any command into a simple multi-line read only text box for easy review. This means that as an NVDA user, you can now review the long output of a process without needing to sacrifice your clipboard's contents, without having to create some temporary file on disk, without having to launch notepad or generally just without any extra 5 seconds of hastle where sighted people spend 0! It even includes a find dialog (ctrl+f, f3, shift+f3) which means that you don't even need to copy the text to notepad to search for a specific phrase! And if you decide you want to preserve a command's output, just press ctrl+s on the text field and you'll be covered! 10 | 11 | ## Usage 12 | It really couldn't be more simple, just drop show.exe somewhere on your path (anywhere from c:\windows to c:\users\%username% so long as it's part of the path environment variable), then in the command line, you can test by running "dir c:\windows\system32|show" and see how you can instantly review every bit of that massive directory listing where as before you could only view the last 30 lines of it without extra hastle! Of course you can pipe anything to show.exe that you could pipe to the clip command. Easily reviewing git logs in a seekable manner is another great use for this, for example. 13 | 14 | The resulting dialog is very simple, containing a read only edit box with command output, a find button, and a save as button. You can also press ctrl+f or f3 in the text field to open the standard find dialog. As with most applications, f3 and shift+f3 remember the last search term so that you can jump between occurances of text easily. The finding here doesn't wrap around yet, I'll likely get around to that soon. The save as feature can also be accessed by pressing control+s in the text field. The aplication always saves files encoded in UTF8. 15 | 16 | You can exit the application by pressing either escape or alt+f4. You can also press ctrl+c in the terminal window you launched the application from by piping another process to it. 17 | 18 | It's not any more complicated than that! 19 | 20 | ## Limitations 21 | The main thing to warn about here is that for the first iteration of this program, the same behavior of the clip command is used (E. the text from a piping process queues up, then the text box displays the text once the piping process exits). This means that this isn't yet suitable for reviewing live output from long running CLI programs (say ssh or python). In the future, I'll make a new version of this thing that reads the piped text on a separate thread and continually updates the text field as the piping process outputs more text. This however requires several other considerations (such as screen reader speech to announce live output), so I've forgone it for the moment in light of at least releasing an initial version of this thing as it's already quite useful as is. 22 | 23 | I've done my very best under the circumstances to support UTF16, UTF8, system codepage and windows-1252 files for reading if they are piped in to the program. I do not know if this will be sufficient in all cases, please let me know if you find a plain text file that easily opens in your text editor but can't be successfully piped to the program. 24 | 25 | ## Building 26 | If you wish to build this yourself, you can run _build_msvc8.bat or _build_msvc19.bat if you have those versions of Visual Studio installed. If you have a different version of Visual Studio, you will need to either remove or alter the "call *" line at the top of the batch script to point to your local version of vcvarsXX.bat. I am fully aware that this building situation probably isn't convenient for anyone but me, and I apologize for that. First my c/c++ experience does not yet include build systems, it will at some point. Second, due to a side-goal of making at least the first version of this project as small as possible, I rely on Visual Studio's ability to uncomplainingly run with no c runtime library which seems to be far easier to get working than with other compilers (I spent a couple hours trying with mixed success). As my programming knowledge continues to evolve, you can rest assured these primative building steps for this and any other projects of mine will become much more mature and will conform to accepted standards. 27 | 28 | The one other thing to mention regarding building is this program's one quite rediculis conditional define. Basically, I'm using a windows rich edit control to show the piped text. This is good because it means control+a to select all works on new versions of windows without shipping a .manifest with the app, there is some support for finding text built in to this control, just generally it's a much more full featured edit box. The one annoyance however is the error beeps when you attempt to arrow past the beginning or end of the text field (you can see the same in wordpad). In one sense they make the text field slightly more accessible (you can tell instantly if you're at the border of the field), but new versions of windows in particular play a sound that's like 2 seconds long, which is a very minor annoyance (at least for me). This caused me to look up how to disable this feature of rich edit controls, and to my horror I found out that the only way to acomplish this was via a COM call! Being frank, I wasn't ready to learn the nastyness of making COM calls in pure c for something like this, so instead made the minorly painful decision to pollute my c code with a little bit of c++ to make the comm call, then make it a conditional define so that the program can still compile in pure c if wanted. Though I'll probably do it sometime myself at some point, if someone wishes to submit a pull request with a pure C version of the disable_richedit_beeps function, I'd be much abliged! Just remember that if the TXT_NOBEEPS macro is defined, you must then make sure your compiler is set to compile as C++. 29 | 30 | ## So much code for such a simple program, why? 31 | Trust me, I'm well aware that I have written this program at a rediculisly low level, it could probably be spun up in 40 lines of python and not much more c# or something. However, I was already interested in seeing how small of an executable Visual Studio could generate without a c runtime library, and my idea was to create a simple program. Indeed, the initial version of this thing was less than 90 lines of code, contained 3 functions, and could have possibly been made even smaller. But then I decided to add a find dialog which added another 50 odd lines, then decided to throw in a save feature as well so there's another 50ish, then I decided right before publishing the project that I wanted to support more text file encodings if someone pipes a file to the program, that was another chunk of more than 50 lines of code. In the end, I ended up learning a lot more than I set out to during this development process, however have happily reached my side goal utterly as compiling with vs2008 produces an executable that's only 6.5kb! The ssl signing on the official release I provide shoots it up to 12k instead. All things considered, in the end I'm not unhappy. It took literally days to develop this, there were some painful moments where I was clearly reminded how much easier this would have been had I done this at a higher level, but certainly the fun and challenge made it well worth it for me. If non-c-programmers have ideas for this thing and want to contribute, I may indeed rewrite it at a higher level. For now though, I hope you all get as much use out of this little program as I will get from the knowledge I've collected while making it! 32 | -------------------------------------------------------------------------------- /show.c: -------------------------------------------------------------------------------- 1 | /* 2 | A purely WINAPI based program which queues any text piped to it for display in a multi-line text box so that it can be easily reviewed with a screen reader. 3 | This was created as a workaround for the inability to scroll up in a terminal window using NVDA. Instead of running "command --help|clip" and then pasting that result into notepad for review, this purposefully tiny application does that in one step. 4 | It's worth noting that partially for extra challenge and partially to make the program as utterly small as possible, we do not link with a crt here (This is PURE win32 API)! 5 | 6 | Copyright (c) 2023 Sam Tupy, under the MIT license (see the file license). 7 | */ 8 | 9 | #define UNICODE 10 | 11 | #include 12 | #include 13 | #include 14 | #ifdef TXT_NOBEEPS 15 | #include 16 | #endif 17 | #include "textbox.h" 18 | 19 | // forward declarations 20 | BOOL CALLBACK textbox_callback(HWND hwnd, UINT message, WPARAM wp, LPARAM lp); 21 | LRESULT CALLBACK edit_control_callback(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam); 22 | #ifdef TXT_NOBEEPS 23 | void disable_richedit_beeps(HMODULE richedit_module, HWND richedit_control); 24 | #endif 25 | void find(HWND hwnd, int dir); 26 | void save(HWND hwnd); 27 | // Globals required for the find dialog. 28 | WNDPROC original_edit_control_callback = NULL; // We need to subclass the textbox since the main dialog isn't receiving WM_KEYDOWN for some reason. 29 | wchar_t text_to_search[256]; 30 | HWND find_dlg = NULL; 31 | DWORD find_dlg_flags = 0; // If not global, f3 and shift+f3 won't work correctly regarding case sensativity and whole word searches. 32 | UINT M_FINDMSGSTRING; 33 | 34 | int main() { 35 | // This was originally c code and may be again if I get borde enough to convert the *1* COM call I need to make into pure c. 36 | HANDLE cin, cout, process_heap; 37 | HWND dlg, output_box; 38 | HMODULE richedit_module; 39 | MSG msg; 40 | size_t cursor, allocated, text_transcode_result; 41 | DWORD console_type, console_bytes_read, text_codepage; 42 | wchar_t* output, * output_adjusted; 43 | char output_tmp[2052]; // Need 4 extra bytes for proper reading of UTF8 data. 44 | cin = GetStdHandle(STD_INPUT_HANDLE); 45 | if((console_type = GetFileType(cin)) == FILE_TYPE_CHAR) { // We don't want to wait for user input when reading stdin if there is no pipe. 46 | const char* message = "This tool displays any text piped to it in a multi-line input box for screen reader accessibility. You should either run \"command | show\" or \"show < filename\".\r\n"; 47 | cout = GetStdHandle(STD_OUTPUT_HANDLE); 48 | if(cout != INVALID_HANDLE_VALUE) 49 | WriteFile(cout, message, lstrlenA(message), NULL, NULL); 50 | return 0; 51 | } else if(console_type == FILE_TYPE_PIPE) text_codepage = GetConsoleOutputCP(); 52 | else text_codepage = 0; 53 | // Allocate a buffer to store characters from stdin 54 | allocated = 4096; cursor = 0; 55 | process_heap = GetProcessHeap(); 56 | output = (wchar_t*)HeapAlloc(process_heap, 0, allocated); 57 | // Read until we reach the end of the stdin stream. 58 | while(ReadFile(cin, output_tmp, 2048, &console_bytes_read, NULL) && console_bytes_read) { 59 | // Reallocate the output buffer if needed. 60 | if((cursor + console_bytes_read) * sizeof(wchar_t) >= allocated) { 61 | allocated *= 2; 62 | output = (wchar_t*)HeapReAlloc(process_heap, 0, output, allocated); 63 | } 64 | if(!text_codepage) { // certainly a file then 65 | text_codepage = IsTextUnicode(output_tmp, console_bytes_read, NULL); 66 | if(!text_codepage) text_codepage = CP_UTF8; 67 | } 68 | if(text_codepage == 1) { // Probably unicode (only check that in first text block) 69 | StringCchCopyW(output + cursor, (console_bytes_read / 2) + 1, (wchar_t*)&output_tmp); 70 | cursor += console_bytes_read / 2; 71 | continue; 72 | } 73 | // If we're reading UTF8 data, we can't just blindly transcode after reading 2048 bytes exactly. Some UTF8 magic numbers here and some code I'm not very proud of (spent hours getting it right erm I mean finding just a few small mistakes), sorry. Probably I should try making some part of this it's own function later, too tired now. Even if text isn't actually UTF8, only a couple of extra bytes should be read in the worst case. 74 | if(text_codepage == CP_UTF8 &&console_bytes_read < 2052 && (output_tmp[console_bytes_read -1] & (1 << 7)) != 0) { 75 | DWORD character_size, bytes_read_tmp, i; 76 | character_size = 0; 77 | for(i = 1; i <= 4; i ++) { // One of these 4 bytes will tell us the size of the character we're dealing with. 78 | unsigned char c; 79 | c = output_tmp[console_bytes_read - i]; 80 | if(c < 192) continue; // another continuation char. 81 | for(character_size = 2; character_size <=4; character_size ++) { 82 | if((c & (1 << (7 - character_size))) == 0) break; 83 | } 84 | if(character_size) break; 85 | } 86 | if(character_size - i > 0) { 87 | ReadFile(cin, output_tmp + console_bytes_read, character_size - i, &bytes_read_tmp, NULL); 88 | console_bytes_read += bytes_read_tmp; 89 | } 90 | } // fwiw, that UTF8 correction thoroughly sucked. 91 | text_transcode_result = MultiByteToWideChar(text_codepage != -1? text_codepage : 1252, text_codepage != -1? MB_ERR_INVALID_CHARS : 0, output_tmp, console_bytes_read, output + cursor, console_bytes_read); // Codepage can end up being unquestioningly windows1252 if transcoding attempts below fail, but we don't want to re-check encodings every text block. 92 | if(!text_transcode_result) { 93 | text_codepage = CP_ACP; 94 | text_transcode_result = MultiByteToWideChar(text_codepage, MB_ERR_INVALID_CHARS, output_tmp, console_bytes_read, output + cursor, console_bytes_read); 95 | } if(!text_transcode_result) { // We are truly desperate... 96 | text_codepage = -1; // Gotta choose something... in this case windows-1252 as seen above and below. 97 | text_transcode_result = MultiByteToWideChar(1252, 0, output_tmp, console_bytes_read, output + cursor, console_bytes_read); 98 | } if(!text_transcode_result) { // and now we are truly done for. 99 | MessageBox(0, L"Failed to decode this input", L"Error", MB_ICONERROR); 100 | HeapFree(process_heap, 0, output); 101 | ExitProcess(1); 102 | } 103 | cursor += text_transcode_result; 104 | } 105 | if(!cursor) { 106 | HeapFree(process_heap, 0, output); 107 | return 0; // No STDOutput, best to bail out encase stderr has anything to say. 108 | } 109 | output[cursor] = '\0'; // Making sure the output string is NULL terminated. 110 | output_adjusted = output; // As you can see below we may need to skip characters at the beginning of text for some reason, must retain the output pointer though so it can be freed. 111 | // Check for and remove a byte order mark in the case of a UTF16 file being piped. 112 | if(output[0] == 0xfeff) output_adjusted += 1; 113 | // Prepare and show the dialog. 114 | richedit_module = LoadLibrary(L"MSFTEDIT.dll"); // This will register the MSFTEDIT_CLASS. 115 | if(!richedit_module) { 116 | HeapFree(process_heap, 0, output); 117 | return 1; 118 | } 119 | dlg = CreateDialog(NULL, MAKEINTRESOURCE(textbox), 0, (DLGPROC)textbox_callback); 120 | if(!dlg) { 121 | HeapFree(process_heap, 0, output); 122 | return 1; 123 | } 124 | output_box = GetDlgItem(dlg, IDC_TEXT); 125 | #ifdef TXT_NOBEEPS 126 | disable_richedit_beeps(richedit_module, output_box); 127 | #endif 128 | SendMessage(output_box, EM_SETLIMITTEXT, 0, 0); 129 | SetWindowText(output_box, output_adjusted); 130 | SendMessage(output_box, EM_SETSEL, 0, 0); 131 | original_edit_control_callback = (WNDPROC)SetWindowLongPtr(output_box, GWLP_WNDPROC, (LONG_PTR)edit_control_callback); 132 | HeapFree(process_heap, 0, output); 133 | // A couple tiny things for the find text dialog. 134 | RtlSecureZeroMemory(&text_to_search, sizeof(text_to_search)); 135 | M_FINDMSGSTRING = RegisterWindowMessage(FINDMSGSTRING); 136 | // Finally, handle events. 137 | while(GetMessage(&msg, 0, 0, 0)) { 138 | if(find_dlg && IsDialogMessage(find_dlg, &msg)) continue; 139 | else if(IsDialogMessage(dlg, &msg)) continue; 140 | else { 141 | TranslateMessage(&msg); 142 | DispatchMessage(&msg); 143 | } 144 | } 145 | ExitProcess(msg.wParam); // We're using /nodefaultlib in the linker as we don't need the crt for this application, forcing us to call ExitProcess manually to prevent an app hang. 146 | return msg.wParam; // LOL if this line of code executes then ExitProcess somehow failed, fancy that. 147 | } 148 | 149 | BOOL CALLBACK textbox_callback(HWND hwnd, UINT message, WPARAM wp, LPARAM lp) { 150 | int control, event; 151 | switch(message) { 152 | case WM_COMMAND: { 153 | control = LOWORD(wp); 154 | event = HIWORD(wp); 155 | if(control == IDCANCEL && event == BN_CLICKED) 156 | DestroyWindow(hwnd); 157 | else if(control == IDC_FIND && event == BN_CLICKED) 158 | find(GetDlgItem(hwnd, IDC_TEXT), 0); 159 | else if(control == IDC_SAVE && event == BN_CLICKED) 160 | save(GetDlgItem(hwnd, IDC_TEXT)); 161 | return TRUE; 162 | } 163 | case WM_DESTROY: { 164 | PostQuitMessage(0); 165 | return TRUE; 166 | } 167 | } 168 | return FALSE; 169 | } 170 | 171 | #ifdef TXT_NOBEEPS 172 | // This function makes richedit controls stop making a sound if you try to scroll past their borders, warning is in c++! Thanks to https://stackoverflow.com/questions/55884687/how-to-eliminate-the-messagebeep-from-the-richedit-control 173 | void disable_richedit_beeps(HMODULE richedit_module, HWND richedit_control) { 174 | IUnknown* unknown; 175 | ITextServices* ts; 176 | IID* ITextservicesId = (IID*)GetProcAddress(richedit_module, "IID_ITextServices"); 177 | if(!ITextservicesId) return; 178 | if(!SendMessage(richedit_control, EM_GETOLEINTERFACE, 0, (LPARAM)&unknown)) return; 179 | HRESULT hr = unknown->QueryInterface(*ITextservicesId, (void**)&ts); 180 | unknown->Release(); 181 | if(hr) return; 182 | ts->OnTxPropertyBitsChange(TXTBIT_ALLOWBEEP, 0); 183 | ts->Release(); 184 | } 185 | #endif 186 | 187 | // This implements the find dialog. 188 | void find(HWND hwnd, int dir) { 189 | if(dir == 0 || !text_to_search[0]) { 190 | static FINDREPLACE fr; // If this is not global or static the program will crash as soon as the find dialog tries sending it's first message. 191 | RtlSecureZeroMemory(&fr, sizeof(fr)); 192 | fr.lStructSize = sizeof(fr); 193 | fr.hwndOwner = hwnd; 194 | fr.lpstrFindWhat = text_to_search; 195 | fr.wFindWhatLen = 256; 196 | fr.Flags = (dir >= 0? FR_DOWN : 0) | (find_dlg_flags & FR_MATCHCASE? FR_MATCHCASE : 0) | (find_dlg_flags & FR_WHOLEWORD ? FR_WHOLEWORD : 0); // Maybe this is clunky? I'm on the fense as to whether fr.Flags = find_dlg_flags then manually resetting the FR_DOWN flag as needed would be less so. 197 | find_dlg = FindText(&fr); 198 | } else { 199 | FINDTEXTEXW ft; 200 | RtlSecureZeroMemory(&ft, sizeof(ft)); 201 | SendMessage(hwnd, EM_EXGETSEL, 0, (LPARAM)&ft.chrg); 202 | if(ft.chrg.cpMin > 0) ft.chrg.cpMin += dir; // We want to start searching at the next or previous cursor position, not the current one. 203 | ft.chrg.cpMax = -1; 204 | ft.lpstrText = text_to_search; 205 | SendMessage(hwnd, EM_FINDTEXTEX, (dir > 0? FR_DOWN : 0) | (find_dlg_flags & FR_MATCHCASE? FR_MATCHCASE : 0) | (find_dlg_flags & FR_WHOLEWORD ? FR_WHOLEWORD : 0), (LPARAM)&ft); 206 | if(ft.chrgText.cpMin >= 0) { 207 | SendMessage(hwnd, EM_EXSETSEL, 0, (LPARAM)&ft.chrgText); 208 | SetFocus(hwnd); // Encase find dialog was activated through button instead of shortcut. 209 | SendMessage(find_dlg, WM_CLOSE, 0, 0); 210 | } else MessageBox((find_dlg? find_dlg : hwnd), L"nothing found for the given search", L"error", MB_ICONERROR); 211 | } 212 | } 213 | 214 | // we don't want the user to have to copy the text they're seeing to notepad just because they want to preserve it, that defeats some of the point of this thing! 215 | // Callback that writes data from a text field to a file. 216 | DWORD CALLBACK save_editstream_callback(DWORD_PTR cookie,LPBYTE buffer, LONG bufsize, LONG* bytes_written) { 217 | DWORD dw_bytes_written; 218 | HANDLE* file=(HANDLE*)cookie; 219 | if(WriteFile(*file, buffer, bufsize, &dw_bytes_written, NULL)) { 220 | *bytes_written = dw_bytes_written; 221 | return 0; 222 | } 223 | else return 1; 224 | } 225 | void save(HWND hwnd) { 226 | EDITSTREAM editstream; 227 | wchar_t save_path[MAX_PATH]; 228 | HANDLE save_file; 229 | OPENFILENAME ofn; 230 | StringCchCopyW(save_path, MAX_PATH, L"output.txt"); 231 | RtlSecureZeroMemory(&ofn, sizeof(ofn)); 232 | ofn.lStructSize = sizeof(ofn); 233 | ofn.hwndOwner = hwnd; 234 | ofn.lpstrFile = save_path; 235 | ofn.nMaxFile = MAX_PATH; 236 | ofn.lpstrFilter = L"TXT files (*.txt)\0*.txt\0All files\0*.*\0\0"; 237 | ofn.nFilterIndex = 1; 238 | ofn.lpstrInitialDir = L""; 239 | ofn.Flags = OFN_OVERWRITEPROMPT; 240 | if(!GetSaveFileName(&ofn)) return; 241 | save_file = CreateFile(save_path, GENERIC_WRITE, FILE_SHARE_READ, NULL, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL); 242 | if(!save_file) return; 243 | RtlSecureZeroMemory(&editstream, sizeof(editstream)); 244 | editstream.dwCookie = (DWORD_PTR)&save_file; 245 | editstream.pfnCallback = save_editstream_callback; 246 | SendMessage(hwnd, EM_STREAMOUT, (CP_UTF8 << 16) | SF_USECODEPAGE | SF_TEXT, (LPARAM)&editstream); 247 | CloseHandle(save_file); 248 | if(editstream.dwError) MessageBox(hwnd, L"potential error saving", L"warning", 0); 249 | } 250 | 251 | LRESULT CALLBACK edit_control_callback(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam) { 252 | FINDREPLACE* fr; 253 | BOOL ret = TRUE; // No switch case here because M_FINDMSGSTRING is not a constant. 254 | if(msg == WM_KEYDOWN) { 255 | if(wParam == 'F' && GetAsyncKeyState(VK_CONTROL) & 0x8000) 256 | find(hwnd, 0); 257 | else if(wParam == 'S' && GetAsyncKeyState(VK_CONTROL) & 0x8000) 258 | save(hwnd); 259 | else if(wParam == VK_F3) 260 | find(hwnd, GetAsyncKeyState(VK_SHIFT) & 0x8000? -1 : 1); 261 | else ret = FALSE; 262 | if(ret) return ret; 263 | } else if(msg == M_FINDMSGSTRING) { 264 | fr = (FINDREPLACE*)lParam; 265 | if(fr->Flags & FR_DIALOGTERM) { 266 | find_dlg = NULL; 267 | return TRUE; 268 | } 269 | find_dlg_flags = fr->Flags; 270 | if(fr->Flags & FR_FINDNEXT) find(hwnd, fr->Flags & FR_DOWN? 1 : -1); 271 | return TRUE; 272 | } 273 | return CallWindowProc(original_edit_control_callback, hwnd, msg, wParam, lParam); 274 | } 275 | -------------------------------------------------------------------------------- /textbox.h: -------------------------------------------------------------------------------- 1 | #ifndef IDC_STATIC 2 | #define IDC_STATIC (-1) 3 | #endif 4 | #define textbox 111 5 | #define IDC_TEXT 1001 6 | #define IDL_TEXT 1002 7 | #define IDC_FIND 1003 8 | #define IDC_SAVE 1004 9 | -------------------------------------------------------------------------------- /textbox.rc: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include "textbox.h" 4 | LANGUAGE LANG_NEUTRAL, SUBLANG_NEUTRAL 5 | textbox DIALOG 0, 0, 300, 250 6 | STYLE DS_CENTER|DS_MODALFRAME|DS_SHELLFONT|WS_CAPTION|WS_VISIBLE|WS_MAXIMIZE|WS_POPUP 7 | CAPTION "output" 8 | FONT 10, "Arial" 9 | { 10 | CONTROL "", IDC_TEXT, MSFTEDIT_CLASS, ES_MULTILINE|ES_READONLY|ES_AUTOHSCROLL|ES_SAVESEL|ES_WANTRETURN|WS_VSCROLL|WS_TABSTOP, 45, 4, 200, 100 11 | PUSHBUTTON "Find... Ctrl+F", IDC_FIND, 1, 94, 40, 20 12 | PUSHBUTTON "Save as... Ctrl+S", IDC_SAVE, 250, 94, 40, 20 // I don't even wanna know how visually horendis this probably looks 13 | } 14 | --------------------------------------------------------------------------------