mirror of
https://github.com/mirkobrombin/raven.git
synced 2026-10-30 21:35:06 +00:00
* feat: give console programs the terminal as their console A standard handle that is a terminal, and CONIN$, CONOUT$ and CON, now act as a console: modes map to terminal settings, reads and writes convert between UTF-16 and the terminal's UTF-8, terminal input becomes key events, and Ctrl+C runs the control handlers. Files and pipes still fail the console functions, as on Windows. Assisted-by: Claude Code:claude-opus-5-5 AI-Scope: Wrote the console functions over the terminal, their pty test and the guest test program from the public console API documentation. * test: let the console client run by hand, saying which step failed Assisted-by: Claude Code:claude-opus-5-5 AI-Scope: Made the exact size and cursor checks optional and gave each step an instruction and a failure number. * feat: follow the console cursor through the output instead of asking the terminal Programs that redraw their command line read and move the cursor on every key; asking the terminal each time was slow, and wrong on terminals that never answer. The terminal is now asked only when the position may be stale. Assisted-by: Claude Code:claude-opus-5-5 AI-Scope: Wrote the cursor tracker, WriteConsoleOutputCharacter, ScrollConsoleScreenBuffer, the resize events and their tests.
210 lines
7.8 KiB
C
210 lines
7.8 KiB
C
#include <windows.h>
|
|
|
|
/* The console over a terminal. Each READY- line hands the turn to whoever is typing, the person or console_test, for what the next step
|
|
reads. With --exact, as console_test runs it through a pseudo-terminal of 100 by 30 cells that answers the cursor position query
|
|
with row 5, column 10, the window and cursor must be exactly those. A failed step says so and returns its own number. The program
|
|
ends in the Ctrl+C that no handler takes, with STATUS_CONTROL_C_EXIT. */
|
|
|
|
static HANDLE input, output;
|
|
static volatile LONG handled, declined;
|
|
|
|
static DWORD length(const char *text)
|
|
{
|
|
DWORD size = 0;
|
|
while (text[size])
|
|
++size;
|
|
return size;
|
|
}
|
|
|
|
static void say(const char *text)
|
|
{
|
|
DWORD written;
|
|
WriteFile(output, text, length(text), &written, NULL);
|
|
}
|
|
|
|
/* Reports a failed step, for a person running the program by hand, and gives the step number to return. */
|
|
static int failed(int step)
|
|
{
|
|
char text[] = "FAILED at step 00\n";
|
|
text[15] = (char)('0' + step / 10);
|
|
text[16] = (char)('0' + step % 10);
|
|
say(text);
|
|
return step;
|
|
}
|
|
|
|
/* Whether the command line holds the argument, anywhere after the program name. */
|
|
static BOOL argument(const WCHAR *wanted)
|
|
{
|
|
const WCHAR *line = GetCommandLineW();
|
|
for (; *line; ++line) {
|
|
DWORD index = 0;
|
|
while (wanted[index] && line[index] == wanted[index])
|
|
++index;
|
|
if (!wanted[index])
|
|
return TRUE;
|
|
}
|
|
return FALSE;
|
|
}
|
|
|
|
static BOOL same(const WCHAR *left, const WCHAR *right, DWORD count)
|
|
{
|
|
for (DWORD index = 0; index < count; ++index)
|
|
if (left[index] != right[index])
|
|
return FALSE;
|
|
return TRUE;
|
|
}
|
|
|
|
static BOOL WINAPI takes(DWORD event)
|
|
{
|
|
if (event == CTRL_C_EVENT)
|
|
InterlockedExchange(&handled, 1);
|
|
return TRUE;
|
|
}
|
|
|
|
static BOOL WINAPI declines(DWORD event)
|
|
{
|
|
(void)event;
|
|
InterlockedExchange(&declined, 1);
|
|
say("DECLINED\n");
|
|
return FALSE;
|
|
}
|
|
|
|
/* The next key press, skipping releases. */
|
|
static BOOL next_press(KEY_EVENT_RECORD *key)
|
|
{
|
|
for (;;) {
|
|
INPUT_RECORD record;
|
|
DWORD count = 0;
|
|
if (!ReadConsoleInputW(input, &record, 1, &count) || count != 1 || record.EventType != KEY_EVENT)
|
|
return FALSE;
|
|
if (record.Event.KeyEvent.bKeyDown) {
|
|
*key = record.Event.KeyEvent;
|
|
return TRUE;
|
|
}
|
|
}
|
|
}
|
|
|
|
int raven_entry(void)
|
|
{
|
|
DWORD mode = 0, count = 0;
|
|
input = GetStdHandle(STD_INPUT_HANDLE);
|
|
output = GetStdHandle(STD_OUTPUT_HANDLE);
|
|
|
|
if (!GetConsoleMode(input, &mode) || (mode & 7) != 7)
|
|
return failed(1);
|
|
if (!GetConsoleMode(output, &mode) || mode != (ENABLE_PROCESSED_OUTPUT | ENABLE_WRAP_AT_EOL_OUTPUT))
|
|
return failed(2);
|
|
if (!SetConsoleMode(output, mode | ENABLE_VIRTUAL_TERMINAL_PROCESSING) || !GetConsoleMode(output, &mode) || !(mode & 4))
|
|
return failed(3);
|
|
if (SetConsoleMode(input, ENABLE_ECHO_INPUT) || GetLastError() != ERROR_INVALID_PARAMETER)
|
|
return failed(4);
|
|
|
|
CONSOLE_SCREEN_BUFFER_INFO info;
|
|
if (!GetConsoleScreenBufferInfo(output, &info) || info.wAttributes != 7 || info.srWindow.Right != info.dwSize.X - 1 ||
|
|
info.srWindow.Bottom != info.dwSize.Y - 1 || info.dwCursorPosition.X >= info.dwSize.X || info.dwCursorPosition.Y >= info.dwSize.Y)
|
|
return failed(5);
|
|
if (argument(L"--exact") && (info.dwSize.X != 100 || info.dwSize.Y != 30 || info.dwCursorPosition.X != 9 || info.dwCursorPosition.Y != 4))
|
|
return failed(5);
|
|
if (GetConsoleScreenBufferInfo(input, &info) || GetLastError() != ERROR_INVALID_HANDLE)
|
|
return failed(6);
|
|
|
|
if (!SetConsoleTextAttribute(output, FOREGROUND_RED | FOREGROUND_INTENSITY))
|
|
return failed(7);
|
|
const WCHAR text[] = L"café \xd83d\xde42\n";
|
|
if (!WriteConsoleW(output, text, 8, &count, NULL) || count != 8)
|
|
return failed(8);
|
|
SetConsoleTextAttribute(output, 7);
|
|
|
|
WCHAR line[64];
|
|
say("READY-LINE: type hello and press Enter\n");
|
|
if (!ReadConsoleW(input, line, 64, &count, NULL) || count != 7 || !same(line, L"hello\r\n", 7))
|
|
return failed(9);
|
|
|
|
/* The cursor is followed through the output and the echoed line, not asked of the terminal again: below the two lines written
|
|
since the first query, which found it at row 5, column 10. Moves are relative, so they hold on a terminal that never answers. */
|
|
BOOL exact = argument(L"--exact");
|
|
CONSOLE_SCREEN_BUFFER_INFO before;
|
|
if (!GetConsoleScreenBufferInfo(output, &before) || (exact && (before.dwCursorPosition.X != 0 || before.dwCursorPosition.Y != 7)))
|
|
return failed(25);
|
|
COORD cell = {4, (SHORT)(before.dwCursorPosition.Y > 2 ? before.dwCursorPosition.Y - 2 : 0)};
|
|
if (!SetConsoleCursorPosition(output, cell) || !GetConsoleScreenBufferInfo(output, &info) || info.dwCursorPosition.X != cell.X ||
|
|
info.dwCursorPosition.Y != cell.Y)
|
|
return failed(26);
|
|
COORD corner = {0, 0};
|
|
if (!WriteConsoleOutputCharacterW(output, L"XY", 2, corner, &count) || count != 2 || !GetConsoleScreenBufferInfo(output, &info) ||
|
|
info.dwCursorPosition.X != cell.X || info.dwCursorPosition.Y != cell.Y)
|
|
return failed(27);
|
|
SMALL_RECT rows = {0, 1, (SHORT)(info.dwSize.X - 1), (SHORT)(info.dwSize.Y - 1)};
|
|
CHAR_INFO blank = {{L' '}, 7};
|
|
if (!ScrollConsoleScreenBufferW(output, &rows, NULL, corner, &blank) || !GetConsoleScreenBufferInfo(output, &info) ||
|
|
info.dwCursorPosition.X != cell.X || info.dwCursorPosition.Y != cell.Y)
|
|
return failed(28);
|
|
if (!SetConsoleCursorPosition(output, before.dwCursorPosition))
|
|
return failed(29);
|
|
|
|
if (!SetConsoleMode(input, ENABLE_PROCESSED_INPUT) || !GetConsoleMode(input, &mode) || (mode & 7) != ENABLE_PROCESSED_INPUT)
|
|
return failed(10);
|
|
say("READY-KEYS: press Up, x, Ctrl+Right, Esc\n");
|
|
KEY_EVENT_RECORD key;
|
|
if (!next_press(&key) || key.wVirtualKeyCode != VK_UP || !(key.dwControlKeyState & ENHANCED_KEY) || key.uChar.UnicodeChar)
|
|
return failed(11);
|
|
if (!next_press(&key) || key.wVirtualKeyCode != 'X' || key.uChar.UnicodeChar != L'x' || key.wVirtualScanCode != 0x2d)
|
|
return failed(12);
|
|
if (!next_press(&key) || key.wVirtualKeyCode != VK_RIGHT || !(key.dwControlKeyState & LEFT_CTRL_PRESSED))
|
|
return failed(13);
|
|
if (!next_press(&key) || key.wVirtualKeyCode != VK_ESCAPE)
|
|
return failed(14);
|
|
say("READY-CHAR: type q\n");
|
|
if (!ReadConsoleW(input, line, 64, &count, NULL) || count != 1 || line[0] != L'q')
|
|
return failed(15);
|
|
if (!FlushConsoleInputBuffer(input) || !GetNumberOfConsoleInputEvents(input, &count) || count)
|
|
return failed(16);
|
|
|
|
HANDLE screen = CreateFileW(L"CONOUT$", GENERIC_READ | GENERIC_WRITE, FILE_SHARE_READ | FILE_SHARE_WRITE, NULL, OPEN_EXISTING, 0, NULL);
|
|
if (screen == INVALID_HANDLE_VALUE || !GetConsoleMode(screen, &mode) || !WriteFile(screen, "via-conout\n", 11, &count, NULL))
|
|
return failed(17);
|
|
CloseHandle(screen);
|
|
|
|
/* A program that asks for window input hears of a new size. */
|
|
if (!SetConsoleMode(input, ENABLE_PROCESSED_INPUT | ENABLE_WINDOW_INPUT))
|
|
return failed(30);
|
|
say("READY-SIZE: resize the window\n");
|
|
for (;;) {
|
|
INPUT_RECORD record;
|
|
if (!ReadConsoleInputW(input, &record, 1, &count) || count != 1)
|
|
return failed(31);
|
|
if (record.EventType != WINDOW_BUFFER_SIZE_EVENT)
|
|
continue;
|
|
if (exact && (record.Event.WindowBufferSizeEvent.dwSize.X != 120 || record.Event.WindowBufferSizeEvent.dwSize.Y != 40))
|
|
return failed(32);
|
|
break;
|
|
}
|
|
|
|
if (!SetConsoleMode(input, 0x1f7))
|
|
return failed(18);
|
|
char bytes[16];
|
|
say("READY-FILE: type abc and press Enter\n");
|
|
if (!ReadFile(input, bytes, sizeof bytes, &count, NULL) || count != 5 || bytes[0] != 'a' || bytes[3] != '\r' || bytes[4] != '\n')
|
|
return failed(19);
|
|
|
|
WCHAR title[32];
|
|
if (!SetConsoleTitleW(L"Raven test") || GetConsoleTitleW(title, 32) != 10 || !same(title, L"Raven test", 11))
|
|
return failed(20);
|
|
|
|
if (!SetConsoleCtrlHandler(takes, TRUE))
|
|
return failed(21);
|
|
say("READY-CTRL: press Ctrl+C\n");
|
|
for (int wait = 0; wait < 6000 && !handled; ++wait)
|
|
Sleep(10);
|
|
if (!handled)
|
|
return failed(22);
|
|
say("HANDLED\n");
|
|
|
|
if (!SetConsoleCtrlHandler(takes, FALSE) || SetConsoleCtrlHandler(takes, FALSE) || !SetConsoleCtrlHandler(declines, TRUE))
|
|
return failed(23);
|
|
say("READY-EXIT: press Ctrl+C to end the program\n");
|
|
for (int wait = 0; wait < 6000; ++wait)
|
|
Sleep(10);
|
|
return failed(24);
|
|
}
|