From d730cfda9f6b820efaa6fd13350c0d7da3aa56bd Mon Sep 17 00:00:00 2001 From: Leonard Hecker Date: Tue, 30 Jul 2024 18:35:42 +0200 Subject: [PATCH] Add a spec for an In-process ConPTY (#17387) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 👉 Preview: https://github.com/microsoft/terminal/blob/dev/lhecker/13000-spec/doc/specs/%2313000%20-%20In-process%20ConPTY.md The spec has a tl;dr! The tl;dr^2 for the commit message: * Less bugs * Less code * More perf --- .github/actions/spelling/expect/expect.txt | 3 + doc/specs/#13000 - In-process ConPTY.md | 840 +++++++++++++++++++++ 2 files changed, 843 insertions(+) create mode 100644 doc/specs/#13000 - In-process ConPTY.md diff --git a/.github/actions/spelling/expect/expect.txt b/.github/actions/spelling/expect/expect.txt index 63c7d273c6..fde8b098ae 100644 --- a/.github/actions/spelling/expect/expect.txt +++ b/.github/actions/spelling/expect/expect.txt @@ -1237,6 +1237,7 @@ NUMSCROLL NUnit nupkg NVIDIA +NVT OACR objbase ocolor @@ -1411,6 +1412,7 @@ processenv processhost PROCESSINFOCLASS PRODEXT +Productize PROPERTYID PROPERTYKEY propertyval @@ -1818,6 +1820,7 @@ TITLEISLINKNAME TJson TLambda TLDP +tldr TLEN TMAE TMPF diff --git a/doc/specs/#13000 - In-process ConPTY.md b/doc/specs/#13000 - In-process ConPTY.md new file mode 100644 index 0000000000..763b836432 --- /dev/null +++ b/doc/specs/#13000 - In-process ConPTY.md @@ -0,0 +1,840 @@ +--- +author: Leonard Hecker @lhecker +created on: 2024-06-07 +last updated: 2024-06-07 +issue id: 13000 +--- + +# In-process ConPTY + +## tl;dr + +**Why?** +* Out-of-process leads to out-of-sync issues. +* Not all Console APIs have a VT equivalent. +* Time consuming maintenance work due to poor encapsulation inside conhost. + +**How?** +1. Remove `VtEngine` and translate Console API calls directly to VT. +2. Move all Console API related code from the `Host` to the `Server` project. + Narrow the `IApiRoutines` interface down to its essentials. + Replace relevant singletons with instances. +3. Make `Server` a standalone library with `IApiRoutines` as its primary callback interface. + Integrate the library in Windows Terminal. +4. (Long term goal:) Ship the `Server` library as part of Windows. Build conhost itself on top of it. + +## Why? + +**Before:** + +```mermaid +%%{ init: { "flowchart": { "curve": "monotoneX" } } }%% +flowchart TD + subgraph conhost + direction LR + + subgraph Server + server_io[Console API calls] + end + + subgraph ConPTY + VtIo + VtEngine + ConPTY_output[ConPTY Output] + end + + conhost_parser[VT parser] + conhost_buffer[Text Buffer] + conhost_renderer[Render Thread] + conhost_atlas[Text Renderer] + + server_io-->conhost_parser + server_io--scroll, clear, etc.-->VtIo + conhost_parser--resize, focus-->VtIo + VtIo--scroll, clear, etc.-->VtEngine + conhost_parser--unknown escape sequences-->VtEngine + conhost_buffer--manual flushing-->VtEngine + conhost_renderer-->VtEngine + VtEngine-->ConPTY_output + conhost_buffer--dirty rects-->conhost_renderer + conhost_parser--dirty rects-->conhost_renderer + conhost_parser--plain text-->conhost_buffer + conhost_renderer-->conhost_atlas + conhost_buffer--ReadBuffer-->server_io + end + + subgraph wt[Windows Terminal] + direction LR + + input[ConPTY Input] + parser[VT parser] + buffer[Text Buffer] + renderer[Render Thread] + atlas[Text Renderer] + + input-->parser + parser-->buffer + buffer-->renderer + renderer-->atlas + parser-->renderer + end + + conhost--ConPTY's text pipe across processes-->wt +``` + +**After:** + +```mermaid +%%{ init: { "flowchart": { "curve": "monotoneX" } } }%% +flowchart LR + subgraph ConPTY_lib["ConPTY (static library)"] + server_io[Console API calls] + ConPTY_Translation[VT Translation] + + server_io<-->ConPTY_Translation + end + + subgraph ConPTY["ConPTY (dynamic library)"] + ConPTY_impl[IApiRoutines impl] + ConPTY_parser[VT parser] + ConPTY_buffer[Text Buffer] + ConPTY_output[ConPTY Output] + + ConPTY_Translation<-->ConPTY_impl + ConPTY_impl-->ConPTY_parser + ConPTY_parser-->ConPTY_buffer + ConPTY_buffer--ReadBuffer-->ConPTY_impl + ConPTY_impl------>ConPTY_output + end + + subgraph conhost + conhost_impl[IApiRoutines impl] + conhost_parser[VT parser] + conhost_buffer[Text Buffer] + conhost_renderer[Render Thread] + conhost_atlas[Text Renderer] + + ConPTY_Translation<-->conhost_impl + conhost_impl-->conhost_parser + conhost_parser-->conhost_buffer + conhost_buffer--->conhost_renderer + conhost_buffer--ReadBuffer-->conhost_impl + conhost_renderer-->conhost_atlas + end + + subgraph wt[Windows Terminal] + impl[IApiRoutines impl] + parser[VT parser] + buffer[Text Buffer] + renderer[Render Thread] + atlas[Text Renderer] + + ConPTY_Translation<-->impl + impl-->parser + parser-->buffer + buffer--->renderer + buffer--ReadBuffer-->impl + renderer-->atlas + end +``` + +To extend on the [tl;dr](#tldr): +* ConPTY runs outside the hosting terminal which leads to an unsolvable issue: The buffer contents between ConPTY and the terminal can go out of sync. + * The terminal and ConPTY may implement escape sequences differently. + * ...may implement text processing differently. + * ...may implement text reflow (resize) differently. + * Resizing the terminal and ConPTY is asynchronous and there may be concurrent text output. + * ...and it may uncover text from the scrollback, which ConPTY doesn't know about. +* Some Console API methods cannot be represented via escape sequences and so ConPTY cannot produce them either. + The most basic example of this is the lack of LVB gridlines. +* The above suggested new architecture represents a significant simplification with no loss in features. +* ConPTY has fulfilled our needs a thousand times over, but as it's layered on top of conhost it has resulted in software decay. + The layer boundary has blurred over the years resulting in debugging and maintenance difficulties. + Lastly, its performance is insufficient and has been subject to much debate. + It's on us now to pay our debt and clean up the architecture so that ConPTY, conhost, and Windows Terminal can be built on top of it. + +Considerations: +* A named `MUTEX` can theoretically solve parts of the out-of-sync issue, because it could be used to synchronize buffer reflow. + However, this requires the lock to be acquired on every API call, on top of the regular console lock, which raises ABBA deadlock concerns. + Making this setup not just hopefully but also provably robust, is likely to be very difficult. + It also doesn't solve any of the other listed problems. + +## Goal 1: Remove VtEngine + +### Goal + +```mermaid +%%{ init: { "flowchart": { "curve": "monotoneX" } } }%% +flowchart TD + subgraph conhost + direction LR + + subgraph Server + server_io[Console API calls] + end + + subgraph ConPTY + VtIo + VtEngine + ConPTY_output[ConPTY Output] + end + + conhost_parser[VT parser] + conhost_buffer[Text Buffer] + conhost_renderer[Render Thread] + conhost_atlas[Text Renderer] + + server_io-->conhost_parser + server_io--VT translation-->VtIo + server_io-->VtIo + conhost_parser-->VtIo + VtIo-->VtEngine + conhost_parser-->VtEngine + conhost_buffer-->VtEngine + conhost_renderer-->VtEngine + VtIo-->ConPTY_output + VtEngine-->ConPTY_output + conhost_buffer--dirty rects-->conhost_renderer + conhost_parser-->conhost_renderer + conhost_parser--plain text-->conhost_buffer + conhost_renderer-->conhost_atlas + conhost_buffer--ReadBuffer-->server_io + end + + subgraph wt[Windows Terminal] + direction LR + + input[ConPTY Input] + parser[VT parser] + buffer[Text Buffer] + renderer[Render Thread] + atlas[Text Renderer] + + input-->parser + parser-->buffer + buffer-->renderer + renderer-->atlas + parser-->renderer + end + + conhost--ConPTY's text pipe across processes-->wt + + linkStyle 2,3,4,5,6,7,9,11 opacity:0.2 + style VtEngine opacity:0.2 +``` + +### Goals + +* Remove VtEngine +* Remove `--vtmode` +* Remove `--resizeQuirk` +* Add buffering to `VtIo` and hook it up to the output pipe +* Implement direct VT translations for all relevant Console APIs + +### Discussion + +The idea is that we can translate Console API calls directly to VT at least as well as the current VtEngine setup can. +For instance, a call to `SetConsoleCursorPosition` clearly translates directly to a `CUP` escape sequence. +Effectively, instead of translating output asynchronously in the renderer thread, we'll do it synchronously right during the Console API call. + +Apart from the Console APIs, the "cooked read" implementation, which handles our builtin "readline"-like text editor, will need to receive some larger changes as well. +Its popups use `ReadConsoleOutput` and `WriteConsoleOutput` to backup and restore the affected rectangle. +It also directly interfaces with the backing text buffer and its translation to VT relies on the existence of VtEngine. +This results in all of the same issues that were previously outlined. +In order to solve this, the popups need to be rewritten to use escape sequences so that they can be directly passed to the hosting terminal. +They should also always be below the current prompt line so that we don't need to perform a potentially lossy backup/restore operation. + +The `--vtmode xterm-ascii` switch exists for the telnet client as it only supports ASCII as per RFC854 section "THE NVT PRINTER AND KEYBOARD". +However, telnet is the only user of this flag and it's trivial to do there (for instance by stripping high codepoints in `WriteOutputToClient` in `telnet/console.cpp`), so there's no reason for us to keep this logic in the new code. + +This change will result in a significant reduction in complexity of our architecture. +VT input from the shell or other clients will be given 1:1 to the hosting terminal, which will resolve our ordering and buffering issues. + +## Goal 2: Move Console API implementations to Server + +### Goal + +```mermaid +%%{ init: { "flowchart": { "curve": "monotoneX" } } }%% +flowchart TD + subgraph conhost + direction LR + + subgraph Server[Server w/ ConPTY] + server_io[Console API calls] + ConPTY_Translation[VT Translation] + + server_io<-->ConPTY_Translation + end + + subgraph ConPTY + VtIo + ConPTY_output[ConPTY Output] + + VtIo-->ConPTY_output + end + + conhost_impl[IApiRoutines impl] + conhost_parser[VT parser] + conhost_buffer[Text Buffer] + conhost_renderer[Render Thread] + conhost_atlas[Text Renderer] + output_new[ConPTY Output] + + ConPTY_Translation<-->conhost_impl + conhost_impl-->conhost_parser + conhost_impl----->output_new + server_io---->VtIo + server_io-->conhost_parser + conhost_parser-->conhost_buffer + conhost_parser-->conhost_renderer + conhost_buffer-->conhost_renderer + conhost_buffer--ReadBuffer-->conhost_impl + conhost_buffer-->server_io + conhost_renderer-->conhost_atlas + end + + subgraph wt[Windows Terminal] + direction LR + + input[ConPTY Input] + parser[VT parser] + buffer[Text Buffer] + renderer[Render Thread] + atlas[Text Renderer] + + input-->parser + parser-->buffer + buffer-->renderer + renderer-->atlas + parser-->renderer + end + + conhost--ConPTY's text pipe across processes-->wt + + linkStyle 1,5,6,11 opacity:0.2 + style ConPTY opacity:0.2 + style VtIo opacity:0.2 + style ConPTY_output opacity:0.2 +``` + +### Goals + +* Move the API implementation from the `Host` to the `Server` project. +* Narrow down the `IApiRoutines` interface to its essentials. +* Replace affected singletons with regular class instances. + +### Discussion + +The basic idea is that instead of having 3 arrows going in and out of the Server component, we got exactly 1. +This makes the console server and its VT translation a reusable component, which we need so that we can solve the out-of-sync issues by integrating it into Windows Terminal. +To make the Server API convenient to use, the interface needs to be narrowed down to as few methods as possible. This interface is currently called `IApiRoutines`. + +### API Design + +Design goals: +* Low overhead
+ The API should never be the reason why the terminal is slow. + Among others, the design includes both UTF8 and UTF16 functions, as both encodings are common on Windows and the server component should not make assumptions which one the terminal prefers. +* Easy to use
+ The Console API is unfortunately quite powerful. + In order to make implementing the callback interface still reasonably easy, the API should have as few methods as are needed. + If a complex Console API call can be expressed as a series of simpler ones, and if no other performance expectations exist, it should be expressed via the simpler ones. +* Works even if only partially implemented
+ Example: `CreateConsoleScreenBuffer` is seldomly used, but its existence adds significant complexity to the callback API design and potential implementations. + A terminal should either be able to return `E_NOTIMPL` and we provide a fallback, or we provide guidance for how reasonable fallbacks can be implemented (e.g. by using the xterm alt buffer in this example). + +> [!IMPORTANT] +> The following API design is a rough draft just to convey the general idea. +> It does not represent a complete, finished design. +> This document will be updated once work on the API begins and the actual API requirements become clearer. + +```cs +// The Console API is built around a freely addressable frame buffer. It allows you to +// address any part of the buffer, even those outside of what's considered the "viewport": +// +// ┌──────────────────┐ +// │y=-3 │ +// │ │ ╮ +// │ │ │ +// ╭ ├──────────────────┤ ├── VT scrollback (partially addressed) +// │ │y=0 │ │ +// │ │ │ ╮ ╯ +// │ │ │ │ +// Console Buffer ──┤ ├──────────────────┤ ├──── VT Viewport (top y = 1) +// │ │y=3 │ │ +// │ │ │ ╯ +// │ │ │ +// ╰ └──────────────────┘ +// +// The good news is that nothing prevents you from giving the Console Buffer the exact +// same size as the VT Viewport and for modern terminals doing so is recommended. +// That way, coordinates are viewport-relative and content below/above the viewport is never addressed: +// +// ┌──────────────────┐ ╮ +// │y=-3 │ │ +// │ │ ├── VT scrollback (unused) +// │ │ │ +// ╭ ├──────────────────┤ ╮ ╯ +// │ │y=0 │ │ +// Console Buffer ──┤ │ │ ├──── VT Viewport +// │ │ │ │ +// ╰ └──────────────────┘ ╯ +// +// Coordinates are 0-indexed. Note that while INT32 coordinates are used by this API, coordinates below +// 0 and above 65535 are generally invalid as the Console ABI currently uses unsigned 16-Bit integers. + +struct CONSRV_POINT_I32 { + INT32 x; + INT32 y; +}; + +struct CONSRV_POINT_F32 { + float x; + float y; +}; + +// These flags are also defined via Windows.h. +#if 0 +// These flags are equivalent to the classic 4-bit indexed colors in VT via SGR. +// However, the position of the blue and red bits are swapped. +#define FOREGROUND_BLUE 0x0001 // Text color contains blue. +#define FOREGROUND_GREEN 0x0002 // Text color contains green. +#define FOREGROUND_RED 0x0004 // Text color contains red. +#define FOREGROUND_INTENSITY 0x0008 // Text color is intensified. +#define BACKGROUND_BLUE 0x0010 // Background color contains blue. +#define BACKGROUND_GREEN 0x0020 // Background color contains green. +#define BACKGROUND_RED 0x0040 // Background color contains red. +#define BACKGROUND_INTENSITY 0x0080 // Background color is intensified. + +// These two bits are used to represent wide glyphs. +#define COMMON_LVB_LEADING_BYTE 0x0100 // Leading byte. +#define COMMON_LVB_TRAILING_BYTE 0x0200 // Trailing byte. + +// This bit is equivalent to the "CSI 7 m" reverse video escape sequence. +#define COMMON_LVB_REVERSE_VIDEO 0x4000 // Reverse foreground and background attribute. + +// NOTE: These flags have no equivalent in VT. COMMON_LVB_UNDERSCORE in particular is not the same as a +// "CSI 4 m" underline in VT, despite the name. They're used to give a cell border (= grid) lines. +#define COMMON_LVB_GRID_HORIZONTAL 0x0400 // Top horizontal. +#define COMMON_LVB_GRID_LVERTICAL 0x0800 // Left vertical. +#define COMMON_LVB_GRID_RVERTICAL 0x1000 // Right vertical. +#define COMMON_LVB_UNDERSCORE 0x8000 // Underscore. +#endif + +// This struct is binary compatible to the CHAR_INFO struct from the Windows API and functionally equivalent. +// +// The following rules MUST be followed: +// * Each instance represents 1 column in the terminal. +// * Any cells that aren't representable with a single UCS-2 character, or are wider than 2 columns, +// must be replaced with U+FFFD. Grapheme clusters, surrogate pairs, and similar, are not allowed. +// Keep in mind that U+FFFD is a narrow character. It does not get the wide wide-glyph treatment below. +// * Colors that cannot be represented via the `attributes` flags can be replaced with an approximation. +// Alternatively, `FOREGROUND_BLUE | FOREGROUND_GREEN | FOREGROUND_RED` can be used (no `BACKGROUND` bit set). +// * If a wide glyph (2 columns wide) is encountered, the following applies: +// * Create 2 consecutive CONSRV_CHAR_INFO instances (as per the first rule). +// * Repeat the same `character` in both instances, even if it's U+FFFD. +// * Assign the first instance an `attributes` flag of `COMMON_LVB_LEADING_BYTE` +// and the second one `COMMON_LVB_TRAILING_BYTE`. +// * BUT if the request for a CONSRV_CHAR_INFO only partially intersects a wide glyph, replace the character +// (and only the character) with U+0020 whitespace. This also means that `COMMON_LVB_LEADING/TRAILING_BYTE` +// should not be set, because the returned character isn't wide anymore. An example: +// If you have a red "猫" on top of a blue background in the buffer and a single `CONSRV_CHAR_INFO` is requested +// for the left half of the glyph, then you should set `character` to whitespace and `attributes` to +// `FOREGROUND_RED | BACKGROUND_BLUE`. Debug builds of ConPTY will assert that you do this. +// +// For more documentation about the `attributes` flags, see the constants defined above. +struct CONSRV_CHAR_INFO { + wchar_t character; + UINT16 attributes; +}; + +struct CONSRV_UTF8_STRING { + [size_is(length)] const char* data; + DWORD length; +}; + +struct CONSRV_UTF16_STRING { + [size_is(length)] const wchar_t* data; + DWORD length; +}; + +// NOTE: At the time of writing the required fields are not fully known. +// NOTE: boolean is 8 Bits large. +struct CONSRV_INFO { + // NOTE: msys2 relies on the HWND value to uniquely identify terminal sessions. + // If we were to hand out the multiplexed terminal window HWND to msys2, it will break. Either we need to + // create fake windows inside conhost (very bad & buggy) or break msys2 intentionally (also very bad). + HWND window; + CONSRV_UTF16_STRING originalWindowTitle; + CONSRV_UTF16_STRING windowTitle; + + CONSRV_POINT_I32 bufferSizeInCells; + CONSRV_POINT_I32 cursorPositionInCells; + CONSRV_POINT_I32 viewPositionInCells; + CONSRV_POINT_I32 viewSizeInCells; + CONSRV_POINT_F32 cellSizeInDIP; + COLORREF colorTable[16]; + + CONSRV_POINT_I32 selectionStart; + CONSRV_POINT_I32 selectionEnd; + boolean selectionActive; + boolean selectionRectangular; + boolean selectionMouseDown; + + float cursorHeight; + boolean cursorHidden; +}; + +// Any item that has changed relative to the current CONSRV_INFO will be set to a non-null pointer. +// In other words, members that are null represent those that remain unchanged. +// +// If the request cannot be supported return E_INVALIDARG. For instance, you may choose to +// do so if you receive a `bufferSizeInCells` change while the xterm alt buffer is active. +// +// NOTE: At the time of writing the required fields are not fully known. +struct CONSRV_INFO_CHANGE { + CONSRV_POINT_I32* bufferSizeInCells; + CONSRV_POINT_I32* cursorPositionInCells; + CONSRV_POINT_I32* viewPositionInCells; + CONSRV_POINT_I32* viewSizeInCells; + COLORREF* colorTable; // The referenced array is always 16 items large. + + float* cursorHeight; + boolean* cursorHidden; + + CONSRV_UTF16_STRING* fontName; + UINT32* fontFamily; + UINT32* fontWeight; + CONSRV_POINT_I32* fontSize; +}; + +interface IConsoleServer : IUnknown { + // TODO: This interface is incomplete. Among others, a way to launch new application into the server is missing. + + // ConPTY manages stdin as a ring buffer for you. When the terminal has focus, you simply need to write your input. + // Keyboard input MUST be written via `WriteInputRecords`. The other 2 functions DO NOT parse any VT sequences. + // They're instead meant either for VT responses (DECRPM, etc.) and for dumping plain text (clipboard, etc.). + void WriteInputRecords([in] DWORD count, [in, length_is(count)] const INPUT_RECORD* records); + void WriteInputUTF8([in] CONSRV_UTF8_STRING text); + void WriteInputUTF16([in] CONSRV_UTF16_STRING text); +}; + +// First of all: You don't need to implement all functions and all structs perfectly for ConPTY to work decently well. +// For instance, if you don't implement `CONSRV_INFO::cursorHeight` properly, barely anything will happen. +interface IConsoleServerCallback : IUnknown { + // The console server is single-threaded and no two calls will be made simultaneously. These two functions + // simply allow you to synchronize the calls down below if your application is multi-threaded. + // + // Lock() will always be called before any of the functions below are called. + // Lock() and Unlock() do not need to support recursive locking. + // Any other calls between Lock() and Unlock() should be treated as an atomic operation. + // + // It is recommended to use a fair lock instead of OS primitives like SRWLOCK. These callback functions may be + // much more often than your text renderer, etc., runs. An unfair lock will result in thread starvation. + HRESULT Lock(); + HRESULT Unlock(); + + // If called, you're requested to create a new console alt buffer. The Console API supports having + // multiple concurrent such buffers. They're not the same as the xterm alt buffer, however (CSI ? 1049 h): + // They can be resized to be larger than the current viewport and switching between such buffers DOES NOT + // clear them nor does it reset any other per-buffer state. + // + // If you have trouble adding support for multiple console alt buffers, consider using the xterm alt buffer + // (CSI ? 1049 h) for the first buffer that gets created, and return E_OUTOFMEMORY for any further buffers. + HRESULT CreateBuffer([out] void** buffer); + + // ReleaseBuffer is called once the buffer isn't needed anymore. It's guaranteed to be called and it's guaranteed + // to be called after ActivateBuffer() was used to switch to another buffer (or the main buffer). + HRESULT ReleaseBuffer([in] void* buffer); + + // This switches between different console alt buffers. Switching to a buffer should change the content that's + // being shown, similar to the xterm alt buffer, however unlike it doing so DOES NOT reset any per-buffer state. + // All it does is to basically swap out the underlying, active text buffer of the terminal. + // + // If `buffer` is NULL it's a request to switch back to the main buffer. + // + // `temporary` is a hint. If it's `true` it indicates that the previous buffer will soon be activated again. + // In other words, on Unlock() the buffer was active during Lock() will be active again. + // It's recommended that temporary switches are lightweight as they may occur relatively often. + HRESULT ActivateBuffer([in] void* buffer, [in] boolean temporary); + + // + // Any functions past this point operate on the currently active buffer. + // + + // This function gets a snapshot of the terminal and buffer state. + // + // You must ensure that the returned pointer stays valid until the next GetInfo() or Unlock() call, + // or until the currently active buffer is released. + // You don't need to return a new instance on each call. ConPTY will only use the last returned pointer. + // You don't need to keep the CONSRV_INFO struct constantly up to date, but you're allowed to do so. + // For instance, it's valid to change the `.cursorPosition` when SetCursorPosition() is called. + // + // It's recommended that this function is lightweight as it may be called relatively often. + const CONSRV_INFO* GetInfo(); + + // When this method is called you're asked to apply any non-null member of the given CONSRV_INFO_CHANGE struct + // to the active buffer. For instance a non-null `.cursorPosition` is identical to calling `SetCursorPosition`, + // a non-null `.bufferSize` is a request to resize the terminal, and so on. + HRESULT SetInfo([in] const CONSRV_INFO_CHANGE* info); + + // As explained in the CONSRV_POINT_I32 documentation, ConPTY coordinates may be outside of the VT viewport. + // This function is necessary in order to support this. If you assign the console buffer the same size + // as the VT viewport, `pos` can be translated to VT using + // printf("\x1b[%d;%dC", pos.y + 1, pos.x + 1); + HRESULT SetCursorPosition([in] CONSRV_POINT_I32 pos); + + // The Console API supports 4 gridline attributes which cannot be translated to VT. + // This function is necessary to represent those. If you don't plan to support the gridlines, + // you can translate the attributes to VT with the following code or some equivalent: + // static const uint8_t lut[] = { 30, 34, 32, 36, 31, 35, 33, 37, 90, 94, 92, 96, 91, 95, 93, 97 }; + // const auto fg = lut[attributes & 0xf]; + // const auto bg = lut[(attributes >> 4) & 0xf] + 10; + // printf("\x1b[%d;%dm", fg, bg); + // + // `attributes` of exactly `FOREGROUND_BLUE | FOREGROUND_GREEN | FOREGROUND_RED` are often used to indicate the + // default colors in Windows Console applications, and so you may choose to translate attributes like that as: + // printf("\x1b[39;49"); + // + // You may also choose to support COMMON_LVB_REVERSE_VIDEO, which translates to: + // printf("\x1b[7m"); + HRESULT SetCurrentAttributes([in] UINT16 attributes); + + // Starting from column `pos.x` in row `pos.y`, this reads `count`-many characters and attributes. + // `pos` and `count` will be clamped such that reads never extend outside of the `CONSRV_INFO::bufferSize`. + // + // However, it may still read cells that have never been written to (for instance below the current viewport!). + // Such reads should not fail. Simply fill the `infos` array with whitespaces and a default attribute of your chosing, + // but `FOREGROUND_BLUE | FOREGROUND_GREEN | FOREGROUND_RED` is recommended (no other bits set). + // + // NOTE that this API should ignore any line renditions (DECDWL, DECDHL), margins (DECSLRM, ...), etc. + // Reading outside of the "valid" range for a given row should behave exactly like reading below the viewport, + // as described in the previous paragraph. + HRESULT ReadBuffer([in] CONSRV_POINT_I32 pos, [in] INT32 count, [out, length_is(count)] CONSRV_CHAR_INFO* infos); + + // These two functions are used to layout text for the internal "GNU Readline"-like implementation. + // `text` is the string to operate on. As with any other method, input validation should be performed. + // It's preferred to pretend as if invalid codepoints (in particular invalid surrogate pairs) + // are U+FFFD, because this provides the user with some level of text editing capability. + // The alternative is to have none at all when facing invalid strings which is strictly worse. + // `maxClusters` is the maximum amount of "cursor movements" these functions should apply + // (like when pressing the left/right arrow buttons). + // `maxColumns` is the maximum amount of columns the functions may iterate over. When the text is "a猫" and + // `maxColumns` is 2, then the result should be "a", because "猫" doesn't fit anymore. + // `position` on input contains the current position of the cursor inside `text`, counted in characters from the + // start of the `text`. On output it's supposed to contain the new cursor position. + // `position` may be out of bounds and you should clamp it to a valid range first. + // `columns` on output should contain the number of columns that have been iterated over. + // + // The idea is that a `maxClusters = 1` and `maxColumns = inf` can be used to implement left/right cursor movement, + // while `maxClusters = inf` and `maxColumns = window width` can be used to layout text within the window. + // + // You don't need to handle escape characters. These functions will never be called with any present. + // For robustness against bugs it's however recommended to handle them anyway, in whatever way you wish. + // If you have no preference, it's recommended to treat them as zero-width characters. + HRESULT MeasureTextForward([in] CONSRV_UTF16_STRING text, [in] DWORD maxClusters, [in] DWORD maxColumns, [in, out] DWORD* position, [out] DWORD* columns); + HRESULT MeasureTextBackward([in] CONSRV_UTF16_STRING text, [in] DWORD maxClusters, [in] DWORD maxColumns, [in, out] DWORD* position, [out] DWORD* columns); + + // UTF8 and UTF16 are both widely used text encodings on Windows and it's recommended that both + // functions are reasonably fast. ConPTY will translate all non-Unicode text to UTF16 for you. + // You must validate incoming text. It's recommended to replace invalid codepoints with U+FFFD. + // You don't need to check for broken up codepoints at the start/end of the text, as ConPTY will handle that for you. + HRESULT WriteUTF8([in] boolean raw, [in] CONSRV_UTF8_STRING text); + HRESULT WriteUTF16([in] boolean raw, [in] CONSRV_UTF16_STRING text); +}; +``` + +The list shows how each Console API function is implemented in terms of the above interface. +* Aliases +
Fully implemented inside the server component without API. + * `AddConsoleAlias` + * `GetConsoleAlias` + * `GetConsoleAliases` + * `GetConsoleAliasesLength` + * `GetConsoleAliasExes` + * `GetConsoleAliasExesLength` +* History +
Fully implemented inside the server component without API. + * `ExpungeConsoleCommandHistory` + * `GetConsoleCommandHistory` + * `GetConsoleCommandHistoryLength` + * `GetConsoleHistoryInfo` + * `SetConsoleHistoryInfo` + * `SetConsoleNumberOfCommands` +* stdin +
Fully implemented inside the server component without API. + * `FlushConsoleInputBuffer` + * `GetConsoleInput` + * `GetConsoleInputCodePage` + * `GetConsoleInputMode` + * `GetNumberOfConsoleInputEvents` + * `ReadConsole` + * `SetConsoleInputCodePage` + * `SetConsoleInputMode` + * `WriteConsoleInput` +* Unsupported since conhost v1 + * `GetConsoleDisplayMode` + * `GetConsoleLangId` + * `SetConsoleDisplayMode` +* Buffer management + * `GetConsoleScreenBufferInfoEx`: + Gets information from server's internal `SCREEN_INFORMATION` class (which represents the `HANDLE`). + * `SetConsoleScreenBufferInfoEx`: + Sets information on server's internal `SCREEN_INFORMATION` class. + * `CreateConsoleScreenBuffer`: + `CreateBuffer` + * `SetConsoleActiveScreenBuffer`: + `ActivateBuffer` + `SetInfo` + * `SetConsoleScreenBufferSize`: + `SetInfo` +* Cursor + * `GetConsoleCursorInfo`: + `GetInfo` + * `SetConsoleCursorInfo`: + `SetInfo` + * `SetConsoleCursorPosition`: + `GetInfo` +* Fonts + * `GetConsoleFontSize`: + `GetInfo` + * `GetCurrentConsoleFontEx`: + `GetInfo` + * `SetCurrentConsoleFontEx`: + `SetInfo` +* Window management + * `GetConsoleOriginalTitle`: + `GetInfo` + * `GetConsoleSelectionInfo`: + `GetInfo` + * `GetConsoleTitle`: + `GetInfo` + * `GetConsoleWindow`: + `GetInfo` + * `GetLargestConsoleWindowSize`: + `GetInfo`; The window frame size can be inferred from the difference between the `GetWindowRect(hwnd)` and the `viewSizeInCells * cellSizeInDIP`. + The max. cell count can then be calculated by getting the `MonitorFromWindow(hwnd)` size, subtracting the frame size and calculating the cell count. + * `GetNumberOfConsoleMouseButtons`: + Implemented inside the server component via `GetSystemMetrics(SM_CMOUSEBUTTONS)` + * `SetConsoleTitle`: + `SetInfo` + * `SetConsoleWindowInfo`: + `SetInfo` +* stdout (writing) + * `FillConsoleOutputAttribute`: + Set the new attributes with `SetCurrentAttributes`. + For each line, get the existing contents with `ReadBuffer`, `SetCursorPosition` to the start, concatenate the cells and write them with `WriteUTF16`. + * `FillConsoleOutputCharacter`: + For each line, get the existing contents with `ReadBuffer`, `SetCursorPosition` to the start, concatenate the cells and write them with `WriteUTF16`. + At the start of each line and every time the attributes change use `SetCurrentAttributes` to set them up. + * `GetConsoleOutputCodePage`: + Implemented inside the server component. + * `GetConsoleOutputMode`: + Implemented inside the server component. + * `ScrollConsoleScreenBuffer`: + **TODO**: It may be necessary to add a `ScrollBuffer` API to make vertical scrolling across the entire buffer width faster. This fast pass currently exists as well. + Otherwise, this will be translated to: Read all lines in the source rectangle with `ReadBuffer`. + Then refer to the `WriteConsoleOutput` implementation for writing it to the target. + * `SetConsoleOutputCodePage`: + Implemented inside the server component. + * `SetConsoleOutputMode`: + Implemented inside the server component. + * `SetConsoleTextAttribute`: + `SetCurrentAttributes` + * `WriteConsole`: + `WriteUTF8` if `CP_UTF8` is active and otherwise `WriteUTF16`. + * `WriteConsoleOutput`: + For each line, `SetCursorPosition` to the start, concatenate the cells and write them with `WriteUTF16`. + At the start of each line and every time the attributes change use `SetCurrentAttributes` to set them up. + * `WriteConsoleOutputAttribute`: + Same as `FillConsoleOutputAttribute`, but with varying attributes. + * `WriteConsoleOutputCharacter`: + Same as `FillConsoleOutputCharacter`, but with varying characters. +* stdout (reading) +
Each of these will be translated to a series of `ReadBuffer` calls, one for each line. + * `ReadConsoleOutput` + * `ReadConsoleOutputAttribute` + * `ReadConsoleOutputCharacter` + +## Goal 3: Productize Server + +### Goal + +```mermaid +flowchart LR + subgraph ConPTY_lib["ConPTY (static library)"] + server_io[Console API calls] + ConPTY_Translation[VT Translation] + + server_io<-->ConPTY_Translation + end + + subgraph ConPTY["ConPTY (dynamic library)"] + ConPTY_impl[IApiRoutines impl] + ConPTY_parser[VT parser] + ConPTY_buffer[Text Buffer] + ConPTY_output[ConPTY Output] + + ConPTY_Translation<-->ConPTY_impl + ConPTY_impl-->ConPTY_parser + ConPTY_parser-->ConPTY_buffer + ConPTY_buffer--ReadBuffer-->ConPTY_impl + ConPTY_impl------>ConPTY_output + end + + subgraph conhost + subgraph conhost_Server[Server] + conhost_server_io[Console API calls] + conhost_ConPTY_Translation[VT Translation] + + conhost_server_io<-->conhost_ConPTY_Translation + end + + conhost_impl[IApiRoutines impl] + conhost_parser[VT parser] + conhost_buffer[Text Buffer] + conhost_renderer[Render Thread] + conhost_atlas[Text Renderer] + output_new[ConPTY Output] + + conhost_ConPTY_Translation<-->conhost_impl + conhost_impl-.remains for
back compat.....->output_new + + ConPTY_Translation<-->conhost_impl + conhost_impl-->conhost_parser + conhost_parser-->conhost_buffer + conhost_buffer--->conhost_renderer + conhost_buffer--ReadBuffer-->conhost_impl + conhost_renderer-->conhost_atlas + end + + subgraph wt[Windows Terminal] + impl[IApiRoutines impl] + parser[VT parser] + buffer[Text Buffer] + renderer[Render Thread] + atlas[Text Renderer] + + ConPTY_Translation<-->impl + impl-->parser + parser-->buffer + buffer--->renderer + buffer--ReadBuffer-->impl + renderer-->atlas + end + + linkStyle 6,7 opacity:0.2 + style conhost_Server opacity:0.2 + style conhost_server_io opacity:0.2 + style conhost_ConPTY_Translation opacity:0.2 +``` + +After creating an _internal_ API in conhost, the next logical step is for us to extract the interface as a public one and consume it in Windows Terminal. +At this point we should also see if we can find early adopters of the API in other projects. + +## Stretch Goal 4: Ship Server in Windows + +The console server on Windows has been an internal ABI for the longest time. +As we have started shipping ConPTY bundled with conhost/OpenConsole to other projects, the internal ABI has become a public one. +While we have no intention of ever breaking forward compatability of the console server with future Windows versions, it would be reassuring if the ABI long-term became internal once more. +This will allow us complete freedom over its design, even in the decades to come, and allow us to fully shim any 3rd party applications on Windows if a ABI change were to ever happen.