Skip to content

Commit 79673b6

Browse files
committed
FEAT: Windows: Console Virtual Terminal Sequences
If available (since Win10) using ENABLE_VIRTUAL_TERMINAL_PROCESSING output mode to process ANSI Escape Sequences. On older Windows versions it keeps using the ANSI emulation like before. It's possible to disable use of ENABLE_VIRTUAL_TERMINAL_PROCESSING mode by defining FORCE_ANSI_ESC_EMULATION_ON_WINDOWS compiler definition. Related documentation: https://docs.microsoft.com/en-us/windows/console/console-virtual-terminal-sequences Known issues: The VIRTUAL_TERMINAL_PROCESSING mode is not handling escape codes for hiding input (^[[8m and ^[[28m), which is currently used in ASK/HIDE function. As these sequences are also not fully supported on some POSIX systems, the hiding input should be solved using other way. metaeducation/rebol-issues#476
1 parent 3c7e7a8 commit 79673b6

File tree

2 files changed

+147
-42
lines changed

2 files changed

+147
-42
lines changed

make/make-settings.r

+14
Original file line numberDiff line numberDiff line change
@@ -15,4 +15,18 @@ Defines: [
1515
;USE_WAV_CODEC ;-- deprecated; using Rebol codec instead
1616
;USE_NO_INFINITY ;-- use when you don't want to support IEEE infinity
1717
USE_LZMA ;-- adds support for LZMA [de]compression
18+
19+
;@@ optional fine tuning:
20+
;DO_NOT_NORMALIZE_MAP_KEYS
21+
; with above define you would get:
22+
; [a b:] = words-of make map! [a 1 b: 2]
23+
; [a 1 b: 2] = body-of make map! [a 1 b: 2]
24+
;
25+
; else:
26+
; [a b] = words-of make map! [a 1 b: 2]
27+
; [a: 1 b: 2] = body-of make map! [a 1 b: 2]
28+
29+
;USE_EMPTY_HASH_AS_NONE ;-- A single # means NONE, else error; Used in l-scan.c file
30+
;FORCE_ANSI_ESC_EMULATION_ON_WINDOWS ;-- would not try to use MS' built-in VIRTUAL_TERMINAL_PROCESSING
31+
1832
]

src/os/win32/dev-stdio.c

+133-42
Original file line numberDiff line numberDiff line change
@@ -52,8 +52,23 @@
5252

5353
#define SF_DEV_NULL 31 // local flag to mark NULL device
5454

55+
// some modes may not be defined
56+
#ifndef ENABLE_VIRTUAL_TERMINAL_PROCESSING
57+
#define ENABLE_VIRTUAL_TERMINAL_PROCESSING 0x0004
58+
#endif
59+
#ifndef ENABLE_VIRTUAL_TERMINAL_INPUT
60+
#define ENABLE_VIRTUAL_TERMINAL_INPUT 0x0200
61+
#endif
62+
#ifndef ENABLE_INSERT_MODE
63+
#define ENABLE_INSERT_MODE 0x0020
64+
#endif
65+
#ifndef ENABLE_QUICK_EDIT_MODE
66+
#define ENABLE_QUICK_EDIT_MODE 0x0040
67+
#endif
68+
69+
5570
#define CONSOLE_MODES ENABLE_LINE_INPUT | ENABLE_PROCESSED_INPUT | ENABLE_ECHO_INPUT \
56-
| ENABLE_EXTENDED_FLAGS | 0x0040 | 0x0020 // quick edit and insert mode (not defined in VC6)
71+
| ENABLE_EXTENDED_FLAGS | ENABLE_QUICK_EDIT_MODE | ENABLE_INSERT_MODE
5772

5873
static HANDLE Std_Out = 0;
5974
static HANDLE Std_Inp = 0;
@@ -63,6 +78,25 @@ static REBCHR *Std_Buf = 0; // for input and output
6378
static BOOL Redir_Out = 0;
6479
static BOOL Redir_Inp = 0;
6580

81+
// Since some Windows10 version it's possible to use the new terminal processing,
82+
// of ANSI escape sequences. From my tests its not faster than my emulation, but
83+
// may be used for functionalities which are not handled in the emulation.
84+
// If the terminal processing is not available, there is a fallback to emulation
85+
// so basic ANSI is supported also on older Windows.
86+
//
87+
// Known issues with the new MS terminal processing:
88+
// * it does not process sequences to switch output echo (^[[8m and ^[[28m)
89+
// currently used in ASK/HIDE
90+
//
91+
// Documentation: https://docs.microsoft.com/en-us/windows/console/console-virtual-terminal-sequences
92+
//
93+
// It's possible to force the emulation with following compilation definition:
94+
#ifdef FORCE_ANSI_ESC_EMULATION_ON_WINDOWS
95+
static BOOL Emulate_ANSI = 1; // forces backward compatible ANSI emulation (not using VIRTUAL_TERMINAL_PROCESSING)
96+
#else
97+
static BOOL Emulate_ANSI = 0;
98+
#endif
99+
66100
// Special access:
67101
extern REBDEV *Devices[];
68102

@@ -95,6 +129,10 @@ static int ANSI_Value1 = 0;
95129
static int ANSI_Value2 = 0;
96130
static int ANSI_Attr = -1;
97131

132+
DWORD dwOriginalOutMode = 0;
133+
DWORD dwOriginalInpMode = 0;
134+
WORD wOriginalAttributes = 0;
135+
98136
int Update_Graphic_Mode(int attribute, int value, boolean set);
99137
const REBYTE* Parse_ANSI_sequence(const REBYTE *cp, const REBYTE *ep);
100138

@@ -167,6 +205,13 @@ static void close_stdio(void)
167205
{
168206
REBDEV *dev = (REBDEV*)dr; // just to keep compiler happy above
169207

208+
// reset original modes on exit
209+
if (Std_Inp) SetConsoleMode(Std_Inp, dwOriginalInpMode);
210+
if (Std_Out) {
211+
SetConsoleMode(Std_Out, dwOriginalOutMode);
212+
SetConsoleTextAttribute(Std_Out, wOriginalAttributes);
213+
}
214+
170215
close_stdio();
171216
//if (GET_FLAG(dev->flags, RDF_OPEN)) FreeConsole();
172217
CLR_FLAG(dev->flags, RDF_OPEN);
@@ -239,6 +284,11 @@ static void close_stdio(void)
239284
//Std_Err = GetStdHandle(STD_ERROR_HANDLE);
240285
Std_Echo = 0;
241286

287+
// store original text attributes:
288+
CONSOLE_SCREEN_BUFFER_INFO csbiInfo;
289+
GetConsoleScreenBufferInfo(Std_Out, &csbiInfo);
290+
wOriginalAttributes = csbiInfo.wAttributes;
291+
242292
Redir_Out = (GetFileType(Std_Out) != FILE_TYPE_CHAR);
243293
Redir_Inp = (GetFileType(Std_Inp) != FILE_TYPE_CHAR);
244294

@@ -267,6 +317,10 @@ static void close_stdio(void)
267317
#endif
268318
Std_Buf = OS_Make(BUF_SIZE * sizeof(REBCHR));
269319

320+
// store original modes
321+
GetConsoleMode(Std_Out, &dwOriginalOutMode);
322+
GetConsoleMode(Std_Inp, &dwOriginalInpMode);
323+
270324
if (!Redir_Inp) {
271325
//
272326
// Windows offers its own "smart" line editor (with history
@@ -277,7 +331,35 @@ static void close_stdio(void)
277331
// While the line editor is running with ENABLE_LINE_INPUT, there
278332
// are very few hooks offered.
279333
//
280-
SetConsoleMode(Std_Inp, CONSOLE_MODES);
334+
DWORD dwInpMode = CONSOLE_MODES;
335+
DWORD dwOutMode = dwOriginalOutMode;
336+
337+
if(!SetConsoleMode(Std_Inp, dwInpMode)) {
338+
printf("Failed to set input ConsoleMode! Error: %i\n", GetLastError());
339+
return DR_ERROR;
340+
}
341+
342+
if(!Emulate_ANSI) {
343+
//dwInpMode |= ENABLE_VIRTUAL_TERMINAL_INPUT;
344+
dwOutMode |= ENABLE_VIRTUAL_TERMINAL_PROCESSING;
345+
if (SetConsoleMode(Std_Out, dwOutMode)) {
346+
//SetConsoleMode(Std_Inp, dwInpMode);
347+
} else {
348+
Emulate_ANSI = 1; // failed to use VIRTUAL_TERMINAL_PROCESSING, so force emulation
349+
}
350+
}
351+
352+
// Try some Set Graphics Rendition (SGR) terminal escape sequences
353+
/*
354+
wprintf(L"\x1b[31mThis text has a red foreground using SGR.31.\r\n");
355+
wprintf(L"\x1b[1mThis text has a bright (bold) red foreground using SGR.1 to affect the previous color setting.\r\n");
356+
wprintf(L"\x1b[mThis text has returned to default colors using SGR.0 implicitly.\r\n");
357+
wprintf(L"\x1b[34;46mThis text shows the foreground and background change at the same time.\r\n");
358+
wprintf(L"\x1b[0mThis text has returned to default colors using SGR.0 explicitly.\r\n");
359+
wprintf(L"\x1b[31;32;33;34;35;36;101;102;103;104;105;106;107mThis text attempts to apply many colors in the same command. Note the colors are applied from left to right so only the right-most option of foreground cyan (SGR.36) and background bright white (SGR.107) is effective.\r\n");
360+
wprintf(L"\x1b[39mThis text has restored the foreground color only.\r\n");
361+
wprintf(L"\x1b[49mThis text has restored the background color only.\r\n");
362+
*/
281363
}
282364

283365
// Handle stdio CTRL-C interrupt:
@@ -338,57 +420,66 @@ static void close_stdio(void)
338420
bp = req->data;
339421
ep = bp + req->length;
340422

341-
// Using this loop for seeking escape char and processing ANSI sequence
342-
do {
343-
344-
if(ANSI_State >= 0) // there is pending ansi sequence state
345-
bp = Parse_ANSI_sequence(bp-1, ep); // the pointer is incremented back in the parser
423+
if(Emulate_ANSI) {
424+
// Using this loop for seeking escape char and processing ANSI sequence
425+
do {
426+
if (ANSI_State >= 0) // there is pending ansi sequence state
427+
bp = Parse_ANSI_sequence(bp - 1, ep); // the pointer is incremented back in the parser
346428

347-
cp = Skip_To_Char(bp, ep, (REBYTE)27); //find ANSI escape char "^["
429+
cp = Skip_To_Char(bp, ep, (REBYTE)27); //find ANSI escape char "^["
348430

349-
if (Redir_Out) { // for Console SubSystem (always UTF-8)
350-
if (cp) {
351-
ok = WriteFile(Std_Out, bp, cp - bp, &total, 0);
352-
bp = Parse_ANSI_sequence(cp, ep);
353-
}
354-
else {
355-
ok = WriteFile(Std_Out, bp, ep - bp, &total, 0);
356-
bp = ep;
431+
if (Redir_Out) { // for Console SubSystem (always UTF-8)
432+
if (cp) {
433+
ok = WriteFile(Std_Out, bp, cp - bp, &total, 0);
434+
bp = Parse_ANSI_sequence(cp, ep);
435+
}
436+
else {
437+
ok = WriteFile(Std_Out, bp, ep - bp, &total, 0);
438+
bp = ep;
439+
}
440+
if (!ok) {
441+
req->error = GetLastError();
442+
return DR_ERROR;
443+
}
357444
}
358-
if (!ok) {
359-
req->error = GetLastError();
360-
return DR_ERROR;
445+
else { // for Windows SubSystem - must be converted to Win32 wide-char format
446+
//if found, write to the console content before it starts, else everything
447+
if (cp) {
448+
len = MultiByteToWideChar(CP_UTF8, 0, bp, cp - bp, Std_Buf, BUF_SIZE);
449+
}
450+
else {
451+
len = MultiByteToWideChar(CP_UTF8, 0, bp, ep - bp, Std_Buf, BUF_SIZE);
452+
bp = ep;
453+
}
454+
if (len > 0) {// no error
455+
ok = WriteConsoleW(Std_Out, Std_Buf, len, &total, 0);
456+
if (!ok) {
457+
req->error = GetLastError();
458+
return DR_ERROR;
459+
}
460+
}
461+
//is escape char was found, parse the ANSI sequence...
462+
if (cp) {
463+
bp = Parse_ANSI_sequence(cp, ep);
464+
}
361465
}
466+
} while (bp < ep);
467+
} else {
468+
// using MS built in ANSI processing
469+
if (Redir_Out) { // Always UTF-8
470+
ok = WriteFile(Std_Out, req->data, req->length, &total, 0);
362471
}
363-
else { // for Windows SubSystem - must be converted to Win32 wide-char format
364-
472+
else {
473+
// Convert UTF-8 buffer to Win32 wide-char format for console.
365474
// Thankfully, MS provides something other than mbstowcs();
366475
// however, if our buffer overflows, it's an error. There's no
367476
// efficient way at this level to split-up the input data,
368477
// because its UTF-8 with variable char sizes.
369-
370-
//if found, write to the console content before it starts, else everything
371-
if (cp) {
372-
len = MultiByteToWideChar(CP_UTF8, 0, bp, cp - bp, Std_Buf, BUF_SIZE);
373-
}
374-
else {
375-
len = MultiByteToWideChar(CP_UTF8, 0, bp, ep - bp, Std_Buf, BUF_SIZE);
376-
bp = ep;
377-
}
378-
if (len > 0) {// no error
478+
len = MultiByteToWideChar(CP_UTF8, 0, req->data, req->length, Std_Buf, BUF_SIZE);
479+
if (len > 0) // no error
379480
ok = WriteConsoleW(Std_Out, Std_Buf, len, &total, 0);
380-
if (!ok) {
381-
req->error = GetLastError();
382-
return DR_ERROR;
383-
}
384-
}
385-
//is escape char was found, parse the ANSI sequence...
386-
if (cp) {
387-
bp = Parse_ANSI_sequence(cp, ep);
388-
}
389481
}
390-
} while (bp < ep);
391-
482+
}
392483
req->actual = req->length; // do not use "total" (can be byte or wide)
393484

394485
//if (GET_FLAG(req->flags, RRF_FLUSH)) {

0 commit comments

Comments
 (0)