зеркало из https://github.com/microsoft/terminal.git
Add a spec for an In-process ConPTY (#17387)
👉 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
This commit is contained in:
Родитель
2f43886ab5
Коммит
d730cfda9f
|
@ -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
|
||||
|
|
|
@ -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<br>
|
||||
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<br>
|
||||
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<br>
|
||||
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
|
||||
<br>Fully implemented inside the server component without API.
|
||||
* `AddConsoleAlias`
|
||||
* `GetConsoleAlias`
|
||||
* `GetConsoleAliases`
|
||||
* `GetConsoleAliasesLength`
|
||||
* `GetConsoleAliasExes`
|
||||
* `GetConsoleAliasExesLength`
|
||||
* History
|
||||
<br>Fully implemented inside the server component without API.
|
||||
* `ExpungeConsoleCommandHistory`
|
||||
* `GetConsoleCommandHistory`
|
||||
* `GetConsoleCommandHistoryLength`
|
||||
* `GetConsoleHistoryInfo`
|
||||
* `SetConsoleHistoryInfo`
|
||||
* `SetConsoleNumberOfCommands`
|
||||
* stdin
|
||||
<br>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)
|
||||
<br>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<br>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.
|
Загрузка…
Ссылка в новой задаче