mirror of
https://github.com/microsoft/terminal.git
synced 2026-04-08 15:21:01 +00:00
Merge branch 'main' into dev/migrie/fhl/non-terminal-panes-2023
This commit is contained in:
499
doc/specs/#11000 - Marks/Shell-Integration-Marks.md
Normal file
499
doc/specs/#11000 - Marks/Shell-Integration-Marks.md
Normal file
@@ -0,0 +1,499 @@
|
||||
---
|
||||
author: Mike Griese
|
||||
created on: 2022-03-28
|
||||
last updated: 2023-07-19
|
||||
issue id: 11000, 1527, 6232
|
||||
---
|
||||
|
||||
##### [Original issue: [#1527]] [experimental PR [#12948]] [remaining marks [#14341]]
|
||||
|
||||
# Windows Terminal - Shell Integration (Marks)
|
||||
|
||||
## Abstract
|
||||
|
||||
_"Shell integration" refers to a broad category of ways by which a commandline
|
||||
shell can drive richer integration with the terminal. This spec in particular is
|
||||
most concerned with "marks" and other semantic markup of the buffer._
|
||||
|
||||
Marks are a new buffer-side feature that allow the commandline application or
|
||||
user to add a bit of metadata to a range of text. This can be used for marking a
|
||||
region of text as a prompt, marking a command as succeeded or failed, quickly
|
||||
marking errors in the output. These marks can then be exposed to the user as
|
||||
pips on the scrollbar, or as icons in the margins. Additionally, the user can
|
||||
quickly scroll between different marks, to allow easy navigation between
|
||||
important information in the buffer.
|
||||
|
||||
Marks in the Windows Terminal are a combination of functionality from a variety
|
||||
of different terminal emulators. "Marks" attempts to unify these different, but
|
||||
related pieces of functionality.
|
||||
|
||||
## Background
|
||||
|
||||
There's a large amount of prior art on this subject. I've attempted to collect
|
||||
as much as possible in the ["Relevant external docs"](#Relevant-external-docs)
|
||||
section below. "Marks" have been used in different scenarios by different
|
||||
emulators for different purposes. The common thread running between them of
|
||||
marking a region of text in the buffer with a special meaning.
|
||||
|
||||
* iTerm2, ConEmu, FinalTerm et.al. support emitting a VT sequence to indicate
|
||||
that a line is a "prompt" line. This is often used for quick navigation
|
||||
between these prompts.
|
||||
* FinalTerm (and xterm.js) also support marking up more than just the prompt.
|
||||
They go so far as to differentiate the start/end of the prompt, the start of
|
||||
the commandline input, and the start/end of the command output.
|
||||
`FTCS_COMMAND_FINISHED` is even designed to include metadata indicating
|
||||
whether a command succeeded or failed.
|
||||
* Additionally, Terminal.app allows users to "bookmark" lines via the UI. That
|
||||
allows users to quickly come back to something they feel is important.
|
||||
* Consider also editors like Visual Studio. VS also uses little marks on the
|
||||
scrollbar to indicate where other matches are for whatever the given search
|
||||
term is.
|
||||
|
||||
### "Elevator" pitch
|
||||
|
||||
The Terminal provides a way for command line shells to semantically mark parts
|
||||
of the command-line output. By marking up parts of the output, the Terminal can
|
||||
provide richer experiences. The Terminal will know where each command starts and
|
||||
stops, what the actual command was and what the output of that command is. This
|
||||
allows the terminal to expose quick actions for:
|
||||
|
||||
* Quickly navigating the history by scrolling between commands
|
||||
* Re-running a previous command in the history
|
||||
* Copying all the output of a single command
|
||||
* A visual indicator to separate out one command-line from the next, for quicker
|
||||
mental parsing of the output of the command-line.
|
||||
* Collapsing the output of a command, as to reduce noise
|
||||
* Visual indicators that highlight commands that succeeded or failed.
|
||||
* Jumping to previously used directories
|
||||
|
||||
### User Stories
|
||||
|
||||
This is a bit of a unusual section, as this feature was already partially
|
||||
implemented when this spec was written.
|
||||
|
||||
Story | Size | Description
|
||||
--|-----------|--
|
||||
A | ✅ Done | The user can use mark each prompt and have a mark displayed on the scrollbar
|
||||
B | ✅ Done | The user can perform an action to scroll between marks
|
||||
C | ✅ Done | The user can manually add marks to the buffer
|
||||
D | ✅ Done | The shell can emit different marks to differentiate between prompt, command, and output
|
||||
E | ✅ Done | Clearing the buffer clears marks
|
||||
F | 🐣 Crawl | Marks stay in the same place you'd expect after resizing the buffer.
|
||||
G | ✅ Done | Users can perform an action to select the previous command's output
|
||||
H | 🚶 Walk | The find dialog can display marks on the scrollbar indicating the position of search matches
|
||||
I | 🏃♂️ Run | The terminal can display icons in the gutter, with quick actions for that command (re-run, copy output, etc)
|
||||
J | 🏃♂️ Run | The terminal can display a faint horizontal separator between commands in the buffer.
|
||||
K | 🚀 Sprint | The terminal can "collapse" content between two marks.
|
||||
L | 🚀 Sprint | The terminal can display a sticky header above the control which displays the most recent command
|
||||
M | 🚀 Sprint | The user can open a dialog to manually bookmark a line with a custom comment
|
||||
|
||||
## Solution Design
|
||||
|
||||
### Supported VT sequences
|
||||
|
||||
* [x] iTerm2's OSC `SetMark` (in [#12948])
|
||||
* [x] FinalTerm prompt markup sequences
|
||||
- [x] **FTCS_PROMPT** was added in [#13163]
|
||||
- [x] The rest in [#14341]
|
||||
* [ ] additionally, VsCode's FinalTerm prompt markup variant, `OSC 663`
|
||||
* [ ] [ConEmu's
|
||||
`OSC9;12`](https://conemu.github.io/en/AnsiEscapeCodes.html#ConEmu_specific_OSC)
|
||||
* [ ] Any custom OSC we may want to author ourselves.
|
||||
|
||||
The FinalTerm prompt sequences are probably the most complicated version of all
|
||||
these, so it's important to give these a special callout. Almost all the other
|
||||
VT sequences are roughly equivalent to **FTCS_PROMPT**. The xterm.js / VsCode
|
||||
version has additional cases, that they ironically added to work around conpty
|
||||
not understanding these sequences originally.
|
||||
|
||||
#### FinalTerm sequences
|
||||
|
||||
The relevant FinalTerm sequences for marking up the prompt are as follows.
|
||||
|
||||
|
||||

|
||||
|
||||
* **FTCS_PROMPT**: `OSC 133 ; A ST`
|
||||
- The start of a prompt. Internally, this sets a marker in the buffer
|
||||
indicating we started a prompt at the current cursor position, and that
|
||||
marker will be used when we get a **FTCS_COMMAND_START**
|
||||
* **FTCS_COMMAND_START**: `OSC 133 ; B ST`
|
||||
- The start of a commandline (READ: the end of the prompt). When it follows a
|
||||
**FTCS_PROMPT**, it creates a mark in the buffer from the location of the
|
||||
**FTCS_PROMPT** to the current cursor position, with the category of
|
||||
`prompt`
|
||||
* **FTCS_COMMAND_EXECUTED**: `OSC 133 ; C ST`
|
||||
- The start of the command output / the end of the commandline.
|
||||
* **FTCS_COMMAND_FINISHED**: `OSC 133 ; D ; [Ps] ST`
|
||||
- the end of a command.
|
||||
|
||||
Same deal for the **FTCS_COMMAND_EXECUTED**/**FTCS_COMMAND_FINISHED** ones.
|
||||
**FTCS_COMMAND_EXECUTED** does nothing until we get a **FTCS_COMMAND_FINISHED**,
|
||||
and the `[Ps]` parameter determines the category.
|
||||
- `[Ps] == 0`: success
|
||||
- anything else: error
|
||||
|
||||
This whole sequence will get turned into a single mark.
|
||||
|
||||
When we get the **FTCS_COMMAND_FINISHED**, set the category of the prompt mark
|
||||
that preceded it, so that the `prompt` becomes an `error` or a `success`.
|
||||
|
||||
|
||||
### Buffer implementation
|
||||
|
||||
In the initial PR ([#12948]), marks were stored simply as a `vector<Mark>`,
|
||||
where a mark had a start and end point. These wouldn't reflow on resize, and
|
||||
didn't support all of the FTCS sequences.
|
||||
|
||||
There's ultimately three types of region here we need to mark:
|
||||
* The prompt (starting from A)
|
||||
* the command (starting from B)
|
||||
* the output (starting from C)
|
||||
|
||||
That intuitively feels a bit like a text attribute almost. Additionally, the
|
||||
prompt should be connected to its subsequent command and output, s.t. we can
|
||||
* Select command output
|
||||
* re-run command
|
||||
|
||||
easily. Supposedly, we could do this by iterating through the whole buffer to
|
||||
find the previous/next {whatever}[[1](#footnote-1)]. Additionally, the prompt
|
||||
needs to be able to contain the status / category, and a `133;D` needs to be
|
||||
able to change the category of the previous prompt/command.
|
||||
|
||||
If we instead do a single mark for each command, from `133;A` to `133;A`, and
|
||||
have sub-points for elements within the command
|
||||
* `133;A` starts a mark on the current line, at the current position.
|
||||
* `133;B` sets the end of the mark to the current position.
|
||||
* `133;C` updates the mark's `commandStart` to the current end, then sets the
|
||||
end of the mark to the current position.
|
||||
* `133;D` updates the mark's `outputStart` to the current end, then sets the end
|
||||
of the mark to the current position. It also updates the category of the mark,
|
||||
if needed.
|
||||
|
||||
Each command then only shows up as a single mark on the scrollbar. Jumping
|
||||
between commands is easy, `scrollToMark` operates on `mark.start`, which is
|
||||
where the prompt started. "Bookmarks", i.e. things started by the user wouldn't
|
||||
have `commandStart` or `outputStart` in them. Getting the text of the command,
|
||||
of the output is easy - it's just the text between sub-points.
|
||||
|
||||
Reflow still sucks though - we'd need to basically iterate over all the marks as
|
||||
we're reflowing, to make sure we put them into the right place in the new
|
||||
buffer. This is annoying and tedious, but shouldn't realistically be a
|
||||
performance problem.
|
||||
|
||||
#### Cmd.exe considerations
|
||||
|
||||
`cmd.exe` is generally a pretty bad shell, and doesn't have a lot of the same
|
||||
hooks that other shells do, that might allow for us to emit the
|
||||
**FTCS_COMMAND_EXECUTED** sequence. However, cmd.exe also doesn't allow
|
||||
multiline prompts, so we can be relatively certain that when the user presses
|
||||
<kbd>enter</kbd>, that's the end of the prompt. We will treat the
|
||||
`autoMarkPrompts` setting (which auto-marks <kbd>enter</kbd>) as the _end of the
|
||||
prompt_. That would at least allow cmd.exe to emit a {command finished}{prompt
|
||||
start}{prompt...}{command start} in the prompt, and have us add the command
|
||||
executed. It is not perfect (we wouldn't be able to get error information), but
|
||||
it does work well enough.
|
||||
|
||||
```cmd
|
||||
PROMPT $e]133;D$e\$e]133;A$e\$e]9;9;$P$e\%PROMPT%$e]133;B$e\
|
||||
```
|
||||
|
||||
## Settings proposals
|
||||
|
||||
The below are the proposed additions to the settings for supporting marks and
|
||||
interacting with them. Some of these have already been added as experimental
|
||||
settings - these would be promoted to no longer be experimental.
|
||||
|
||||
Many of the sub-points on these settings are definitely "Future Consideration"
|
||||
level settings. For example, the `scrollToMark` `"highlight"` property. That one
|
||||
is certainly not something we need to ship with.
|
||||
|
||||
### Actions
|
||||
|
||||
In addition to driving marks via the output, we will also want to support adding
|
||||
marks manually. These can be thought of like "bookmarks" - a user indicated
|
||||
region that means something to the user.
|
||||
|
||||
* [ ] `addMark`: add a mark to the buffer. If there's a selection, place the
|
||||
mark covering at the selection. Otherwise, place the mark on the cursor row.
|
||||
- [x] `color`: a color for the scrollbar mark. (in [#12948])
|
||||
- [ ] `category`: one of `{"prompt", "error", "warning", "success", "info"}`
|
||||
* [ ] `scrollToMark`
|
||||
- [x] `direction`: `["first", "previous", "next", "last"]` (in [#12948])
|
||||
- [ ] `category`: `flags({categories}...)`, default `"all"`. Only scroll to
|
||||
one of the categories specified (e.g. only scroll to the previous error,
|
||||
only the previous prompt, or just any mark)
|
||||
- [ ] [#13449] - `center` or some other setting that controls how the mark is scrolled in.
|
||||
- Maybe `top` (current) /`center` (as proposed) /`nearestEdge` (when
|
||||
scrolling down, put the mark at the bottom line of viewport , up -> top
|
||||
line)?
|
||||
- [ ] [#13455] - `highlight`: `bool`, default true. Display a temporary
|
||||
highlight around the mark when scrolling to it. ("Highlight" != "select")
|
||||
- If the mark has prompt/command/output sections, only select the prompt and command.
|
||||
- If the mark has zero width (i.e. the user just wanted to bookmark a line),
|
||||
then highlight the entire row.
|
||||
* [x] `clearMark`: Remove any marks in the selected region (or at the cursor
|
||||
position) (in [#12948])
|
||||
* [x] `clearAllMarks`: Remove all the marks from the buffer. (in [#12948])
|
||||
|
||||
|
||||
#### Selecting commands & output
|
||||
|
||||
_Inspired by a long weekend of manually copying .csv output from the Terminal to
|
||||
a spreadsheet, only to discover that we rejected [#4588] some years ago._
|
||||
|
||||
* [x] `selectCommand(direction=[prev, next])`: Starting at the active selection
|
||||
anchor, (or the cursor if there's no selection), select the command that
|
||||
starts before/after this point (exclusive). Probably shouldn't wrap around
|
||||
the buffer.
|
||||
* Since this will create a selection including the start of the command,
|
||||
performing this again will select the _next_ command (in whatever
|
||||
direction).
|
||||
* [x] `selectOutput(direction=[prev, next])`: same as above, but with command outputs.
|
||||
|
||||
A convenient workflow might be a `multipleActions([selectOutput(prev),
|
||||
copy()])`, to quickly select the previous commands output.
|
||||
|
||||
### Per-profile settings
|
||||
|
||||
* [x] `autoMarkPrompts`: `bool`, default `false`. (in [#12948])
|
||||
* [ ] `showFindMatchesOnScrollbar`: `bool`, default `true`.
|
||||
* [ ] `showMarksOnScrollbar`: `bool` or `flags({categories}...)`
|
||||
* As an example: `"showMarksOnScrollbar": ["error", "success"]`).
|
||||
* Controls if marks should be displayed on the scrollbar.
|
||||
* If `true`/`"all"`, then all marks are displayed.
|
||||
* If `false`/`"none"`, then no marks are displayed.
|
||||
* If a set of categories are provided, only display marks from those categories.
|
||||
* [x] the bool version is (in [#12948])
|
||||
* [ ] The `flags({categories}...)` version is not done yet.
|
||||
* [ ] `showGutterIcons`, for displaying gutter icons.
|
||||
|
||||
## UX Design
|
||||
|
||||
An example of what colored marks look like:
|
||||
|
||||

|
||||
|
||||
This gif demos both prompt marks and marks for search results:
|
||||
|
||||

|
||||
|
||||
|
||||
### Gutter icons
|
||||
|
||||

|
||||
_An example of what the icons in the VsCode gutter look like_
|
||||
|
||||
### Multiple marks on the same line
|
||||
|
||||
When it comes to displaying marks on the scrollbar, or in the margins, the
|
||||
relative priority of these marks matters. Marks are given the following
|
||||
priority, with errors being the highest priority.
|
||||
* Error
|
||||
* Warning
|
||||
* Success
|
||||
* Prompt
|
||||
* Info (default)
|
||||
|
||||
## Work needed to get marks to v1
|
||||
|
||||
* [x] Clearing the screen leaves marks behind
|
||||
* [x] Make sure `ED2` works to clear/move marks
|
||||
* [x] Same with `ED3`
|
||||
* [x] Same with `cls` / `Clear-Host`
|
||||
* [x] Clear Buffer action too.
|
||||
* [X] Circling doesn't update scrollbar
|
||||
* I think this was fixed in [#14341], or in [#14045]
|
||||
* [ ] Resizing / reflowing marks
|
||||
* [x] marks should be stored in the `TextBuffer`
|
||||
|
||||
## Future Considerations
|
||||
* adding a timestamp for when a line was marked?
|
||||
* adding a comment to the mark. How do we display that comment? a TeachingTip on
|
||||
the scrollbar maybe (actually that's a cool idea)
|
||||
* adding a shape to the mark? Terminal.app marks prompt lines with brackets in
|
||||
the margins
|
||||
* Marks are currently just displayed as "last one in wins", they should have a
|
||||
real sort order
|
||||
* Should the height of a mark on the scrollbar be dependent on font size &
|
||||
buffer height? I think I've got it set to like, a static 2dip, but maybe it
|
||||
should represent the actual height of the row (down to a min 1dip)
|
||||
* [#13455] - highlight a mark when scrolled to with the `scrollToMark` action.
|
||||
This is left as a future consideration to figure out what kind of UI we want
|
||||
here. Do we want to highlight
|
||||
- the prompt?
|
||||
- the whole row of the prompt?
|
||||
- the prompt and the command?
|
||||
- The whole prompt & command & output?
|
||||
* an `addBookmark` action: This one's basically just `addMark`, but opens a prompt
|
||||
(like the window renamer) to add some text as a comment. Automatically
|
||||
populated with the selected text (if there was some).
|
||||
- A dedicated transient pane for displaying non-terminal content might be
|
||||
useful for such a thing.
|
||||
- This might just make more sense as a parameter to `addMark`.
|
||||
* Other ideas for `addMark` parameters:
|
||||
- `icon`: This would require us to better figure out how we display gutter
|
||||
icons. This would probably be like, a _shape_ rather than an arbitrary
|
||||
image.
|
||||
- `note`: a note to stick on the mark, as a comment. Might be more valuable
|
||||
with something like `addBookmark`.
|
||||
|
||||
### Gutter icons
|
||||
|
||||
VsCode implements a set of gutter icons to the left of the buffer lines, to
|
||||
provide a UI element for exposing some quick actions to perform, powered by
|
||||
shell integration.
|
||||
|
||||
Gutter icons don't need to implement app-level actions at all. They _should_ be
|
||||
part of the control. At least, part of the UWP `TermControl`. These are some
|
||||
basic "actions" we could add to that menu. Since these are all attached to a
|
||||
mark anyways, we already know what mark the user interacted with, and where the
|
||||
start/end already is.
|
||||
* Copy command
|
||||
* Copy output
|
||||
* Re-run command
|
||||
* Save as task
|
||||
* Explain this (for errors)
|
||||
|
||||
To allow comments in marks (ala "bookmarks"), we can use
|
||||
the gutter flyout to display the comment, and have the tooltip display that
|
||||
comment.
|
||||
|
||||
This is being left as a future consideration for now. We need to really sit and
|
||||
consider what the UX is like for this.
|
||||
* Do we just stick the gutter icons in the margin/padding?
|
||||
* Have it be a separate space in the "buffer"
|
||||
- If it's in the buffer itself, we can render it with the renderer, which by
|
||||
all accounts, we probably should.
|
||||
|
||||
### Edge cases where these might not work as expected
|
||||
|
||||
Much of the benefits of shell integration come from literal buffer text parsing.
|
||||
This can lead to some rough edge cases, such as:
|
||||
|
||||
* the user presses <kbd>Ctrl+V</kbd><kbd>Escape</kbd> to input an ESC character
|
||||
and the shell displays it as `^[`
|
||||
* the user presses <kbd>Ctrl+V</kbd><kbd>Ctrl+J</kbd> to input an LF character
|
||||
and the shell displays it as a line break
|
||||
* the user presses <kbd>Enter</kbd> within a quoted string and the shell
|
||||
displays a continuation prompt
|
||||
* the user types a command including an exclamation point, and the shell invokes
|
||||
history expansion and echoes the result of expansion before it runs the
|
||||
command
|
||||
* The user has a prompt with a right-aligned status, ala
|
||||

|
||||
|
||||
In these cases, the effects of shell integration will likely not work as
|
||||
intended. There are various possible solutions that are being explored. We might
|
||||
want to in the future also use [VsCode's extension to the FTCS sequences] to
|
||||
enable the shell to tell the terminal the literal resulting commandline.
|
||||
|
||||
There's been [other proposals] to extend shell integration features as well.
|
||||
|
||||
### Rejected ideas
|
||||
|
||||
There was originally some discussion as to whether this is a design that should
|
||||
be unified with generic pattern matchers. Something like the URL detection,
|
||||
which identifies part of the buffer and then "marks" it. Prototypes for both of
|
||||
these features are going in very different directions, however. Likely best to
|
||||
leave them separate.
|
||||
|
||||
## Resources
|
||||
|
||||
### Other related issues
|
||||
|
||||
Not necessarily marks related, but could happily leverage this functionality.
|
||||
|
||||
* [#5916] and [#12366], which are likely to be combined into a single thread
|
||||
- Imagine a trigger that automatically detects `error:.*` and then marks the line
|
||||
* [#9583]
|
||||
- Imagine selecting some text, colorizing & marking it all at once
|
||||
- `addMark(selection:false)`, above, was inspired by this.
|
||||
* [#7561] (and broadly, [#3920])
|
||||
- Search results should maybe show up here on the scrollbar too.
|
||||
* [#13455]
|
||||
* [#13449]
|
||||
* [#4588]
|
||||
* [#14754] - A "sticky header" for the `TermControl` that could display the
|
||||
previous command at the top of the buffer.
|
||||
* [#2226] - a scrollable "minimap" in te scrollbar, as opposed to marks
|
||||
|
||||
### Relevant external docs
|
||||
* **GREAT** summary of the state of the ecosystem: https://gitlab.freedesktop.org/terminal-wg/specifications/-/issues/28
|
||||
* https://iterm2.com/documentation-escape-codes.html
|
||||
- `OSC 1337 ; SetMark ST` under "Set Mark"
|
||||
- under "Shell Integration/FinalTerm
|
||||
* https://support.apple.com/en-ca/guide/terminal/trml135fbc26/mac
|
||||
- discusses auto-marked lines on `enter`/`^C`/`^D`
|
||||
- allows bookmarking lines with selection
|
||||
- bookmarks can have a name (maybe not super important)
|
||||
* [How to Use Marks in OS X’s Terminal for Easier Navigation](https://www.howtogeek.com/256548/how-to-use-marks-in-os-xs-terminal-for-easier-navigation/)
|
||||
* [Terminal: The ‘\[‘ Marks the Spot](https://scriptingosx.com/2017/03/terminal-the-marks-the-spot/)
|
||||
* Thread with VsCode (xterm.js) implementation notes: https://github.com/microsoft/terminal/issues/1527#issuecomment-1076455642
|
||||
* [xterm.js prompt markup sequences](https://github.com/microsoft/vscode/blob/39cc1e1c42b2e53e83b1846c2857ca194848cc1d/src/vs/workbench/contrib/terminal/browser/xterm/shellIntegrationAddon.ts#L50-L52)
|
||||
* [VsCode command tracking release notes](https://code.visualstudio.com/updates/v1_22#_command-tracking), also [Terminal shell integration](https://code.visualstudio.com/updates/v1_65#_terminal-shell-integration)
|
||||
* ConEMU:
|
||||
Sequence | Description
|
||||
-- | --
|
||||
ESC ] 9 ; 12 ST | Let ConEmu treat current cursor position as prompt start. Useful with PS1.
|
||||
|
||||
* https://iterm2.com/documentation-one-page.html#documentation-triggers.html"
|
||||
|
||||
### Footnotes
|
||||
|
||||
<a name="footnote-1"><a>[1]: Intuitively, this feels prohibitively expensive,
|
||||
but you'd be mistaken.
|
||||
|
||||
An average device right now (I mean something that was alright about 5 years
|
||||
ago, like an 8700k with regular DDR4) does about 4GB/s of random, un-cached
|
||||
memory access. While low-end devices are probably a bit slower, I think 4GB/s is
|
||||
a good estimate regardless. That's because non-random memory access is way way
|
||||
faster than that at around 20GB/s (DDR4 2400 - what most laptops had for the
|
||||
last decade).
|
||||
|
||||
Assuming a 120 column * 32k line buffer (our current maximum), the buffer would
|
||||
be about 21MB large. Going through the entire buffer linearly at 20GB/s would
|
||||
take just 1ms (including all text and metadata). If we assume that each row has
|
||||
a mark, that marks are 36 byte large and assuming the worst case of random
|
||||
access, we can go through all 32k within about 0.3ms.
|
||||
|
||||
|
||||
|
||||
_(Thanks lhecker for these notes)_
|
||||
|
||||
|
||||
[#1527]: https://github.com/microsoft/terminal/issues/1527
|
||||
[#12948]: https://github.com/microsoft/terminal/issues/12948
|
||||
[#1527]: https://github.com/microsoft/terminal/issues/1527
|
||||
[#6232]: https://github.com/microsoft/terminal/issues/6232
|
||||
[#2226]: https://github.com/microsoft/terminal/issues/2226
|
||||
[#12948]: https://github.com/microsoft/terminal/issues/12948
|
||||
[#13163]: https://github.com/microsoft/terminal/issues/13163
|
||||
[#12948]: https://github.com/microsoft/terminal/issues/12948
|
||||
[#12948]: https://github.com/microsoft/terminal/issues/12948
|
||||
[#12948]: https://github.com/microsoft/terminal/issues/12948
|
||||
[#13455]: https://github.com/microsoft/terminal/issues/13455
|
||||
[#13449]: https://github.com/microsoft/terminal/issues/13449
|
||||
[#12948]: https://github.com/microsoft/terminal/issues/12948
|
||||
[#12948]: https://github.com/microsoft/terminal/issues/12948
|
||||
[#4588]: https://github.com/microsoft/terminal/issues/4588
|
||||
[#5804]: https://github.com/microsoft/terminal/issues/5804
|
||||
[#12948]: https://github.com/microsoft/terminal/issues/12948
|
||||
[#12948]: https://github.com/microsoft/terminal/issues/12948
|
||||
[#13455]: https://github.com/microsoft/terminal/issues/13455
|
||||
[#5916]: https://github.com/microsoft/terminal/issues/5916
|
||||
[#12366]: https://github.com/microsoft/terminal/issues/12366
|
||||
[#9583]: https://github.com/microsoft/terminal/issues/9583
|
||||
[#7561]: https://github.com/microsoft/terminal/issues/7561
|
||||
[#3920]: https://github.com/microsoft/terminal/issues/3920
|
||||
[#13455]: https://github.com/microsoft/terminal/issues/13455
|
||||
[#13449]: https://github.com/microsoft/terminal/issues/13449
|
||||
[#4588]: https://github.com/microsoft/terminal/issues/4588
|
||||
[#14341]: https://github.com/microsoft/terminal/issues/14341
|
||||
[#14045]: https://github.com/microsoft/terminal/issues/14045
|
||||
[#14754]: https://github.com/microsoft/terminal/issues/14754
|
||||
[#14341]: https://github.com/microsoft/terminal/issues/14341
|
||||
|
||||
[VsCode's extension to the FTCS sequences]: https://code.visualstudio.com/docs/terminal/shell-integration#_vs-code-custom-sequences-osc-633-st
|
||||
|
||||
[other proposals]: https://gitlab.freedesktop.org/terminal-wg/specifications/-/merge_requests/6#f6de1e5703f5806d0821d92b0274e895c4b6d850
|
||||
BIN
doc/specs/#11000 - Marks/ftcs-diagram.png
Normal file
BIN
doc/specs/#11000 - Marks/ftcs-diagram.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 201 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 38 KiB |
@@ -1021,6 +1021,16 @@ namespace winrt::TerminalApp::implementation
|
||||
args.Handled(true);
|
||||
}
|
||||
|
||||
void TerminalPage::_HandleDisplayWorkingDirectory(const IInspectable& /*sender*/,
|
||||
const ActionEventArgs& args)
|
||||
{
|
||||
if (_settings.GlobalSettings().DebugFeaturesEnabled())
|
||||
{
|
||||
ShowTerminalWorkingDirectory();
|
||||
args.Handled(true);
|
||||
}
|
||||
}
|
||||
|
||||
void TerminalPage::_HandleSearchForText(const IInspectable& /*sender*/,
|
||||
const ActionEventArgs& args)
|
||||
{
|
||||
|
||||
@@ -216,7 +216,7 @@
|
||||
</data>
|
||||
<data name="SearchWebText" xml:space="preserve">
|
||||
<value>Web Search</value>
|
||||
</data>
|
||||
</data>
|
||||
<data name="TabColorChoose" xml:space="preserve">
|
||||
<value>Color...</value>
|
||||
</data>
|
||||
@@ -836,4 +836,8 @@
|
||||
<data name="MoveTabToNewWindowToolTip" xml:space="preserve">
|
||||
<value>Moves tab to a new window </value>
|
||||
</data>
|
||||
</root>
|
||||
<data name="RunAsAdminFlyout.Text" xml:space="preserve">
|
||||
<value>Run as Administrator</value>
|
||||
<comment>This text is displayed on context menu for profile entries in add new tab button.</comment>
|
||||
</data>
|
||||
</root>
|
||||
@@ -1057,6 +1057,15 @@ namespace winrt::TerminalApp::implementation
|
||||
}
|
||||
});
|
||||
|
||||
// Using the static method on the base class seems to do what we want in terms of placement.
|
||||
WUX::Controls::Primitives::FlyoutBase::SetAttachedFlyout(profileMenuItem, _CreateRunAsAdminFlyout(profileIndex));
|
||||
|
||||
// Since we are not setting the ContextFlyout property of the item we have to handle the ContextRequested event
|
||||
// and rely on the base class to show our menu.
|
||||
profileMenuItem.ContextRequested([profileMenuItem](auto&&, auto&&) {
|
||||
WUX::Controls::Primitives::FlyoutBase::ShowAttachedFlyout(profileMenuItem);
|
||||
});
|
||||
|
||||
return profileMenuItem;
|
||||
}
|
||||
|
||||
@@ -4113,6 +4122,33 @@ namespace winrt::TerminalApp::implementation
|
||||
}
|
||||
}
|
||||
|
||||
winrt::fire_and_forget TerminalPage::ShowTerminalWorkingDirectory()
|
||||
{
|
||||
auto weakThis{ get_weak() };
|
||||
co_await wil::resume_foreground(Dispatcher());
|
||||
if (auto page{ weakThis.get() })
|
||||
{
|
||||
// If we haven't ever loaded the TeachingTip, then do so now and
|
||||
// create the toast for it.
|
||||
if (page->_windowCwdToast == nullptr)
|
||||
{
|
||||
if (auto tip{ page->FindName(L"WindowCwdToast").try_as<MUX::Controls::TeachingTip>() })
|
||||
{
|
||||
page->_windowCwdToast = std::make_shared<Toast>(tip);
|
||||
// Make sure to use the weak ref when setting up this
|
||||
// callback.
|
||||
tip.Closed({ page->get_weak(), &TerminalPage::_FocusActiveControl });
|
||||
}
|
||||
}
|
||||
_UpdateTeachingTipTheme(WindowCwdToast().try_as<winrt::Windows::UI::Xaml::FrameworkElement>());
|
||||
|
||||
if (page->_windowCwdToast != nullptr)
|
||||
{
|
||||
page->_windowCwdToast->Open();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Method Description:
|
||||
// - Called when the user hits the "Ok" button on the WindowRenamer TeachingTip.
|
||||
// - Will raise an event that will bubble up to the monarch, asking if this
|
||||
@@ -4940,4 +4976,42 @@ namespace winrt::TerminalApp::implementation
|
||||
// _RemoveTab will make sure to null out the _stashed.draggedTab
|
||||
_RemoveTab(*_stashed.draggedTab);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a sub flyout menu for profile items in the split button menu that when clicked will show a menu item for
|
||||
/// Run as Administrator
|
||||
/// </summary>
|
||||
/// <param name="profileIndex">The index for the profileMenuItem</param>
|
||||
/// <returns>MenuFlyout that will show when the context is request on a profileMenuItem</returns>
|
||||
WUX::Controls::MenuFlyout TerminalPage::_CreateRunAsAdminFlyout(int profileIndex)
|
||||
{
|
||||
// Create the MenuFlyout and set its placement
|
||||
WUX::Controls::MenuFlyout profileMenuItemFlyout{};
|
||||
profileMenuItemFlyout.Placement(WUX::Controls::Primitives::FlyoutPlacementMode::BottomEdgeAlignedRight);
|
||||
|
||||
// Create the menu item and an icon to use in the menu
|
||||
WUX::Controls::MenuFlyoutItem runAsAdminItem{};
|
||||
WUX::Controls::FontIcon adminShieldIcon{};
|
||||
|
||||
adminShieldIcon.Glyph(L"\xEA18");
|
||||
adminShieldIcon.FontFamily(Media::FontFamily{ L"Segoe Fluent Icons, Segoe MDL2 Assets" });
|
||||
|
||||
runAsAdminItem.Icon(adminShieldIcon);
|
||||
runAsAdminItem.Text(RS_(L"RunAsAdminFlyout/Text"));
|
||||
|
||||
// Click handler for the flyout item
|
||||
runAsAdminItem.Click([profileIndex, weakThis{ get_weak() }](auto&&, auto&&) {
|
||||
if (auto page{ weakThis.get() })
|
||||
{
|
||||
NewTerminalArgs args{ profileIndex };
|
||||
args.Elevate(true);
|
||||
page->_OpenNewTerminalViaDropdown(args);
|
||||
}
|
||||
});
|
||||
|
||||
profileMenuItemFlyout.Items().Append(runAsAdminItem);
|
||||
|
||||
return profileMenuItemFlyout;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -147,6 +147,7 @@ namespace winrt::TerminalApp::implementation
|
||||
|
||||
winrt::fire_and_forget IdentifyWindow();
|
||||
winrt::fire_and_forget RenameFailed();
|
||||
winrt::fire_and_forget ShowTerminalWorkingDirectory();
|
||||
|
||||
winrt::fire_and_forget ProcessStartupActions(Windows::Foundation::Collections::IVector<Microsoft::Terminal::Settings::Model::ActionAndArgs> actions,
|
||||
const bool initial,
|
||||
@@ -256,6 +257,7 @@ namespace winrt::TerminalApp::implementation
|
||||
|
||||
std::shared_ptr<Toast> _windowIdToast{ nullptr };
|
||||
std::shared_ptr<Toast> _windowRenameFailedToast{ nullptr };
|
||||
std::shared_ptr<Toast> _windowCwdToast{ nullptr };
|
||||
|
||||
winrt::Windows::UI::Xaml::Controls::TextBox::LayoutUpdated_revoker _renamerLayoutUpdatedRevoker;
|
||||
int _renamerLayoutCount{ 0 };
|
||||
@@ -530,7 +532,7 @@ namespace winrt::TerminalApp::implementation
|
||||
void _ContextMenuOpened(const IInspectable& sender, const IInspectable& args);
|
||||
void _SelectionMenuOpened(const IInspectable& sender, const IInspectable& args);
|
||||
void _PopulateContextMenu(const IInspectable& sender, const bool withSelection);
|
||||
|
||||
winrt::Windows::UI::Xaml::Controls::MenuFlyout _CreateRunAsAdminFlyout(int profileIndex);
|
||||
#pragma region ActionHandlers
|
||||
// These are all defined in AppActionHandlers.cpp
|
||||
#define ON_ALL_ACTIONS(action) DECLARE_ACTION_HANDLER(action);
|
||||
|
||||
@@ -205,5 +205,11 @@
|
||||
Text="{x:Bind WindowProperties.WindowName, Mode=OneWay}" />
|
||||
</mux:TeachingTip.Content>
|
||||
</mux:TeachingTip>
|
||||
|
||||
<mux:TeachingTip x:Name="WindowCwdToast"
|
||||
x:Uid="WindowCwdToast"
|
||||
Title="{x:Bind WindowProperties.VirtualWorkingDirectory, Mode=OneWay}"
|
||||
x:Load="False"
|
||||
IsLightDismissEnabled="True" />
|
||||
</Grid>
|
||||
</Page>
|
||||
|
||||
@@ -72,6 +72,7 @@ static constexpr std::string_view IdentifyWindowKey{ "identifyWindow" };
|
||||
static constexpr std::string_view IdentifyWindowsKey{ "identifyWindows" };
|
||||
static constexpr std::string_view RenameWindowKey{ "renameWindow" };
|
||||
static constexpr std::string_view OpenWindowRenamerKey{ "openWindowRenamer" };
|
||||
static constexpr std::string_view DisplayWorkingDirectoryKey{ "debugTerminalCwd" };
|
||||
static constexpr std::string_view SearchForTextKey{ "searchWeb" };
|
||||
static constexpr std::string_view GlobalSummonKey{ "globalSummon" };
|
||||
static constexpr std::string_view QuakeModeKey{ "quakeMode" };
|
||||
@@ -404,6 +405,7 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation
|
||||
{ ShortcutAction::IdentifyWindows, RS_(L"IdentifyWindowsCommandKey") },
|
||||
{ ShortcutAction::RenameWindow, RS_(L"ResetWindowNameCommandKey") },
|
||||
{ ShortcutAction::OpenWindowRenamer, RS_(L"OpenWindowRenamerCommandKey") },
|
||||
{ ShortcutAction::DisplayWorkingDirectory, RS_(L"DisplayWorkingDirectoryCommandKey") },
|
||||
{ ShortcutAction::GlobalSummon, MustGenerate },
|
||||
{ ShortcutAction::SearchForText, MustGenerate },
|
||||
{ ShortcutAction::QuakeMode, RS_(L"QuakeModeCommandKey") },
|
||||
|
||||
@@ -85,6 +85,7 @@
|
||||
ON_ALL_ACTIONS(IdentifyWindows) \
|
||||
ON_ALL_ACTIONS(RenameWindow) \
|
||||
ON_ALL_ACTIONS(OpenWindowRenamer) \
|
||||
ON_ALL_ACTIONS(DisplayWorkingDirectory) \
|
||||
ON_ALL_ACTIONS(SearchForText) \
|
||||
ON_ALL_ACTIONS(GlobalSummon) \
|
||||
ON_ALL_ACTIONS(QuakeMode) \
|
||||
|
||||
@@ -511,6 +511,9 @@
|
||||
<data name="OpenWindowRenamerCommandKey" xml:space="preserve">
|
||||
<value>Rename window...</value>
|
||||
</data>
|
||||
<data name="DisplayWorkingDirectoryCommandKey" xml:space="preserve">
|
||||
<value>Display Terminal's current working directory</value>
|
||||
</data>
|
||||
<data name="GlobalSummonCommandKey" xml:space="preserve">
|
||||
<value>Show/Hide the Terminal window</value>
|
||||
</data>
|
||||
|
||||
@@ -962,8 +962,6 @@ winrt::fire_and_forget AppHost::_peasantNotifyActivateWindow()
|
||||
// - The window layout as a json string.
|
||||
winrt::Windows::Foundation::IAsyncOperation<winrt::hstring> AppHost::_GetWindowLayoutAsync()
|
||||
{
|
||||
winrt::apartment_context peasant_thread;
|
||||
|
||||
winrt::hstring layoutJson = L"";
|
||||
// Use the main thread since we are accessing controls.
|
||||
co_await wil::resume_foreground(_windowLogic.GetRoot().Dispatcher());
|
||||
@@ -974,9 +972,6 @@ winrt::Windows::Foundation::IAsyncOperation<winrt::hstring> AppHost::_GetWindowL
|
||||
}
|
||||
CATCH_LOG()
|
||||
|
||||
// go back to give the result to the peasant.
|
||||
co_await peasant_thread;
|
||||
|
||||
co_return layoutJson;
|
||||
}
|
||||
|
||||
@@ -1052,9 +1047,6 @@ void AppHost::_DisplayWindowId(const winrt::Windows::Foundation::IInspectable& /
|
||||
winrt::fire_and_forget AppHost::_RenameWindowRequested(const winrt::Windows::Foundation::IInspectable /*sender*/,
|
||||
const winrt::TerminalApp::RenameWindowRequestedArgs args)
|
||||
{
|
||||
// Capture calling context.
|
||||
winrt::apartment_context ui_thread;
|
||||
|
||||
// Switch to the BG thread - anything x-proc must happen on a BG thread
|
||||
co_await winrt::resume_background();
|
||||
|
||||
@@ -1064,12 +1056,9 @@ winrt::fire_and_forget AppHost::_RenameWindowRequested(const winrt::Windows::Fou
|
||||
|
||||
_peasant.RequestRename(requestArgs);
|
||||
|
||||
// Switch back to the UI thread. Setting the WindowName needs to happen
|
||||
// on the UI thread, because it'll raise a PropertyChanged event
|
||||
co_await ui_thread;
|
||||
|
||||
if (requestArgs.Succeeded())
|
||||
{
|
||||
co_await wil::resume_foreground(_windowLogic.GetRoot().Dispatcher());
|
||||
_windowLogic.WindowName(args.ProposedName());
|
||||
}
|
||||
else
|
||||
|
||||
@@ -18,7 +18,7 @@ enum TraceKeywords
|
||||
Output = 0x008, // _DBGOUTPUT
|
||||
General = 0x100,
|
||||
Input = 0x200,
|
||||
API = 0x400,
|
||||
//API = 0x400, // No longer used
|
||||
UIA = 0x800,
|
||||
CookedRead = 0x1000,
|
||||
ConsoleAttachDetach = 0x2000,
|
||||
@@ -26,241 +26,8 @@ enum TraceKeywords
|
||||
};
|
||||
DEFINE_ENUM_FLAG_OPERATORS(TraceKeywords);
|
||||
|
||||
// Routine Description:
|
||||
// - Creates a tracing object to assist with automatically firing a stop event
|
||||
// when this object goes out of scope.
|
||||
// - Give it back to the caller and they will release it when the event period is over.
|
||||
// Arguments:
|
||||
// - onExit - Function to process when the object is destroyed (on exit)
|
||||
Tracing::Tracing(std::function<void()> onExit) :
|
||||
_onExit(onExit)
|
||||
{
|
||||
}
|
||||
|
||||
// Routine Description:
|
||||
// - Destructs a tracing object, running any on exit routine, if necessary.
|
||||
Tracing::~Tracing()
|
||||
{
|
||||
if (_onExit)
|
||||
{
|
||||
_onExit();
|
||||
}
|
||||
}
|
||||
|
||||
// Routine Description:
|
||||
// - Provides generic tracing for all API call types in the form of
|
||||
// start/stop period events for timing and region-of-interest purposes
|
||||
// while doing performance analysis.
|
||||
// Arguments:
|
||||
// - result - Reference to the area where the result code from the Api call
|
||||
// will be stored for use in the stop event.
|
||||
// - traceName - The name of the API call to list in the trace details
|
||||
// Return Value:
|
||||
// - An object for the caller to hold until the API call is complete.
|
||||
// Then destroy it to signal that the call is over so the stop trace can be written.
|
||||
Tracing Tracing::s_TraceApiCall(const NTSTATUS result, PCSTR traceName)
|
||||
{
|
||||
// clang-format off
|
||||
TraceLoggingWrite(
|
||||
g_hConhostV2EventTraceProvider,
|
||||
"ApiCall",
|
||||
TraceLoggingString(traceName, "ApiName"),
|
||||
TraceLoggingOpcode(WINEVENT_OPCODE_START),
|
||||
TraceLoggingLevel(WINEVENT_LEVEL_VERBOSE),
|
||||
TraceLoggingKeyword(TIL_KEYWORD_TRACE),
|
||||
TraceLoggingKeyword(TraceKeywords::API));
|
||||
|
||||
return Tracing([traceName, &result] {
|
||||
TraceLoggingWrite(
|
||||
g_hConhostV2EventTraceProvider,
|
||||
"ApiCall",
|
||||
TraceLoggingString(traceName, "ApiName"),
|
||||
TraceLoggingHResult(result, "Result"),
|
||||
TraceLoggingOpcode(WINEVENT_OPCODE_STOP),
|
||||
TraceLoggingLevel(WINEVENT_LEVEL_VERBOSE),
|
||||
TraceLoggingKeyword(TIL_KEYWORD_TRACE),
|
||||
TraceLoggingKeyword(TraceKeywords::API));
|
||||
});
|
||||
// clang-format on
|
||||
}
|
||||
|
||||
ULONG Tracing::s_ulDebugFlag = 0x0;
|
||||
|
||||
void Tracing::s_TraceApi(const NTSTATUS status, const CONSOLE_GETLARGESTWINDOWSIZE_MSG* const a)
|
||||
{
|
||||
TraceLoggingWrite(
|
||||
g_hConhostV2EventTraceProvider,
|
||||
"API_GetLargestWindowSize",
|
||||
TraceLoggingHexInt32(status, "ResultCode"),
|
||||
TraceLoggingInt32(a->Size.X, "MaxWindowWidthInChars"),
|
||||
TraceLoggingInt32(a->Size.Y, "MaxWindowHeightInChars"),
|
||||
TraceLoggingLevel(WINEVENT_LEVEL_VERBOSE),
|
||||
TraceLoggingKeyword(TIL_KEYWORD_TRACE),
|
||||
TraceLoggingKeyword(TraceKeywords::API));
|
||||
}
|
||||
|
||||
void Tracing::s_TraceApi(const NTSTATUS status, const CONSOLE_SCREENBUFFERINFO_MSG* const a, const bool fSet)
|
||||
{
|
||||
// Duplicate copies required by TraceLogging documentation ("don't get cute" examples)
|
||||
// Using logic inside these macros can make problems. Do all logic outside macros.
|
||||
|
||||
if (fSet)
|
||||
{
|
||||
TraceLoggingWrite(
|
||||
g_hConhostV2EventTraceProvider,
|
||||
"API_SetConsoleScreenBufferInfo",
|
||||
TraceLoggingHexInt32(status, "ResultCode"),
|
||||
TraceLoggingInt32(a->Size.X, "BufferWidthInChars"),
|
||||
TraceLoggingInt32(a->Size.Y, "BufferHeightInChars"),
|
||||
TraceLoggingInt32(a->CurrentWindowSize.X, "WindowWidthInChars"),
|
||||
TraceLoggingInt32(a->CurrentWindowSize.Y, "WindowHeightInChars"),
|
||||
TraceLoggingInt32(a->MaximumWindowSize.X, "MaxWindowWidthInChars"),
|
||||
TraceLoggingInt32(a->MaximumWindowSize.Y, "MaxWindowHeightInChars"),
|
||||
TraceLoggingLevel(WINEVENT_LEVEL_VERBOSE),
|
||||
TraceLoggingKeyword(TIL_KEYWORD_TRACE),
|
||||
TraceLoggingKeyword(TraceKeywords::API));
|
||||
}
|
||||
else
|
||||
{
|
||||
TraceLoggingWrite(
|
||||
g_hConhostV2EventTraceProvider,
|
||||
"API_GetConsoleScreenBufferInfo",
|
||||
TraceLoggingHexInt32(status, "ResultCode"),
|
||||
TraceLoggingInt32(a->Size.X, "BufferWidthInChars"),
|
||||
TraceLoggingInt32(a->Size.Y, "BufferHeightInChars"),
|
||||
TraceLoggingInt32(a->CurrentWindowSize.X, "WindowWidthInChars"),
|
||||
TraceLoggingInt32(a->CurrentWindowSize.Y, "WindowHeightInChars"),
|
||||
TraceLoggingInt32(a->MaximumWindowSize.X, "MaxWindowWidthInChars"),
|
||||
TraceLoggingInt32(a->MaximumWindowSize.Y, "MaxWindowHeightInChars"),
|
||||
TraceLoggingLevel(WINEVENT_LEVEL_VERBOSE),
|
||||
TraceLoggingKeyword(TIL_KEYWORD_TRACE),
|
||||
TraceLoggingKeyword(TraceKeywords::API));
|
||||
}
|
||||
}
|
||||
|
||||
void Tracing::s_TraceApi(const NTSTATUS status, const CONSOLE_SETSCREENBUFFERSIZE_MSG* const a)
|
||||
{
|
||||
TraceLoggingWrite(
|
||||
g_hConhostV2EventTraceProvider,
|
||||
"API_SetConsoleScreenBufferSize",
|
||||
TraceLoggingHexInt32(status, "ResultCode"),
|
||||
TraceLoggingInt32(a->Size.X, "BufferWidthInChars"),
|
||||
TraceLoggingInt32(a->Size.Y, "BufferHeightInChars"),
|
||||
TraceLoggingLevel(WINEVENT_LEVEL_VERBOSE),
|
||||
TraceLoggingKeyword(TIL_KEYWORD_TRACE),
|
||||
TraceLoggingKeyword(TraceKeywords::API));
|
||||
}
|
||||
|
||||
void Tracing::s_TraceApi(const NTSTATUS status, const CONSOLE_SETWINDOWINFO_MSG* const a)
|
||||
{
|
||||
TraceLoggingWrite(
|
||||
g_hConhostV2EventTraceProvider,
|
||||
"API_SetConsoleWindowInfo",
|
||||
TraceLoggingHexInt32(status, "ResultCode"),
|
||||
TraceLoggingBool(a->Absolute, "IsWindowRectAbsolute"),
|
||||
TraceLoggingInt32(a->Window.Left, "WindowRectLeft"),
|
||||
TraceLoggingInt32(a->Window.Right, "WindowRectRight"),
|
||||
TraceLoggingInt32(a->Window.Top, "WindowRectTop"),
|
||||
TraceLoggingInt32(a->Window.Bottom, "WindowRectBottom"),
|
||||
TraceLoggingLevel(WINEVENT_LEVEL_VERBOSE),
|
||||
TraceLoggingKeyword(TIL_KEYWORD_TRACE),
|
||||
TraceLoggingKeyword(TraceKeywords::API));
|
||||
}
|
||||
|
||||
void Tracing::s_TraceApi(_In_ const void* const buffer, const CONSOLE_WRITECONSOLE_MSG* const a)
|
||||
{
|
||||
// clang-format off
|
||||
if (a->Unicode)
|
||||
{
|
||||
const auto buf = static_cast<const wchar_t* const>(buffer);
|
||||
TraceLoggingWrite(
|
||||
g_hConhostV2EventTraceProvider,
|
||||
"API_WriteConsole",
|
||||
TraceLoggingBoolean(a->Unicode, "Unicode"),
|
||||
TraceLoggingUInt32(a->NumBytes, "NumBytes"),
|
||||
TraceLoggingCountedWideString(buf, static_cast<UINT16>(a->NumBytes / sizeof(wchar_t)), "input buffer"),
|
||||
TraceLoggingLevel(WINEVENT_LEVEL_VERBOSE),
|
||||
TraceLoggingKeyword(TIL_KEYWORD_TRACE),
|
||||
TraceLoggingKeyword(TraceKeywords::API));
|
||||
}
|
||||
else
|
||||
{
|
||||
const auto buf = static_cast<const char* const>(buffer);
|
||||
TraceLoggingWrite(
|
||||
g_hConhostV2EventTraceProvider,
|
||||
"API_WriteConsole",
|
||||
TraceLoggingBoolean(a->Unicode, "Unicode"),
|
||||
TraceLoggingUInt32(a->NumBytes, "NumBytes"),
|
||||
TraceLoggingCountedString(buf, static_cast<UINT16>(a->NumBytes / sizeof(char)), "input buffer"),
|
||||
TraceLoggingLevel(WINEVENT_LEVEL_VERBOSE),
|
||||
TraceLoggingKeyword(TIL_KEYWORD_TRACE),
|
||||
TraceLoggingKeyword(TraceKeywords::API));
|
||||
}
|
||||
// clang-format on
|
||||
}
|
||||
|
||||
void Tracing::s_TraceApi(const CONSOLE_SCREENBUFFERINFO_MSG* const a)
|
||||
{
|
||||
TraceLoggingWrite(
|
||||
g_hConhostV2EventTraceProvider,
|
||||
"API_GetConsoleScreenBufferInfo",
|
||||
TraceLoggingInt16(a->Size.X, "Size.X"),
|
||||
TraceLoggingInt16(a->Size.Y, "Size.Y"),
|
||||
TraceLoggingInt16(a->CursorPosition.X, "CursorPosition.X"),
|
||||
TraceLoggingInt16(a->CursorPosition.Y, "CursorPosition.Y"),
|
||||
TraceLoggingInt16(a->ScrollPosition.X, "ScrollPosition.X"),
|
||||
TraceLoggingInt16(a->ScrollPosition.Y, "ScrollPosition.Y"),
|
||||
TraceLoggingHexUInt16(a->Attributes, "Attributes"),
|
||||
TraceLoggingInt16(a->CurrentWindowSize.X, "CurrentWindowSize.X"),
|
||||
TraceLoggingInt16(a->CurrentWindowSize.Y, "CurrentWindowSize.Y"),
|
||||
TraceLoggingInt16(a->MaximumWindowSize.X, "MaximumWindowSize.X"),
|
||||
TraceLoggingInt16(a->MaximumWindowSize.Y, "MaximumWindowSize.Y"),
|
||||
TraceLoggingHexUInt16(a->PopupAttributes, "PopupAttributes"),
|
||||
TraceLoggingBoolean(a->FullscreenSupported, "FullscreenSupported"),
|
||||
TraceLoggingHexUInt32FixedArray((UINT32 const*)a->ColorTable, _countof(a->ColorTable), "ColorTable"),
|
||||
TraceLoggingLevel(WINEVENT_LEVEL_VERBOSE),
|
||||
TraceLoggingKeyword(TIL_KEYWORD_TRACE),
|
||||
TraceLoggingKeyword(TraceKeywords::API));
|
||||
static_assert(sizeof(UINT32) == sizeof(*a->ColorTable), "a->ColorTable");
|
||||
}
|
||||
|
||||
void Tracing::s_TraceApi(const CONSOLE_MODE_MSG* const a, const std::wstring_view handleType)
|
||||
{
|
||||
TraceLoggingWrite(
|
||||
g_hConhostV2EventTraceProvider,
|
||||
"API_GetConsoleMode",
|
||||
TraceLoggingHexUInt32(a->Mode, "Mode"),
|
||||
TraceLoggingCountedWideString(handleType.data(), gsl::narrow_cast<ULONG>(handleType.size()), "Handle type"),
|
||||
TraceLoggingLevel(WINEVENT_LEVEL_VERBOSE),
|
||||
TraceLoggingKeyword(TIL_KEYWORD_TRACE),
|
||||
TraceLoggingKeyword(TraceKeywords::API));
|
||||
}
|
||||
|
||||
void Tracing::s_TraceApi(const CONSOLE_SETTEXTATTRIBUTE_MSG* const a)
|
||||
{
|
||||
TraceLoggingWrite(
|
||||
g_hConhostV2EventTraceProvider,
|
||||
"API_SetConsoleTextAttribute",
|
||||
TraceLoggingHexUInt16(a->Attributes, "Attributes"),
|
||||
TraceLoggingLevel(WINEVENT_LEVEL_VERBOSE),
|
||||
TraceLoggingKeyword(TIL_KEYWORD_TRACE),
|
||||
TraceLoggingKeyword(TraceKeywords::API));
|
||||
}
|
||||
|
||||
void Tracing::s_TraceApi(const CONSOLE_WRITECONSOLEOUTPUTSTRING_MSG* const a)
|
||||
{
|
||||
TraceLoggingWrite(
|
||||
g_hConhostV2EventTraceProvider,
|
||||
"API_WriteConsoleOutput",
|
||||
TraceLoggingInt16(a->WriteCoord.X, "WriteCoord.X"),
|
||||
TraceLoggingInt16(a->WriteCoord.Y, "WriteCoord.Y"),
|
||||
TraceLoggingHexUInt32(a->StringType, "StringType"),
|
||||
TraceLoggingUInt32(a->NumRecords, "NumRecords"),
|
||||
TraceLoggingLevel(WINEVENT_LEVEL_VERBOSE),
|
||||
TraceLoggingKeyword(TIL_KEYWORD_TRACE),
|
||||
TraceLoggingKeyword(TraceKeywords::API));
|
||||
}
|
||||
|
||||
void Tracing::s_TraceWindowViewport(const Viewport& viewport)
|
||||
{
|
||||
TraceLoggingWrite(
|
||||
@@ -413,10 +180,10 @@ void Tracing::s_TraceCookedRead(_In_ ConsoleProcessHandle* const pConsoleProcess
|
||||
TraceLoggingWrite(
|
||||
g_hConhostV2EventTraceProvider,
|
||||
"CookedRead",
|
||||
TraceLoggingPid(pConsoleProcessHandle->dwProcessId, "AttachedProcessId"),
|
||||
TraceLoggingCountedWideString(pwchCookedBuffer, cchCookedBufferLength, "ReadBuffer"),
|
||||
TraceLoggingULong(cchCookedBufferLength, "ReadBufferLength"),
|
||||
TraceLoggingUInt32(pConsoleProcessHandle->dwProcessId, "AttachedProcessId"),
|
||||
TraceLoggingUInt64(pConsoleProcessHandle->GetProcessCreationTime(), "AttachedProcessCreationTime"),
|
||||
TraceLoggingFileTime(pConsoleProcessHandle->GetProcessCreationTime(), "AttachedProcessCreationTime"),
|
||||
TraceLoggingKeyword(TIL_KEYWORD_TRACE),
|
||||
TraceLoggingKeyword(TraceKeywords::CookedRead));
|
||||
}
|
||||
@@ -431,8 +198,8 @@ void Tracing::s_TraceConsoleAttachDetach(_In_ ConsoleProcessHandle* const pConso
|
||||
TraceLoggingWrite(
|
||||
g_hConhostV2EventTraceProvider,
|
||||
"ConsoleAttachDetach",
|
||||
TraceLoggingUInt32(pConsoleProcessHandle->dwProcessId, "AttachedProcessId"),
|
||||
TraceLoggingUInt64(pConsoleProcessHandle->GetProcessCreationTime(), "AttachedProcessCreationTime"),
|
||||
TraceLoggingPid(pConsoleProcessHandle->dwProcessId, "AttachedProcessId"),
|
||||
TraceLoggingFileTime(pConsoleProcessHandle->GetProcessCreationTime(), "AttachedProcessCreationTime"),
|
||||
TraceLoggingBool(bIsAttach, "IsAttach"),
|
||||
TraceLoggingBool(bIsUserInteractive, "IsUserInteractive"),
|
||||
TraceLoggingKeyword(TIL_KEYWORD_TRACE),
|
||||
|
||||
@@ -35,25 +35,14 @@ Author(s):
|
||||
#define DBGOUTPUT(_params_)
|
||||
#endif
|
||||
|
||||
#define TraceLoggingConsoleCoord(value, name) \
|
||||
TraceLoggingStruct(2, name), \
|
||||
TraceLoggingInt32(value.X, "X"), \
|
||||
TraceLoggingInt32(value.Y, "Y")
|
||||
|
||||
class Tracing
|
||||
{
|
||||
public:
|
||||
~Tracing();
|
||||
|
||||
static Tracing s_TraceApiCall(const NTSTATUS result, PCSTR traceName);
|
||||
|
||||
static void s_TraceApi(const NTSTATUS status, const CONSOLE_GETLARGESTWINDOWSIZE_MSG* const a);
|
||||
static void s_TraceApi(const NTSTATUS status, const CONSOLE_SCREENBUFFERINFO_MSG* const a, const bool fSet);
|
||||
static void s_TraceApi(const NTSTATUS status, const CONSOLE_SETSCREENBUFFERSIZE_MSG* const a);
|
||||
static void s_TraceApi(const NTSTATUS status, const CONSOLE_SETWINDOWINFO_MSG* const a);
|
||||
|
||||
static void s_TraceApi(_In_ const void* const buffer, const CONSOLE_WRITECONSOLE_MSG* const a);
|
||||
|
||||
static void s_TraceApi(const CONSOLE_SCREENBUFFERINFO_MSG* const a);
|
||||
static void s_TraceApi(const CONSOLE_MODE_MSG* const a, const std::wstring_view handleType);
|
||||
static void s_TraceApi(const CONSOLE_SETTEXTATTRIBUTE_MSG* const a);
|
||||
static void s_TraceApi(const CONSOLE_WRITECONSOLEOUTPUTSTRING_MSG* const a);
|
||||
|
||||
static void s_TraceWindowViewport(const Microsoft::Console::Types::Viewport& viewport);
|
||||
|
||||
static void s_TraceChars(_In_z_ const char* pszMessage, ...);
|
||||
@@ -69,8 +58,4 @@ public:
|
||||
|
||||
private:
|
||||
static ULONG s_ulDebugFlag;
|
||||
|
||||
Tracing(std::function<void()> onExit);
|
||||
|
||||
std::function<void()> _onExit;
|
||||
};
|
||||
|
||||
@@ -12,6 +12,29 @@
|
||||
#include "../host/telemetry.hpp"
|
||||
#include "../host/cmdline.h"
|
||||
|
||||
// Assumes that it will find <m> in the calling environment.
|
||||
#define TraceConsoleAPICallWithOrigin(ApiName, ...) \
|
||||
TraceLoggingWrite( \
|
||||
g_hConhostV2EventTraceProvider, \
|
||||
"API_" ApiName, \
|
||||
TraceLoggingPid(TraceGetProcessId(m), "OriginatingProcess"), \
|
||||
TraceLoggingTid(TraceGetThreadId(m), "OriginatingThread"), \
|
||||
__VA_ARGS__ __VA_OPT__(, ) \
|
||||
TraceLoggingLevel(WINEVENT_LEVEL_VERBOSE), \
|
||||
TraceLoggingKeyword(TIL_KEYWORD_TRACE));
|
||||
|
||||
static DWORD TraceGetProcessId(CONSOLE_API_MSG* const m)
|
||||
{
|
||||
const auto p = m->GetProcessHandle();
|
||||
return p ? p->dwProcessId : 0;
|
||||
}
|
||||
|
||||
static DWORD TraceGetThreadId(CONSOLE_API_MSG* const m)
|
||||
{
|
||||
const auto p = m->GetProcessHandle();
|
||||
return p ? p->dwThreadId : 0;
|
||||
}
|
||||
|
||||
[[nodiscard]] HRESULT ApiDispatchers::ServerGetConsoleCP(_Inout_ CONSOLE_API_MSG* const m,
|
||||
_Inout_ BOOL* const /*pbReplyPending*/)
|
||||
{
|
||||
@@ -35,35 +58,22 @@
|
||||
{
|
||||
Telemetry::Instance().LogApiCall(Telemetry::ApiCall::GetConsoleMode);
|
||||
const auto a = &m->u.consoleMsgL1.GetConsoleMode;
|
||||
std::wstring_view handleType = L"unknown";
|
||||
|
||||
TraceLoggingWrite(g_hConhostV2EventTraceProvider,
|
||||
"API_GetConsoleMode",
|
||||
TraceLoggingLevel(WINEVENT_LEVEL_VERBOSE),
|
||||
TraceLoggingKeyword(TIL_KEYWORD_TRACE),
|
||||
TraceLoggingOpcode(WINEVENT_OPCODE_START));
|
||||
|
||||
auto tracing = wil::scope_exit([&]() {
|
||||
Tracing::s_TraceApi(a, handleType);
|
||||
TraceLoggingWrite(g_hConhostV2EventTraceProvider,
|
||||
"API_GetConsoleMode",
|
||||
TraceLoggingLevel(WINEVENT_LEVEL_VERBOSE),
|
||||
TraceLoggingKeyword(TIL_KEYWORD_TRACE),
|
||||
TraceLoggingOpcode(WINEVENT_OPCODE_STOP));
|
||||
});
|
||||
|
||||
const auto pObjectHandle = m->GetObjectHandle();
|
||||
RETURN_HR_IF_NULL(E_HANDLE, pObjectHandle);
|
||||
|
||||
TraceConsoleAPICallWithOrigin(
|
||||
"GetConsoleMode",
|
||||
TraceLoggingBool(pObjectHandle->IsInputHandle(), "InputHandle"));
|
||||
|
||||
if (pObjectHandle->IsInputHandle())
|
||||
{
|
||||
handleType = L"input";
|
||||
InputBuffer* pObj;
|
||||
RETURN_IF_FAILED(pObjectHandle->GetInputBuffer(GENERIC_READ, &pObj));
|
||||
m->_pApiRoutines->GetConsoleInputModeImpl(*pObj, a->Mode);
|
||||
}
|
||||
else
|
||||
{
|
||||
handleType = L"output";
|
||||
SCREEN_INFORMATION* pObj;
|
||||
RETURN_IF_FAILED(pObjectHandle->GetScreenBuffer(GENERIC_READ, &pObj));
|
||||
m->_pApiRoutines->GetConsoleOutputModeImpl(*pObj, a->Mode);
|
||||
@@ -79,6 +89,12 @@
|
||||
|
||||
const auto pObjectHandle = m->GetObjectHandle();
|
||||
RETURN_HR_IF_NULL(E_HANDLE, pObjectHandle);
|
||||
|
||||
TraceConsoleAPICallWithOrigin(
|
||||
"SetConsoleMode",
|
||||
TraceLoggingBool(pObjectHandle->IsInputHandle(), "InputHandle"),
|
||||
TraceLoggingHexULong(a->Mode, "Mode"));
|
||||
|
||||
if (pObjectHandle->IsInputHandle())
|
||||
{
|
||||
InputBuffer* pObj;
|
||||
@@ -365,9 +381,6 @@
|
||||
// Get input parameter buffer
|
||||
PVOID pvBuffer;
|
||||
ULONG cbBufferSize;
|
||||
auto tracing = wil::scope_exit([&]() {
|
||||
Tracing::s_TraceApi(pvBuffer, a);
|
||||
});
|
||||
RETURN_IF_FAILED(m->GetInputBuffer(&pvBuffer, &cbBufferSize));
|
||||
|
||||
std::unique_ptr<IWaitRoutine> waiter;
|
||||
@@ -385,6 +398,11 @@
|
||||
const std::wstring_view buffer(reinterpret_cast<wchar_t*>(pvBuffer), cbBufferSize / sizeof(wchar_t));
|
||||
size_t cchInputRead;
|
||||
|
||||
TraceConsoleAPICallWithOrigin(
|
||||
"WriteConsoleW",
|
||||
TraceLoggingUInt32(a->NumBytes, "NumBytes"),
|
||||
TraceLoggingCountedWideString(buffer.data(), static_cast<ULONG>(buffer.size()), "Buffer"));
|
||||
|
||||
hr = m->_pApiRoutines->WriteConsoleWImpl(*pScreenInfo, buffer, cchInputRead, requiresVtQuirk, waiter);
|
||||
|
||||
// We must set the reply length in bytes. Convert back from characters.
|
||||
@@ -395,6 +413,11 @@
|
||||
const std::string_view buffer(reinterpret_cast<char*>(pvBuffer), cbBufferSize);
|
||||
size_t cchInputRead;
|
||||
|
||||
TraceConsoleAPICallWithOrigin(
|
||||
"WriteConsoleA",
|
||||
TraceLoggingUInt32(a->NumBytes, "NumBytes"),
|
||||
TraceLoggingCountedString(buffer.data(), static_cast<ULONG>(buffer.size()), "Buffer"));
|
||||
|
||||
hr = m->_pApiRoutines->WriteConsoleAImpl(*pScreenInfo, buffer, cchInputRead, requiresVtQuirk, waiter);
|
||||
|
||||
// Reply length is already in bytes (chars), don't need to convert.
|
||||
@@ -581,10 +604,6 @@
|
||||
Telemetry::Instance().LogApiCall(Telemetry::ApiCall::GetConsoleScreenBufferInfoEx);
|
||||
const auto a = &m->u.consoleMsgL2.GetConsoleScreenBufferInfo;
|
||||
|
||||
auto tracing = wil::scope_exit([&]() {
|
||||
Tracing::s_TraceApi(a);
|
||||
});
|
||||
|
||||
CONSOLE_SCREEN_BUFFER_INFOEX ex = { 0 };
|
||||
ex.cbSize = sizeof(CONSOLE_SCREEN_BUFFER_INFOEX);
|
||||
|
||||
@@ -640,6 +659,12 @@
|
||||
ex.wAttributes = a->Attributes;
|
||||
ex.wPopupAttributes = a->PopupAttributes;
|
||||
|
||||
TraceConsoleAPICallWithOrigin(
|
||||
"SetConsoleScreenBufferInfoEx",
|
||||
TraceLoggingConsoleCoord(a->Size, "BufferSize"),
|
||||
TraceLoggingConsoleCoord(a->CurrentWindowSize, "WindowSize"),
|
||||
TraceLoggingConsoleCoord(a->MaximumWindowSize, "MaxWindowSize"));
|
||||
|
||||
return m->_pApiRoutines->SetConsoleScreenBufferInfoExImpl(*pObj, ex);
|
||||
}
|
||||
|
||||
@@ -655,6 +680,10 @@
|
||||
SCREEN_INFORMATION* pObj;
|
||||
RETURN_IF_FAILED(pObjectHandle->GetScreenBuffer(GENERIC_WRITE, &pObj));
|
||||
|
||||
TraceConsoleAPICallWithOrigin(
|
||||
"SetConsoleScreenBufferSize",
|
||||
TraceLoggingConsoleCoord(a->Size, "BufferSize"));
|
||||
|
||||
return m->_pApiRoutines->SetConsoleScreenBufferSizeImpl(*pObj, til::wrap_coord_size(a->Size));
|
||||
}
|
||||
|
||||
@@ -731,15 +760,16 @@
|
||||
Telemetry::Instance().LogApiCall(Telemetry::ApiCall::SetConsoleTextAttribute);
|
||||
const auto a = &m->u.consoleMsgL2.SetConsoleTextAttribute;
|
||||
|
||||
auto tracing = wil::scope_exit([&]() {
|
||||
Tracing::s_TraceApi(a);
|
||||
});
|
||||
|
||||
const auto pObjectHandle = m->GetObjectHandle();
|
||||
RETURN_HR_IF_NULL(E_HANDLE, pObjectHandle);
|
||||
|
||||
SCREEN_INFORMATION* pObj;
|
||||
RETURN_IF_FAILED(pObjectHandle->GetScreenBuffer(GENERIC_WRITE, &pObj));
|
||||
|
||||
TraceConsoleAPICallWithOrigin(
|
||||
"SetConsoleTextAttribute",
|
||||
TraceLoggingHexUInt16(a->Attributes, "Attributes"));
|
||||
|
||||
RETURN_HR(m->_pApiRoutines->SetConsoleTextAttributeImpl(*pObj, a->Attributes));
|
||||
}
|
||||
|
||||
@@ -755,6 +785,14 @@
|
||||
SCREEN_INFORMATION* pObj;
|
||||
RETURN_IF_FAILED(pObjectHandle->GetScreenBuffer(GENERIC_WRITE, &pObj));
|
||||
|
||||
TraceConsoleAPICallWithOrigin(
|
||||
"SetConsoleWindowInfo",
|
||||
TraceLoggingBool(a->Absolute, "IsWindowRectAbsolute"),
|
||||
TraceLoggingInt32(a->Window.Left, "WindowRectLeft"),
|
||||
TraceLoggingInt32(a->Window.Right, "WindowRectRight"),
|
||||
TraceLoggingInt32(a->Window.Top, "WindowRectTop"),
|
||||
TraceLoggingInt32(a->Window.Bottom, "WindowRectBottom"));
|
||||
|
||||
return m->_pApiRoutines->SetConsoleWindowInfoImpl(*pObj, a->Absolute, til::wrap_small_rect(a->Window));
|
||||
}
|
||||
|
||||
@@ -911,10 +949,6 @@
|
||||
{
|
||||
const auto a = &m->u.consoleMsgL2.WriteConsoleOutputString;
|
||||
|
||||
auto tracing = wil::scope_exit([&]() {
|
||||
Tracing::s_TraceApi(a);
|
||||
});
|
||||
|
||||
switch (a->StringType)
|
||||
{
|
||||
case CONSOLE_ATTRIBUTE:
|
||||
@@ -951,6 +985,11 @@
|
||||
{
|
||||
const std::string_view text(reinterpret_cast<char*>(pvBuffer), cbBufferSize);
|
||||
|
||||
TraceConsoleAPICallWithOrigin(
|
||||
"WriteConsoleOutputCharacterA",
|
||||
TraceLoggingConsoleCoord(a->WriteCoord, "WriteCoord"),
|
||||
TraceLoggingUInt32(a->NumRecords, "NumRecords"));
|
||||
|
||||
hr = m->_pApiRoutines->WriteConsoleOutputCharacterAImpl(*pScreenInfo,
|
||||
text,
|
||||
til::wrap_coord(a->WriteCoord),
|
||||
@@ -963,6 +1002,11 @@
|
||||
{
|
||||
const std::wstring_view text(reinterpret_cast<wchar_t*>(pvBuffer), cbBufferSize / sizeof(wchar_t));
|
||||
|
||||
TraceConsoleAPICallWithOrigin(
|
||||
"WriteConsoleOutputCharacterW",
|
||||
TraceLoggingConsoleCoord(a->WriteCoord, "WriteCoord"),
|
||||
TraceLoggingUInt32(a->NumRecords, "NumRecords"));
|
||||
|
||||
hr = m->_pApiRoutines->WriteConsoleOutputCharacterWImpl(*pScreenInfo,
|
||||
text,
|
||||
til::wrap_coord(a->WriteCoord),
|
||||
@@ -974,6 +1018,11 @@
|
||||
{
|
||||
const std::span<const WORD> text(reinterpret_cast<WORD*>(pvBuffer), cbBufferSize / sizeof(WORD));
|
||||
|
||||
TraceConsoleAPICallWithOrigin(
|
||||
"WriteConsoleOutputAttribute",
|
||||
TraceLoggingConsoleCoord(a->WriteCoord, "WriteCoord"),
|
||||
TraceLoggingUInt32(a->NumRecords, "NumRecords"));
|
||||
|
||||
hr = m->_pApiRoutines->WriteConsoleOutputAttributeImpl(*pScreenInfo,
|
||||
text,
|
||||
til::wrap_coord(a->WriteCoord),
|
||||
|
||||
@@ -174,7 +174,6 @@ PCONSOLE_API_MSG ApiSorter::ConsoleDispatchRequest(_Inout_ PCONSOLE_API_MSG Mess
|
||||
// alias API.
|
||||
NTSTATUS Status = S_OK;
|
||||
{
|
||||
const auto trace = Tracing::s_TraceApiCall(Status, Descriptor->TraceName);
|
||||
Status = (*Descriptor->Routine)(Message, &ReplyPending);
|
||||
}
|
||||
if (Status != STATUS_BUFFER_TOO_SMALL)
|
||||
|
||||
@@ -28,7 +28,7 @@ ConsoleProcessHandle::ConsoleProcessHandle(const DWORD dwProcessId,
|
||||
dwProcessId))),
|
||||
_policy(ConsoleProcessPolicy::s_CreateInstance(_hProcess.get())),
|
||||
_shimPolicy(_hProcess.get()),
|
||||
_processCreationTime(0)
|
||||
_processCreationTime{}
|
||||
{
|
||||
if (nullptr != _hProcess.get())
|
||||
{
|
||||
@@ -77,24 +77,17 @@ const HANDLE ConsoleProcessHandle::GetRawHandle() const
|
||||
// Routine Description:
|
||||
// - Retrieves the process creation time (currently used in telemetry traces)
|
||||
// - The creation time is lazily populated on first call
|
||||
const ULONG64 ConsoleProcessHandle::GetProcessCreationTime() const
|
||||
const FILETIME ConsoleProcessHandle::GetProcessCreationTime() const
|
||||
{
|
||||
if (_processCreationTime == 0 && _hProcess != nullptr)
|
||||
if (_processCreationTime.dwHighDateTime == 0 && _processCreationTime.dwLowDateTime == 0 && _hProcess != nullptr)
|
||||
{
|
||||
FILETIME ftCreationTime, ftDummyTime = { 0 };
|
||||
ULARGE_INTEGER creationTime = { 0 };
|
||||
FILETIME ftDummyTime = { 0 };
|
||||
|
||||
if (::GetProcessTimes(_hProcess.get(),
|
||||
&ftCreationTime,
|
||||
&ftDummyTime,
|
||||
&ftDummyTime,
|
||||
&ftDummyTime))
|
||||
{
|
||||
creationTime.HighPart = ftCreationTime.dwHighDateTime;
|
||||
creationTime.LowPart = ftCreationTime.dwLowDateTime;
|
||||
}
|
||||
|
||||
_processCreationTime = creationTime.QuadPart;
|
||||
::GetProcessTimes(_hProcess.get(),
|
||||
&_processCreationTime,
|
||||
&ftDummyTime,
|
||||
&ftDummyTime,
|
||||
&ftDummyTime);
|
||||
}
|
||||
|
||||
return _processCreationTime;
|
||||
|
||||
@@ -53,14 +53,14 @@ public:
|
||||
|
||||
CD_CONNECTION_INFORMATION GetConnectionInformation(IDeviceComm* deviceComm) const;
|
||||
|
||||
const ULONG64 GetProcessCreationTime() const;
|
||||
const FILETIME GetProcessCreationTime() const;
|
||||
|
||||
private:
|
||||
ULONG _ulTerminateCount;
|
||||
ULONG const _ulProcessGroupId;
|
||||
wil::unique_handle const _hProcess;
|
||||
|
||||
mutable ULONG64 _processCreationTime;
|
||||
mutable FILETIME _processCreationTime;
|
||||
|
||||
const ConsoleProcessPolicy _policy;
|
||||
const ConsoleShimPolicy _shimPolicy;
|
||||
|
||||
@@ -166,7 +166,8 @@ namespace Microsoft::Console::VirtualTerminal
|
||||
|
||||
constexpr VTParameter at(const size_t index) const noexcept
|
||||
{
|
||||
return til::at(_subParams, index);
|
||||
// If the index is out of range, we return a sub parameter with no value.
|
||||
return index < _subParams.size() ? til::at(_subParams, index) : defaultParameter;
|
||||
}
|
||||
|
||||
VTSubParameters subspan(const size_t offset, const size_t count) const noexcept
|
||||
@@ -191,6 +192,8 @@ namespace Microsoft::Console::VirtualTerminal
|
||||
}
|
||||
|
||||
private:
|
||||
static constexpr VTParameter defaultParameter{};
|
||||
|
||||
std::span<const VTParameter> _subParams;
|
||||
};
|
||||
|
||||
|
||||
@@ -4117,14 +4117,14 @@ void AdaptDispatch::_ReportSGRSetting() const
|
||||
else if (color.IsIndex256())
|
||||
{
|
||||
const auto index = color.GetIndex();
|
||||
fmt::format_to(std::back_inserter(response), FMT_COMPILE(L";{};5;{}"), base + 8, index);
|
||||
fmt::format_to(std::back_inserter(response), FMT_COMPILE(L";{}:5:{}"), base + 8, index);
|
||||
}
|
||||
else if (color.IsRgb())
|
||||
{
|
||||
const auto r = GetRValue(color.GetRGB());
|
||||
const auto g = GetGValue(color.GetRGB());
|
||||
const auto b = GetBValue(color.GetRGB());
|
||||
fmt::format_to(std::back_inserter(response), FMT_COMPILE(L";{};2;{};{};{}"), base + 8, r, g, b);
|
||||
fmt::format_to(std::back_inserter(response), FMT_COMPILE(L";{}:2::{}:{}:{}"), base + 8, r, g, b);
|
||||
}
|
||||
};
|
||||
addColor(30, attr.GetForeground());
|
||||
|
||||
@@ -297,12 +297,15 @@ namespace Microsoft::Console::VirtualTerminal
|
||||
size_t _SetRgbColorsHelper(const VTParameters options,
|
||||
TextAttribute& attr,
|
||||
const bool isForeground) noexcept;
|
||||
void _SetRgbColorsHelperFromSubParams(const VTParameter colorItem,
|
||||
const VTSubParameters options,
|
||||
TextAttribute& attr) noexcept;
|
||||
size_t _ApplyGraphicsOption(const VTParameters options,
|
||||
const size_t optionIndex,
|
||||
TextAttribute& attr) noexcept;
|
||||
void _ApplyGraphicsOptionSubParam(const VTParameter option,
|
||||
const VTSubParameters subParams,
|
||||
TextAttribute& attr) noexcept;
|
||||
void _ApplyGraphicsOptionWithSubParams(const VTParameter option,
|
||||
const VTSubParameters subParams,
|
||||
TextAttribute& attr) noexcept;
|
||||
void _ApplyGraphicsOptions(const VTParameters options,
|
||||
TextAttribute& attr) noexcept;
|
||||
|
||||
|
||||
@@ -35,7 +35,8 @@ size_t AdaptDispatch::_SetRgbColorsHelper(const VTParameters options,
|
||||
const size_t red = options.at(1).value_or(0);
|
||||
const size_t green = options.at(2).value_or(0);
|
||||
const size_t blue = options.at(3).value_or(0);
|
||||
// ensure that each value fits in a byte
|
||||
// We only apply the color if the R, G, B values fit within a byte.
|
||||
// This is to match XTerm's and VTE's behavior.
|
||||
if (red <= 255 && green <= 255 && blue <= 255)
|
||||
{
|
||||
const auto rgbColor = RGB(red, green, blue);
|
||||
@@ -46,6 +47,9 @@ size_t AdaptDispatch::_SetRgbColorsHelper(const VTParameters options,
|
||||
{
|
||||
optionsConsumed = 2;
|
||||
const size_t tableIndex = options.at(1).value_or(0);
|
||||
|
||||
// We only apply the color if the index value fit within a byte.
|
||||
// This is to match XTerm's and VTE's behavior.
|
||||
if (tableIndex <= 255)
|
||||
{
|
||||
const auto adjustedIndex = gsl::narrow_cast<BYTE>(tableIndex);
|
||||
@@ -62,6 +66,77 @@ size_t AdaptDispatch::_SetRgbColorsHelper(const VTParameters options,
|
||||
return optionsConsumed;
|
||||
}
|
||||
|
||||
// Routine Description:
|
||||
// - Helper to parse extended graphics options, which start with 38 (FG) or 48 (BG)
|
||||
// - These options are followed by either a 2 (RGB) or 5 (xterm index):
|
||||
// - RGB sequences then take 4 MORE options to designate the ColorSpaceID, R, G, B parts
|
||||
// of the color.
|
||||
// - Xterm index will use the option that follows to use a color from the
|
||||
// preset 256 color xterm color table.
|
||||
// Arguments:
|
||||
// - colorItem - One of FG(38) and BG(48), indicating which color we're setting.
|
||||
// - options - An array of options that will be used to generate the RGB color
|
||||
// - attr - The attribute that will be updated with the parsed color.
|
||||
// Return Value:
|
||||
// - <none>
|
||||
void AdaptDispatch::_SetRgbColorsHelperFromSubParams(const VTParameter colorItem,
|
||||
const VTSubParameters options,
|
||||
TextAttribute& attr) noexcept
|
||||
{
|
||||
// This should be called for applying FG and BG colors only.
|
||||
assert(colorItem == GraphicsOptions::ForegroundExtended ||
|
||||
colorItem == GraphicsOptions::BackgroundExtended);
|
||||
|
||||
const bool isForeground = (colorItem == GraphicsOptions::ForegroundExtended);
|
||||
const DispatchTypes::GraphicsOptions typeOpt = options.at(0);
|
||||
|
||||
if (typeOpt == DispatchTypes::GraphicsOptions::RGBColorOrFaint)
|
||||
{
|
||||
// sub params are in the order:
|
||||
// :2:<color-space-id>:<r>:<g>:<b>
|
||||
|
||||
// We treat a color as invalid, if it has a color space ID, as some
|
||||
// applications that support non-standard ODA color sequence may send
|
||||
// the red value in its place.
|
||||
const bool hasColorSpaceId = options.at(1).has_value();
|
||||
|
||||
// Skip color-space-id.
|
||||
const size_t red = options.at(2).value_or(0);
|
||||
const size_t green = options.at(3).value_or(0);
|
||||
const size_t blue = options.at(4).value_or(0);
|
||||
|
||||
// We only apply the color if the R, G, B values fit within a byte.
|
||||
// This is to match XTerm's and VTE's behavior.
|
||||
if (!hasColorSpaceId && red <= 255 && green <= 255 && blue <= 255)
|
||||
{
|
||||
const auto rgbColor = RGB(red, green, blue);
|
||||
attr.SetColor(rgbColor, isForeground);
|
||||
}
|
||||
}
|
||||
else if (typeOpt == DispatchTypes::GraphicsOptions::BlinkOrXterm256Index)
|
||||
{
|
||||
// sub params are in the order:
|
||||
// :5:<n>
|
||||
// where 'n' is the index into the xterm color table.
|
||||
const size_t tableIndex = options.at(1).value_or(0);
|
||||
|
||||
// We only apply the color if the index value fit within a byte.
|
||||
// This is to match XTerm's and VTE's behavior.
|
||||
if (tableIndex <= 255)
|
||||
{
|
||||
const auto adjustedIndex = gsl::narrow_cast<BYTE>(tableIndex);
|
||||
if (isForeground)
|
||||
{
|
||||
attr.SetIndexedForeground256(adjustedIndex);
|
||||
}
|
||||
else
|
||||
{
|
||||
attr.SetIndexedBackground256(adjustedIndex);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Routine Description:
|
||||
// - Helper to apply a single graphic rendition option to an attribute.
|
||||
// - Calls appropriate helper to apply the option with sub parameters when necessary.
|
||||
@@ -80,7 +155,7 @@ size_t AdaptDispatch::_ApplyGraphicsOption(const VTParameters options,
|
||||
if (options.hasSubParamsFor(optionIndex))
|
||||
{
|
||||
const auto subParams = options.subParamsFor(optionIndex);
|
||||
_ApplyGraphicsOptionSubParam(opt, subParams, attr);
|
||||
_ApplyGraphicsOptionWithSubParams(opt, subParams, attr);
|
||||
return 1;
|
||||
}
|
||||
|
||||
@@ -260,20 +335,30 @@ size_t AdaptDispatch::_ApplyGraphicsOption(const VTParameters options,
|
||||
}
|
||||
|
||||
// Routine Description:
|
||||
// - This is a no-op until we have something meaningful to do with sub parameters.
|
||||
// - Helper to apply a single graphic rendition option with sub parameters to an attribute.
|
||||
// Arguments:
|
||||
// - option - An option to apply.
|
||||
// - subParams - Sub parameters associated with the option.
|
||||
// - attr - The attribute that will be updated with the applied option.
|
||||
// Return Value:
|
||||
// - <None>
|
||||
void AdaptDispatch::_ApplyGraphicsOptionSubParam(const VTParameter /* option */,
|
||||
const VTSubParameters /* subParam */,
|
||||
TextAttribute& /* attr */) noexcept
|
||||
void AdaptDispatch::_ApplyGraphicsOptionWithSubParams(const VTParameter option,
|
||||
const VTSubParameters subParams,
|
||||
TextAttribute& attr) noexcept
|
||||
{
|
||||
// here, we apply our "best effort" rule, while handling sub params if we don't
|
||||
// recognise the parameter substring (parameter and it's sub parameters) then
|
||||
// we should just skip over them.
|
||||
switch (option)
|
||||
{
|
||||
case ForegroundExtended:
|
||||
case BackgroundExtended:
|
||||
_SetRgbColorsHelperFromSubParams(option, subParams, attr);
|
||||
break;
|
||||
default:
|
||||
/* do nothing */
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Routine Description:
|
||||
|
||||
@@ -1699,7 +1699,7 @@ public:
|
||||
attribute.SetIndexedBackground256(45);
|
||||
_testGetSet->_textBuffer->SetCurrentAttributes(attribute);
|
||||
requestSetting(L"m");
|
||||
_testGetSet->ValidateInputEvent(L"\033P1$r0;38;5;123;48;5;45m\033\\");
|
||||
_testGetSet->ValidateInputEvent(L"\033P1$r0;38:5:123;48:5:45m\033\\");
|
||||
|
||||
Log::Comment(L"Requesting SGR attributes (ITU RGB colors).");
|
||||
_testGetSet->PrepData();
|
||||
@@ -1708,7 +1708,7 @@ public:
|
||||
attribute.SetBackground(RGB(65, 43, 21));
|
||||
_testGetSet->_textBuffer->SetCurrentAttributes(attribute);
|
||||
requestSetting(L"m");
|
||||
_testGetSet->ValidateInputEvent(L"\033P1$r0;38;2;12;34;56;48;2;65;43;21m\033\\");
|
||||
_testGetSet->ValidateInputEvent(L"\033P1$r0;38:2::12:34:56;48:2::65:43:21m\033\\");
|
||||
|
||||
Log::Comment(L"Requesting DECSCA attributes (unprotected).");
|
||||
_testGetSet->PrepData();
|
||||
@@ -2464,6 +2464,119 @@ public:
|
||||
rgOptions[4] = 123;
|
||||
_testGetSet->_expectedAttribute.SetForeground(RGB(0, 0, 123));
|
||||
VERIFY_IS_TRUE(_pDispatch->SetGraphicsRendition({ rgOptions, 5 }));
|
||||
|
||||
Log::Comment(L"Test 6: Ignore Rgb color when R, G or B is out of range (>255)");
|
||||
_testGetSet->PrepData(); // default color from here is gray on black, FOREGROUND_BLUE | FOREGROUND_GREEN | FOREGROUND_RED
|
||||
rgOptions[0] = DispatchTypes::GraphicsOptions::ForegroundExtended;
|
||||
rgOptions[1] = DispatchTypes::GraphicsOptions::RGBColorOrFaint;
|
||||
rgOptions[2] = 283;
|
||||
rgOptions[3] = 182;
|
||||
rgOptions[4] = 123;
|
||||
// expect no change
|
||||
_testGetSet->_expectedAttribute = TextAttribute{ FOREGROUND_BLUE | FOREGROUND_GREEN | FOREGROUND_RED };
|
||||
VERIFY_IS_TRUE(_pDispatch->SetGraphicsRendition({ rgOptions, 5 }));
|
||||
|
||||
Log::Comment(L"Test 7: Ignore indexed color when index is out of range (>255)");
|
||||
_testGetSet->PrepData(); // default color from here is gray on black, FOREGROUND_BLUE | FOREGROUND_GREEN | FOREGROUND_RED
|
||||
rgOptions[0] = DispatchTypes::GraphicsOptions::ForegroundExtended;
|
||||
rgOptions[1] = DispatchTypes::GraphicsOptions::BlinkOrXterm256Index;
|
||||
rgOptions[2] = 283;
|
||||
// expect no change
|
||||
_testGetSet->_expectedAttribute = TextAttribute{ FOREGROUND_BLUE | FOREGROUND_GREEN | FOREGROUND_RED };
|
||||
VERIFY_IS_TRUE(_pDispatch->SetGraphicsRendition({ rgOptions, 3 }));
|
||||
}
|
||||
|
||||
TEST_METHOD(XtermExtendedSubParameterColorTest)
|
||||
{
|
||||
Log::Comment(L"Starting test...");
|
||||
|
||||
_testGetSet->PrepData(); // default color from here is gray on black, FOREGROUND_BLUE | FOREGROUND_GREEN | FOREGROUND_RED
|
||||
|
||||
VTParameter rgOptions[1];
|
||||
VTParameter rgSubParamOpts[16];
|
||||
std::pair<BYTE, BYTE> subParamRanges[1];
|
||||
|
||||
_testGetSet->_expectedAttribute = _testGetSet->_textBuffer->GetCurrentAttributes();
|
||||
|
||||
Log::Comment(L"Test 1: Change Indexed Foreground with missing index sub parameter");
|
||||
rgOptions[0] = DispatchTypes::GraphicsOptions::ForegroundExtended;
|
||||
rgSubParamOpts[0] = DispatchTypes::GraphicsOptions::BlinkOrXterm256Index;
|
||||
subParamRanges[0] = { (BYTE)0, (BYTE)1 };
|
||||
_testGetSet->_expectedAttribute.SetIndexedForeground256(TextColor::DARK_BLACK);
|
||||
VERIFY_IS_TRUE(_pDispatch->SetGraphicsRendition({ rgOptions, rgSubParamOpts, subParamRanges }));
|
||||
|
||||
Log::Comment(L"Test 2: Change Indexed Background with default index sub parameter");
|
||||
rgOptions[0] = DispatchTypes::GraphicsOptions::BackgroundExtended;
|
||||
rgSubParamOpts[0] = DispatchTypes::GraphicsOptions::BlinkOrXterm256Index;
|
||||
rgSubParamOpts[1] = {};
|
||||
subParamRanges[0] = { (BYTE)0, (BYTE)2 };
|
||||
_testGetSet->_expectedAttribute.SetIndexedBackground256(TextColor::DARK_BLACK);
|
||||
VERIFY_IS_TRUE(_pDispatch->SetGraphicsRendition({ rgOptions, rgSubParamOpts, subParamRanges }));
|
||||
|
||||
Log::Comment(L"Test 3: Change RGB Foreground with all RGB sub parameters missing");
|
||||
rgOptions[0] = DispatchTypes::GraphicsOptions::ForegroundExtended;
|
||||
rgSubParamOpts[0] = DispatchTypes::GraphicsOptions::RGBColorOrFaint;
|
||||
subParamRanges[0] = { (BYTE)0, (BYTE)1 };
|
||||
_testGetSet->_expectedAttribute.SetForeground(RGB(0, 0, 0));
|
||||
VERIFY_IS_TRUE(_pDispatch->SetGraphicsRendition({ rgOptions, rgSubParamOpts, subParamRanges }));
|
||||
|
||||
Log::Comment(L"Test 4: Change RGB Background with some missing RGB sub parameters");
|
||||
rgOptions[0] = DispatchTypes::GraphicsOptions::BackgroundExtended;
|
||||
rgSubParamOpts[0] = DispatchTypes::GraphicsOptions::RGBColorOrFaint;
|
||||
rgSubParamOpts[1] = {}; // color-space-id
|
||||
rgSubParamOpts[2] = 123;
|
||||
subParamRanges[0] = { (BYTE)0, (BYTE)3 };
|
||||
_testGetSet->_expectedAttribute.SetBackground(RGB(123, 0, 0));
|
||||
VERIFY_IS_TRUE(_pDispatch->SetGraphicsRendition({ rgOptions, rgSubParamOpts, subParamRanges }));
|
||||
|
||||
Log::Comment(L"Test 5: Change RGB Foreground with some default RGB sub parameters");
|
||||
rgOptions[0] = DispatchTypes::GraphicsOptions::ForegroundExtended;
|
||||
rgSubParamOpts[0] = DispatchTypes::GraphicsOptions::RGBColorOrFaint;
|
||||
rgSubParamOpts[1] = {}; // color-space-id
|
||||
rgSubParamOpts[2] = {};
|
||||
rgSubParamOpts[3] = {};
|
||||
rgSubParamOpts[4] = 123;
|
||||
subParamRanges[0] = { (BYTE)0, (BYTE)5 };
|
||||
_testGetSet->_expectedAttribute.SetForeground(RGB(0, 0, 123));
|
||||
VERIFY_IS_TRUE(_pDispatch->SetGraphicsRendition({ rgOptions, rgSubParamOpts, subParamRanges }));
|
||||
|
||||
Log::Comment(L"Test 6: Ignore color when ColorSpaceID is not empty");
|
||||
_testGetSet->PrepData(); // default color from here is gray on black, FOREGROUND_BLUE | FOREGROUND_GREEN | FOREGROUND_RED
|
||||
rgOptions[0] = DispatchTypes::GraphicsOptions::ForegroundExtended;
|
||||
rgSubParamOpts[0] = DispatchTypes::GraphicsOptions::RGBColorOrFaint;
|
||||
rgSubParamOpts[1] = 7; // color-space-id
|
||||
rgSubParamOpts[2] = 182;
|
||||
rgSubParamOpts[3] = 182;
|
||||
rgSubParamOpts[4] = 123;
|
||||
subParamRanges[0] = { (BYTE)0, (BYTE)5 };
|
||||
// expect no change
|
||||
_testGetSet->_expectedAttribute = TextAttribute{ FOREGROUND_BLUE | FOREGROUND_GREEN | FOREGROUND_RED };
|
||||
VERIFY_IS_TRUE(_pDispatch->SetGraphicsRendition({ rgOptions, rgSubParamOpts, subParamRanges }));
|
||||
|
||||
Log::Comment(L"Test 7: Ignore Rgb color when R, G or B is out of range (>255)");
|
||||
_testGetSet->PrepData(); // default color from here is gray on black, FOREGROUND_BLUE | FOREGROUND_GREEN | FOREGROUND_RED
|
||||
rgOptions[0] = DispatchTypes::GraphicsOptions::BackgroundExtended;
|
||||
rgSubParamOpts[0] = DispatchTypes::GraphicsOptions::RGBColorOrFaint;
|
||||
rgSubParamOpts[1] = {}; // color-space-id
|
||||
// Ensure r, g and b set a color that is different from current color.
|
||||
// Otherwise, the test will pass even if the color is not ignored.
|
||||
rgSubParamOpts[2] = 128;
|
||||
rgSubParamOpts[3] = 283;
|
||||
rgSubParamOpts[4] = 155;
|
||||
subParamRanges[0] = { (BYTE)0, (BYTE)5 };
|
||||
// expect no change
|
||||
_testGetSet->_expectedAttribute = TextAttribute{ FOREGROUND_BLUE | FOREGROUND_GREEN | FOREGROUND_RED };
|
||||
VERIFY_IS_TRUE(_pDispatch->SetGraphicsRendition({ rgOptions, rgSubParamOpts, subParamRanges }));
|
||||
|
||||
Log::Comment(L"Test 8: Ignore indexed color when index is out of range (>255)");
|
||||
_testGetSet->PrepData(); // default color from here is gray on black, FOREGROUND_BLUE | FOREGROUND_GREEN | FOREGROUND_RED
|
||||
rgOptions[0] = DispatchTypes::GraphicsOptions::ForegroundExtended;
|
||||
rgSubParamOpts[0] = DispatchTypes::GraphicsOptions::BlinkOrXterm256Index;
|
||||
rgSubParamOpts[1] = 283;
|
||||
subParamRanges[0] = { (BYTE)0, (BYTE)2 };
|
||||
// expect no change
|
||||
_testGetSet->_expectedAttribute = TextAttribute{ FOREGROUND_BLUE | FOREGROUND_GREEN | FOREGROUND_RED };
|
||||
VERIFY_IS_TRUE(_pDispatch->SetGraphicsRendition({ rgOptions, rgSubParamOpts, subParamRanges }));
|
||||
}
|
||||
|
||||
TEST_METHOD(SetColorTableValue)
|
||||
|
||||
Reference in New Issue
Block a user