-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathshow.c
274 lines (264 loc) · 12.8 KB
/
show.c
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
/*
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.
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.
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)!
Copyright (c) 2023 Sam Tupy, under the MIT license (see the file license).
*/
#define UNICODE
#include <windows.h>
#include <richedit.h>
#include <strsafe.h>
#ifdef TXT_NOBEEPS
#include <textserv.h>
#endif
#include "textbox.h"
// forward declarations
BOOL CALLBACK textbox_callback(HWND hwnd, UINT message, WPARAM wp, LPARAM lp);
LRESULT CALLBACK edit_control_callback(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam);
#ifdef TXT_NOBEEPS
void disable_richedit_beeps(HMODULE richedit_module, HWND richedit_control);
#endif
void find(HWND hwnd, int dir);
void save(HWND hwnd);
// Globals required for the find dialog.
WNDPROC original_edit_control_callback = NULL; // We need to subclass the textbox since the main dialog isn't receiving WM_KEYDOWN for some reason.
wchar_t text_to_search[256];
HWND find_dlg = NULL;
DWORD find_dlg_flags = 0; // If not global, f3 and shift+f3 won't work correctly regarding case sensativity and whole word searches.
UINT M_FINDMSGSTRING;
int main() {
// 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.
HANDLE cin, cout, process_heap;
HWND dlg, output_box;
HMODULE richedit_module;
MSG msg;
size_t cursor, allocated, text_transcode_result;
DWORD console_type, console_bytes_read, text_codepage;
wchar_t* output, * output_adjusted;
char output_tmp[2052]; // Need 4 extra bytes for proper reading of UTF8 data.
cin = GetStdHandle(STD_INPUT_HANDLE);
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.
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";
cout = GetStdHandle(STD_OUTPUT_HANDLE);
if(cout != INVALID_HANDLE_VALUE)
WriteFile(cout, message, lstrlenA(message), NULL, NULL);
return 0;
} else if(console_type == FILE_TYPE_PIPE) text_codepage = GetConsoleOutputCP();
else text_codepage = 0;
// Allocate a buffer to store characters from stdin
allocated = 4096; cursor = 0;
process_heap = GetProcessHeap();
output = (wchar_t*)HeapAlloc(process_heap, 0, allocated);
// Read until we reach the end of the stdin stream.
while(ReadFile(cin, output_tmp, 2048, &console_bytes_read, NULL) && console_bytes_read) {
// Reallocate the output buffer if needed.
if((cursor + console_bytes_read) * sizeof(wchar_t) >= allocated) {
allocated *= 2;
output = (wchar_t*)HeapReAlloc(process_heap, 0, output, allocated);
}
if(!text_codepage) { // certainly a file then
text_codepage = IsTextUnicode(output_tmp, console_bytes_read, NULL);
if(!text_codepage) text_codepage = CP_UTF8;
}
if(text_codepage == 1) { // Probably unicode (only check that in first text block)
StringCchCopyW(output + cursor, (console_bytes_read / 2) + 1, (wchar_t*)&output_tmp);
cursor += console_bytes_read / 2;
continue;
}
// 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.
if(text_codepage == CP_UTF8 &&console_bytes_read < 2052 && (output_tmp[console_bytes_read -1] & (1 << 7)) != 0) {
DWORD character_size, bytes_read_tmp, i;
character_size = 0;
for(i = 1; i <= 4; i ++) { // One of these 4 bytes will tell us the size of the character we're dealing with.
unsigned char c;
c = output_tmp[console_bytes_read - i];
if(c < 192) continue; // another continuation char.
for(character_size = 2; character_size <=4; character_size ++) {
if((c & (1 << (7 - character_size))) == 0) break;
}
if(character_size) break;
}
if(character_size - i > 0) {
ReadFile(cin, output_tmp + console_bytes_read, character_size - i, &bytes_read_tmp, NULL);
console_bytes_read += bytes_read_tmp;
}
} // fwiw, that UTF8 correction thoroughly sucked.
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.
if(!text_transcode_result) {
text_codepage = CP_ACP;
text_transcode_result = MultiByteToWideChar(text_codepage, MB_ERR_INVALID_CHARS, output_tmp, console_bytes_read, output + cursor, console_bytes_read);
} if(!text_transcode_result) { // We are truly desperate...
text_codepage = -1; // Gotta choose something... in this case windows-1252 as seen above and below.
text_transcode_result = MultiByteToWideChar(1252, 0, output_tmp, console_bytes_read, output + cursor, console_bytes_read);
} if(!text_transcode_result) { // and now we are truly done for.
MessageBox(0, L"Failed to decode this input", L"Error", MB_ICONERROR);
HeapFree(process_heap, 0, output);
ExitProcess(1);
}
cursor += text_transcode_result;
}
if(!cursor) {
HeapFree(process_heap, 0, output);
return 0; // No STDOutput, best to bail out encase stderr has anything to say.
}
output[cursor] = '\0'; // Making sure the output string is NULL terminated.
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.
// Check for and remove a byte order mark in the case of a UTF16 file being piped.
if(output[0] == 0xfeff) output_adjusted += 1;
// Prepare and show the dialog.
richedit_module = LoadLibrary(L"MSFTEDIT.dll"); // This will register the MSFTEDIT_CLASS.
if(!richedit_module) {
HeapFree(process_heap, 0, output);
return 1;
}
dlg = CreateDialog(NULL, MAKEINTRESOURCE(textbox), 0, (DLGPROC)textbox_callback);
if(!dlg) {
HeapFree(process_heap, 0, output);
return 1;
}
output_box = GetDlgItem(dlg, IDC_TEXT);
#ifdef TXT_NOBEEPS
disable_richedit_beeps(richedit_module, output_box);
#endif
SendMessage(output_box, EM_SETLIMITTEXT, 0, 0);
SetWindowText(output_box, output_adjusted);
SendMessage(output_box, EM_SETSEL, 0, 0);
original_edit_control_callback = (WNDPROC)SetWindowLongPtr(output_box, GWLP_WNDPROC, (LONG_PTR)edit_control_callback);
HeapFree(process_heap, 0, output);
// A couple tiny things for the find text dialog.
RtlSecureZeroMemory(&text_to_search, sizeof(text_to_search));
M_FINDMSGSTRING = RegisterWindowMessage(FINDMSGSTRING);
// Finally, handle events.
while(GetMessage(&msg, 0, 0, 0)) {
if(find_dlg && IsDialogMessage(find_dlg, &msg)) continue;
else if(IsDialogMessage(dlg, &msg)) continue;
else {
TranslateMessage(&msg);
DispatchMessage(&msg);
}
}
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.
return msg.wParam; // LOL if this line of code executes then ExitProcess somehow failed, fancy that.
}
BOOL CALLBACK textbox_callback(HWND hwnd, UINT message, WPARAM wp, LPARAM lp) {
int control, event;
switch(message) {
case WM_COMMAND: {
control = LOWORD(wp);
event = HIWORD(wp);
if(control == IDCANCEL && event == BN_CLICKED)
DestroyWindow(hwnd);
else if(control == IDC_FIND && event == BN_CLICKED)
find(GetDlgItem(hwnd, IDC_TEXT), 0);
else if(control == IDC_SAVE && event == BN_CLICKED)
save(GetDlgItem(hwnd, IDC_TEXT));
return TRUE;
}
case WM_DESTROY: {
PostQuitMessage(0);
return TRUE;
}
}
return FALSE;
}
#ifdef TXT_NOBEEPS
// 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
void disable_richedit_beeps(HMODULE richedit_module, HWND richedit_control) {
IUnknown* unknown;
ITextServices* ts;
IID* ITextservicesId = (IID*)GetProcAddress(richedit_module, "IID_ITextServices");
if(!ITextservicesId) return;
if(!SendMessage(richedit_control, EM_GETOLEINTERFACE, 0, (LPARAM)&unknown)) return;
HRESULT hr = unknown->QueryInterface(*ITextservicesId, (void**)&ts);
unknown->Release();
if(hr) return;
ts->OnTxPropertyBitsChange(TXTBIT_ALLOWBEEP, 0);
ts->Release();
}
#endif
// This implements the find dialog.
void find(HWND hwnd, int dir) {
if(dir == 0 || !text_to_search[0]) {
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.
RtlSecureZeroMemory(&fr, sizeof(fr));
fr.lStructSize = sizeof(fr);
fr.hwndOwner = hwnd;
fr.lpstrFindWhat = text_to_search;
fr.wFindWhatLen = 256;
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.
find_dlg = FindText(&fr);
} else {
FINDTEXTEXW ft;
RtlSecureZeroMemory(&ft, sizeof(ft));
SendMessage(hwnd, EM_EXGETSEL, 0, (LPARAM)&ft.chrg);
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.
ft.chrg.cpMax = -1;
ft.lpstrText = text_to_search;
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);
if(ft.chrgText.cpMin >= 0) {
SendMessage(hwnd, EM_EXSETSEL, 0, (LPARAM)&ft.chrgText);
SetFocus(hwnd); // Encase find dialog was activated through button instead of shortcut.
SendMessage(find_dlg, WM_CLOSE, 0, 0);
} else MessageBox((find_dlg? find_dlg : hwnd), L"nothing found for the given search", L"error", MB_ICONERROR);
}
}
// 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!
// Callback that writes data from a text field to a file.
DWORD CALLBACK save_editstream_callback(DWORD_PTR cookie,LPBYTE buffer, LONG bufsize, LONG* bytes_written) {
DWORD dw_bytes_written;
HANDLE* file=(HANDLE*)cookie;
if(WriteFile(*file, buffer, bufsize, &dw_bytes_written, NULL)) {
*bytes_written = dw_bytes_written;
return 0;
}
else return 1;
}
void save(HWND hwnd) {
EDITSTREAM editstream;
wchar_t save_path[MAX_PATH];
HANDLE save_file;
OPENFILENAME ofn;
StringCchCopyW(save_path, MAX_PATH, L"output.txt");
RtlSecureZeroMemory(&ofn, sizeof(ofn));
ofn.lStructSize = sizeof(ofn);
ofn.hwndOwner = hwnd;
ofn.lpstrFile = save_path;
ofn.nMaxFile = MAX_PATH;
ofn.lpstrFilter = L"TXT files (*.txt)\0*.txt\0All files\0*.*\0\0";
ofn.nFilterIndex = 1;
ofn.lpstrInitialDir = L"";
ofn.Flags = OFN_OVERWRITEPROMPT;
if(!GetSaveFileName(&ofn)) return;
save_file = CreateFile(save_path, GENERIC_WRITE, FILE_SHARE_READ, NULL, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL);
if(!save_file) return;
RtlSecureZeroMemory(&editstream, sizeof(editstream));
editstream.dwCookie = (DWORD_PTR)&save_file;
editstream.pfnCallback = save_editstream_callback;
SendMessage(hwnd, EM_STREAMOUT, (CP_UTF8 << 16) | SF_USECODEPAGE | SF_TEXT, (LPARAM)&editstream);
CloseHandle(save_file);
if(editstream.dwError) MessageBox(hwnd, L"potential error saving", L"warning", 0);
}
LRESULT CALLBACK edit_control_callback(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam) {
FINDREPLACE* fr;
BOOL ret = TRUE; // No switch case here because M_FINDMSGSTRING is not a constant.
if(msg == WM_KEYDOWN) {
if(wParam == 'F' && GetAsyncKeyState(VK_CONTROL) & 0x8000)
find(hwnd, 0);
else if(wParam == 'S' && GetAsyncKeyState(VK_CONTROL) & 0x8000)
save(hwnd);
else if(wParam == VK_F3)
find(hwnd, GetAsyncKeyState(VK_SHIFT) & 0x8000? -1 : 1);
else ret = FALSE;
if(ret) return ret;
} else if(msg == M_FINDMSGSTRING) {
fr = (FINDREPLACE*)lParam;
if(fr->Flags & FR_DIALOGTERM) {
find_dlg = NULL;
return TRUE;
}
find_dlg_flags = fr->Flags;
if(fr->Flags & FR_FINDNEXT) find(hwnd, fr->Flags & FR_DOWN? 1 : -1);
return TRUE;
}
return CallWindowProc(original_edit_control_callback, hwnd, msg, wParam, lParam);
}