Compare commits

..

68 Commits

Author SHA1 Message Date
Mike Griese
70d905b658 add velocity, and a setting per-control for this 2024-02-08 09:29:13 -06:00
Mike Griese
9a6ded2e39 comments, disable with velocity 2024-02-08 06:11:01 -06:00
Mike Griese
eac27dbe25 spel 2024-02-07 13:58:34 -06:00
Mike Griese
d3a98b3754 Merge branch 'dev/migrie/fhl/7718-notifications' of https://github.com/microsoft/terminal into dev/migrie/fhl/7718-notifications 2024-02-07 13:08:26 -06:00
Mike Griese
d234049640 Revert "we do not want this"
This reverts commit 4ab628d62f.
2024-02-07 13:03:26 -06:00
Mike Griese
4ab628d62f we do not want this 2024-02-07 13:02:09 -06:00
Mike Griese
8309901fc9 We do want this 2024-02-07 13:01:33 -06:00
Mike Griese
015c5e8b93 Merge remote-tracking branch 'origin/main' into dev/migrie/fhl/7718-notifications-reboot 2024-02-07 09:19:58 -06:00
Mike Griese
71efdcb21b Disable DECKPAM mode behind velocity (#16675)
DECKPAM originally tracked in #16506.
Support was added in #16511.
But turns out people didn't expect the Terminal to actually be like,
compliant: #16654

This closes #16654 while we think over in #16672 how we want to solve
this
2024-02-06 23:11:34 +00:00
James Holderness
ec91be5995 Improve handling of Space key combinations (#16645)
This fixes two issues where the `Space` key wasn't being handled
correctly:

* Keyboards with an `AltGr`+`Space` mapping were not generating the
  expected character.
* Pressing a dead key followed by `Space` is supposed to generate the
  accent character associated with that key, but it wasn't doing so.

## References and Relevant Issues

These were both regressions from the keyboard refactor in PR #16511.

## Detailed Description of the Pull Request / Additional comments

The problem was that we were treating `VK_SPACE` as a "functional" key,
which means it gets hardcoded VT mappings which take precedence over
whatever is in the keyboard layout. This was deemed necessary to deal
with the fact that many keyboards incorrectly map `Ctrl`+`Space` as a
`SP` character, when it's expected to be `NUL`.

I've now dropped `VK_SPACE` from the functional mapping table and allow
it be handled by the default mapping algorithm for "graphic" keys.
However, I've also introduced a special case check for `Ctrl`+`Space`
(and other modifier variants), so we can bypass any incorrect keyboard
layouts for those combinations.

## Validation Steps Performed

I couldn't test with a French-BEPO keyboard layout directly, because the
MS Keyboard Layout Creator wouldn't accept a `Space` key mapping that
wasn't whitespace. However, if I remapped the `AltGr`+`Space` combo to
`LF`, I could confirm that we are now generating that correctly.

I've also tested the dead key `Space` combination on various keyboard
layouts and confirmed that that is now working correctly, and checked
that the `Ctrl`+`Space` combinations are still working too.

Closes #16641
Closes #16642
2024-02-06 17:02:25 -06:00
Leonard Hecker
5dda50767b General improvements in preparation for #16598 (#16601)
This contains all the parts of #16598 that aren't specific to session
restore, but are required for the code in #16598:
* Adds new GUID<>String functions that remove the `{}` brackets.
* Adds `SessionId` to the `ITerminalConnection` interface.
* Flush the `ApplicationState` before we terminate the process.
* Not monitoring `state.json` for changes is important as it prevents
  disturbing the session state while session persistence is ongoing.
  That's because when `ApplicationState` flushes to disk, the FS
  monitor will be triggered and reload the `ApplicationState` again.
2024-02-06 23:58:19 +01:00
Leonard Hecker
b70fd5e9c6 Port clipboard improvements from inbox to oss (#16670)
#16618 contained a bug which was fixed inbox. This ports the
changes to the OSS repo manually since the two slightly diverged.
2024-02-06 23:50:15 +01:00
Leonard Hecker
ef96e225da Restore support for pasting files (#16634)
TIL: You could Ctrl+V files into Windows Terminal and here I am,
always opening the context menu and selecting "Copy as path"... smh

This restores the support by adding a very rudimentary HDROP handler.
The flip side of the regression is that I learned about this and so
conhost also gets this now, because why not!

Closes #16627

## Validation Steps Performed
* Single files can be pasted in WT and conhost 
2024-02-02 00:49:57 +01:00
Leonard Hecker
c669afe2a0 Fix a bug caused by #16592 (#16624)
#16592 passes the return value of `GetEnvironmentStringsW` directly
to the `hstring` constructor even though the former returns a
double-null terminated string and the latter expects a regular one.

This PR fixes the issue by using a basic strlen() loop to compute
the length ourselves. It's still theoretically beneficial over
the previous code, but now it's rather bitter since the code isn't
particularly short anymore and so the biggest benefit is gone.

Closes #16623

## Validation Steps Performed
* Validated the `env` string in a debugger 
  It's 1 character shorter than the old `til::env` string.
  That's fine however, since any `HSTRING` is always null-terminated
  anyways and so we get an extra null-terminator for free.
* `wt powershell` works 
2024-01-31 00:16:24 +00:00
Dustin L. Howett
ce30e7c89c Upgrade Microsoft.Windows.ImplementationLibrary to 1.0.240122.1 (#16617)
This includes a fix for the hang on shutdown due to the folder change
reader.

WIL now validates format strings in `LOG...` macros (yay!) and so we
needed to fix some of our `LOG` macros.

Closes #16456
2024-01-30 16:01:12 -08:00
Dustin L. Howett
bcca7aac1b build: remove symbols' dependency on the Package phase (#16625)
Due to things outside our control, sometimes the Package phase fails
when VPack publication is enabled. Because of this, symbols won't be
published. We still want these builds to be considered "golden" and we
are still shipping them, so we *must* publish symbols.
2024-01-30 15:34:27 -08:00
Dustin L. Howett
a2bb3136bb version: bump to 1.21 on main 2024-01-29 19:24:18 -06:00
James Holderness
dccc1f4240 Refactor VT terminal input (#16511)
The primary reason for this refactoring was to simplify the management
of VT input sequences that vary depending on modes, adding support for
the missing application keypad sequences, and preparing the way for
future extensions like `S8C1T`.

However, it also includes fixes for a number of keyboard related bugs,
including a variety of missing or incorrect mappings for the `Ctrl` and
`Ctrl`+`Alt` key combinations, 

## References and Relevant Issues

This PR also includes a fix for #10308, which was previously closed as a
duplicate of #10551. I don't think those bugs were related, though, and
although they're both supposed to be fixed in Windows 11, this PR fixes
the issue in Windows 10.

## Detailed Description of the Pull Request / Additional comments

The way the input now works, there's a single keyboard map that takes a
virtual key code combined with `Ctrl`, `Alt`, and `Shift` modifier bits
as the lookup key, and the expected VT input sequence as the value. This
map is initially constructed at startup, and then regenerated whenever a
keyboard mode is changed.

This map takes care of the cursor keys, editing keys, function keys, and
keys like `BkSp` and `Return` which can be affected by mode changes. The
remaining "graphic" key combinations are determined manually at the time
of input.

The order of precedence looks like this:

1. If the virtual key is `0` or `VK_PACKET`, it's considered to be a
   synthesized keyboard event, and the `UnicodeChar` value is used
   exactly as given.

2. If it's a numeric keypad key, and `Alt` is pressed (but not `Ctrl`),
   then it's assumedly part of an Alt-Numpad composition, so the key
   press is ignored (the generated character will be transmitted when
   the `Alt` is released).

3. If the virtual key combined with modifier bits is found in the key
   map described above, then the matched escape sequence will be used
   used as the output.

4. If a `UnicodeChar` value has been provided, that will be used as the
   output, but possibly with additional Ctrl and Alt modifiers applied:

   a. If it's an `AltGr` key, and we've got either two `Ctrl` keys
      pressed or a left `Ctrl` key that is distinctly separate from a
      right `Alt` key, then we will try and convert the character into
      a C0 control code.

   b. If an `Alt` key is pressed (or in the case of an `AltGr` value,
      both `Alt` keys are pressed), then we will convert it into an
      Alt-key sequence by prefixing the character with an `ESC`.

5. If we don't have a `UnicodeChar`, we'll use the `ToUnicodeEx` API to
   check whether the current keyboard state reflects a dead key, and if
   so, return nothing.

6. Otherwise we'll make another `ToUnicodeEx` call but with any `Ctrl`
   and `Alt` modifiers removed from the state to determine the base key
   value. Once we have that, we can apply the modifiers ourself.

   a. If the `Ctrl` key is pressed, we'll try and convert the base value
      into a C0 control code. But if we can't do that, we'll try again
      with the virtual key code (if it's alphanumeric) as a fallback.

   b. If the `Alt` key is pressed, we'll convert the base value (or
      control code value) into an Alt-key sequence by prefixing it with
      an `ESC`.

For step 4-a, we determine whether the left `Ctrl` key is distinctly
separate from the right `Alt` key by recording the time that those keys
are pressed, and checking for a time gap greater than 50ms. This is
necessary to distinguish between the user pressing `Ctrl`+`AltGr`, or
just pressing `AltGr` alone, which triggers a fake `Ctrl` key press at
the same time.

## Validation Steps Performed

I created a test script to automate key presses in the terminal window
for every relevant key, along with every Ctrl/Alt/Shift modifier, and
every relevant mode combination. I then compared the generated input
sequences with XTerm and a DEC VT240 terminal. The idea wasn't to match
either of them exactly, but to make sure the places where we differed
were intentional and reasonable.

This mostly dealt with the US keyboard layout. Comparing international
layouts wasn't really feasible because DEC, Linux, and Windows keyboard
assignments tend to be quite different. However, I've manually tested a
number of different layouts, and tried to make sure that they were all
working in a reasonable manner.

In terms of unit testing, I haven't done much more than patching the
ones that already existed to get them to pass. They're honestly not
great tests, because they aren't generating events in the form that
you'd expect for a genuine key press, and that can significantly affect
the results, but I can't think of an easy way to improve them.

## PR Checklist
- [x] Closes #16506
- [x] Closes #16508
- [x] Closes #16509
- [x] Closes #16510
- [x] Closes #3483
- [x] Closes #11194
- [x] Closes #11700
- [x] Closes #12555
- [x] Closes #13319
- [x] Closes #15367
- [x] Closes #16173
- [x] Tests added/passed
2024-01-29 16:58:39 -08:00
Leonard Hecker
86c30bdaa2 Fix conhost clipboard handling bugs (#16457)
conhost has 2 bugs related to clipboard handling:
* Missing retry on `OpenClipboard`: When copying to the clipboard
  explorer.exe is very eager to open the clipboard and peek into it.
  I'm not sure why it happens, but I can see `CFSDropTarget` in the
  call stack. It uses COM RPC and so this takes ~20ms every time.
  That breaks conhost's clipboard randomly during `ConsoleBench`.
  During non-benchmarks I expect this to break during RDP.
* Missing null-terminator check during paste: `CF_UNICODETEXT` is
  documented to be a null-terminated string, which conhost v2
  failed to handle as it relied entirely on `GlobalSize`.

Additionally, this changeset simplifies the `HGLOBAL` code slightly
by adding `_copyToClipboard` to abstract it away.

## Validation Steps Performed
* `ConsoleBench` (#16453) doesn't fail randomly anymore 
2024-01-29 23:23:26 +00:00
Leonard Hecker
5f71cf3e94 Reset _wrapForced when erasing scrollback (#16610)
#15541 changed `AdaptDispatch::_FillRect` which caused it to not affect
the `ROW::_wrapForced` flag anymore. This change in behavior was not
noticeable as `TextBuffer::GetLastNonSpaceCharacter` had a bug where
rows of only whitespace text would always be treated as empty.
This would then affect `AdaptDispatch::_EraseAll` to accidentally
correctly guess the last row with text despite the `_FillRect` change.

#15701 then fixed `GetLastNonSpaceCharacter` indirectly by fixing
`ROW::MeasureRight` which now made the previous change apparent.
`_EraseAll` would now guess the last row of text incorrectly,
because it would find the rows that `_FillRect` cleared but still
had `_wrapForced` set to `true`.

This PR fixes the issue by replacing the `_FillRect` usage to clear
rows with direct calls to `ROW::Reset()`. In the future this could be
extended by also `MEM_DECOMMIT`ing the now unused underlying memory.

Closes #16603

## Validation Steps Performed
* Enter WSL and resize the window to <40 columns
* Execute
  ```sh
  cd /bin
  ls -la
  printf "\e[3J"
  ls -la
  printf "\e[3J"
  printf "\e[2J"
  ```
* Only one viewport-height-many lines of whitespace exist between the
  current prompt line and the previous scrollback contents 
2024-01-29 14:49:42 -08:00
Tushar Singh
a3ac337d88 Refactor TextBuffer::GenHTML/RTF to read the buffer directly (#16377)
`TextBuffer::GenHTML` and `TextBuffer::GenRTF` now read directly from
the TextBuffer.

- Since we're reading from the buffer, we can now read _all_ the
attributes saved in the buffer. Formatted copy now copies most (if not
all) font/color attributes in the requested format (RTF/HTML).
- Use `TextBuffer::CopyRequest` to pass all copy-related options into
text generation functions as one unit.
- Helper function `TextBuffer::CopyRequest::FromConfig()` generates a
copy request based on Selection mode and user configuration.
- Both formatted text generation functions now use `std::string` and
`fmt::format_to` to generate the required strings. Previously, we were
using `std::ostringstream` which is not recommended due to its potential
overhead.
- Reading attributes from `ROW`'s attribute RLE simplified the logic as
we don't have to track attribute change between the text.
- On the caller side, we do not have to rebuild the plain text string
from the vector of strings anymore. `TextBuffer::GetPlainText()` returns
the entire text as one `std::string`.
- Removed `TextBuffer::TextAndColors`.
- Removed `TextBuffer::GetText()`. `TextBuffer::GetPlainText()` took its
place.

This PR also fixes two bugs in the formatted copy:

- We were applying line breaks after each selected row, even though the
row could have been a Wrapped row. This caused the wrapped rows to break
when they shouldn't.
- We mishandled Unicode text (\uN) within the RTF copy. Every next
character that uses a surrogate pair or high codepoint was missing in
the copied text when pasted to MSWord. The command `\uc4` should have
been `\uc1`, which is used to tell how many fallback characters are used
for each Unicode codepoint (\u). We always use one `?` character as the
fallback.

Closes #16191

**References and Relevant Issues**

- #16270

**Validation Steps Performed**

- Casual copy-pasting from Terminal or OpenConsole to word editors works
as before.
- Verified HTML copy by copying the generated HTML string and running it
through an HTML viewer.
[Sample](https://codepen.io/tusharvickey/pen/wvNXbVN)
- Verified RTF copy by copy-pasting the generated RTF string into
MSWord.
- SingleLine mode works (<kbd>Shift</kbd>+ copy)
- BlockSelection mode works (<kbd>Alt</kbd> selection)
2024-01-29 22:20:33 +00:00
Leonard Hecker
5d2fa4782f Pump the message queue on frozen windows (#16588)
This changeset ensures that the message queue of frozen windows is
always being serviced. This should ensure that it won't fill up and
lead to deadlocks, freezes, or similar. I've tried _a lot_ of different
approaches before settling on this one. Introducing a custom `WM_APP`
message has the benefit of being the least intrusive to the existing
code base.

The approach that I would have favored the most would be to never
destroy the `AppHost` instance in the first place, as I imagined that
this would be more robust in general and resolve other (rare) bugs.
However, I found that this requires rewriting some substantial parts
of the code base around `AppHost` and it could be something that may
be of interest in the future.

Closes #16332
Depends on #16587 and #16575
2024-01-29 14:01:18 -08:00
Dustin L. Howett
63bfdb2e1e Merge remote-tracking branch 'origin/release-1.19' 2024-01-29 14:07:27 -06:00
Leonard Hecker
5575187b26 Add missing TraceLoggingRegister calls (VT ONLY) (#16467)
17cc109 and e9de646 both made the same mistake: When cleaning up our
telemetry code they also removed the calls to `TraceLoggingRegister`
which also broke regular tracing. Windows Defender in particular uses
the "CookedRead" event to monitor for malicious shell commands.

This doesn't fix it the "right way", because destructors of statics
aren't executed when DLLs are unloaded. But I felt like that this is
fine because we have way more statics than that in conhost land,
all of which have the same kind of issue.

(cherry picked from commit a65d5f321f)
Service-Card-Id: 91337330
Service-Version: 1.19
2024-01-29 14:05:55 -06:00
Leonard Hecker
48a6d92255 Fix font preview for conhost (#16324)
After exiting the main loop in this function the invariant
`nFont <= NumberOfFonts` still holds true. Additionally,
preceding this removed code is this (paraphrased):
```cpp
if (nFont < NumberOfFonts) {
    RtlMoveMemory(...);
}
```
It ensures that the given slot `nFont` is always unoccupied by moving
it and all following items upwards if needed. As such, the call to
`DeleteObject` is always incorrect, as the slot is always "empty",
but may contain a copy of the previous occupant due to the `memmove`.

This regressed in 154ac2b.

Closes #16297

## Validation Steps Performed
* All fonts have a unique look in the preview panel 

(cherry picked from commit 35240f263e)
Service-Card-Id: 91120871
Service-Version: 1.19
2024-01-29 14:04:05 -06:00
Dustin L. Howett
e727aaf679 Merge remote-tracking branch 'origin/inbox' into release-1.19 2024-01-29 13:52:43 -06:00
Mike Griese
98146c9d1b Use TerminateProcess to exit early (#16575)
Closes MSFT:46744208

BODGY: If the emperor is being dtor'd, it's because we've gone past the
end of main, and released the ref in main. Then we might run into an
edge case where main releases it's ref to the emperor, but one of the
window threads might be in the process of exiting, and still holding a
strong ref to the emperor. In that case, we can actually end up with
the _window thread_ being the last reference, and calling App::Close
on that thread will crash us with a E_WRONG_THREAD.

This fixes the issue by calling `TerminateProcess` explicitly.

How validated: The ES team manually ran the test pass this was
crashing in a hundred times to make sure this actually fixed it.

Co-authored-by: Leonard Hecker <lhecker@microsoft.com>
(cherry picked from commit 0d47c862c2)
Service-Card-Id: 91642489
Service-Version: 1.19
2024-01-29 13:09:24 -06:00
Leonard Hecker
e6ac014fc8 Simplify WindowEmperor::HandleCommandlineArgs (#16592)
This simplifies the function in two ways:
* Passing `nCmdShow` from `wWinMain` alleviates the need to interpret
  the return value of `GetStartupInfoW`.
* `til::env::from_current_environment()` calls `GetEnvironmentStringsW`
  to get the environment variables, while `to_string()` turns it back.
  Calling the latter directly alleviates the need for this round-trip.

(cherry picked from commit a39ac598cd)
Service-Card-Id: 91643115
Service-Version: 1.19
2024-01-29 13:09:22 -06:00
Mike Griese
0d47c862c2 Use TerminateProcess to exit early (#16575)
Closes MSFT:46744208

BODGY: If the emperor is being dtor'd, it's because we've gone past the
end of main, and released the ref in main. Then we might run into an
edge case where main releases it's ref to the emperor, but one of the
window threads might be in the process of exiting, and still holding a
strong ref to the emperor. In that case, we can actually end up with
the _window thread_ being the last reference, and calling App::Close
on that thread will crash us with a E_WRONG_THREAD.

This fixes the issue by calling `TerminateProcess` explicitly.

How validated: The ES team manually ran the test pass this was
crashing in a hundred times to make sure this actually fixed it. 

Co-authored-by: Leonard Hecker <lhecker@microsoft.com>
2024-01-26 00:28:41 +00:00
Leonard Hecker
a39ac598cd Simplify WindowEmperor::HandleCommandlineArgs (#16592)
This simplifies the function in two ways:
* Passing `nCmdShow` from `wWinMain` alleviates the need to interpret
  the return value of `GetStartupInfoW`.
* `til::env::from_current_environment()` calls `GetEnvironmentStringsW`
  to get the environment variables, while `to_string()` turns it back.
  Calling the latter directly alleviates the need for this round-trip.
2024-01-25 14:51:08 -08:00
glenrgordon
b08dc61a9c Eliminate two memory leaks (#16597)
In WindowsTerminal, there was a leak of a BSTR with every call to
ITextRangeProvider::GetText, and a failure to call VariantClear in
ITextRange::GetAttributeValue when the value stored in the variant is
VT_BSTR. These were fixed by switching to wil::unique_bstr and
wil::unique_variant.

(cherry picked from commit da99d892f4)
Service-Card-Id: 91631736
Service-Version: 1.19
2024-01-25 16:46:02 -06:00
James Holderness
ba6f1e905d Add support for the DECST8C escape sequence (#16534)
## Summary of the Pull Request

This PR adds support for the `DECST8C` escape sequence, which resets the
tab stops to every 8 columns.

## Detailed Description of the Pull Request / Additional comments

This is actually a private parameter variant of the ANSI `CTC` sequence
(Cursor Tabulation Control), which accepts a selective parameter which
specifies the type of tab operation to be performed. But the DEC variant
only defines a single parameter value (5), which resets all tab stops.
It also considers an omitted parameter to be the equivalent of 5, so we
support that too.

## Validation Steps Performed

I've extended the existing tab stop tests in `ScreenBufferTests` with
some basic coverage of this sequence.

I've also manually verified that the `DECTABSR` script in #14984 now
passes the `DECST8C` portion of the test.

## PR Checklist
- [x] Closes #16533
- [x] Tests added/passed

(cherry picked from commit f5898886be)
Service-Card-Id: 91631721
Service-Version: 1.19
2024-01-25 16:46:01 -06:00
Dustin L. Howett
bc452c61dc Revert "Add magic incantation to tell Store we support Server" (#16594)
This reverts commit abab8705fe.

It went badly, as you might imagine.

(cherry picked from commit fe65d9ac8f)
Service-Card-Id: 91620326
Service-Version: 1.19
2024-01-25 16:46:00 -06:00
James Holderness
204794f9f3 Add support for more DSR queries. (#16525)
## Summary of the Pull Request

This PR adds support for more Device Status Report (`DSR`) queries,
specifically:

* Printer Status (`DSR ?15`)
* User Defined Keys (`DSR ?25`)
* Keyboard Status (`DSR ?26`)
* Locator Status (`DSR ?55`)
* Locator Identity (`DSR ?56`)
* Data Integrity (`DSR ?75`)
* Multiple Session Status (`DSR ?85`)

## Detailed Description of the Pull Request / Additional comments

For most of these, we just need to return a `DSR` sequence indicating
that the functionality isn't supported.

* `DSR ?13` indicates that a printer isn't connected.
* `DSR ?23` indicates the UDK extension isn't supported.
* `DSR ?53` indicates that a locator device isn't connected
* `DSR ?57;0` indicates the locator type is unknown or not connected.
* `DSR ?83` indicates that multiple sessions aren't supported.

For the keyboard, we report `DSR ?27;0;0;5`, indicating a PC keyboard
(the `5` parameter), a "ready" status (the second `0` parameter), and an
unknown language (the first `0` parameter). In the long term, there may
be some value in identifying the actual keyboard language, but for now
this should be good enough.

The data integrity report was originally used to detect communication
errors between the terminal and host, but that's not really applicable
for modern terminals, so we always just report `DSR ?70`, indicating
that there are no errors.

## Validation Steps Performed

I've added some more adapter tests and output engine tests covering the
new reports.

## PR Checklist
- [x] Closes #16518
- [x] Tests added/passed

(cherry picked from commit 6c192d15be)
Service-Card-Id: 91631713
Service-Version: 1.19
2024-01-25 16:45:59 -06:00
Leonard Hecker
4902b342ef Avoid timer ticks on frozen windows (#16587)
At the time of writing, closing the last tab of a window inexplicably
doesn't lead to the destruction of the remaining TermControl instance.
On top of that, on Win10 we don't destroy window threads due to bugs in
DesktopWindowXamlSource. In other words, we leak TermControl instances.

Additionally, the XAML timer class is "self-referential".
Releasing all references to an instance will not stop the timer.
Only calling Stop() explicitly will achieve that.

The result is that the message loop of a frozen window thread has so
far received 1-2 messages per second due to the blink timer not being
stopped. This may have filled the message queue and lead to bugs as
described in #16332 where keyboard input stopped working.

(cherry picked from commit 521a300c17)
Service-Card-Id: 91642474
Service-Version: 1.19
2024-01-25 16:45:58 -06:00
Carlos Zamora
03aa8a6231 Update SUI Color Scheme colors' AutoProp.Name and ToolTip (#16544)
In the Settings UI's Color Scheme page (where you edit the color scheme itself), update the color chip buttons to include the RGB value in the tooltip and screen reader announcements.

Closes #15985
Closes #15983

## Validation Steps Performed
Tooltip and screen reader announcement is updated on launch and when a new value is selected.

(cherry picked from commit 057183b651)
Service-Card-Id: 91642735
Service-Version: 1.19
2024-01-25 16:45:56 -06:00
Tushar Singh
e75a4be4fe Fix overlapping disclaimer text in Profiles' Defaults section (#16602)
Fix overlapping disclaimer text in Profiles' Defaults section

In #16261, when we removed ScrollViewer from the subpages in the
settings UI, the main Grid child element order was not preserved and as
a result, the disclaimer text overlapped with the main content on the
page.

To fix that we now apply (the lost) `Grid.Row` property on the parent
StackPanel of the main content.

### Validation Steps Performed
- Disclaimer text does not overlap.

### PR Checklist
- [x] Tests added/passed
2024-01-25 13:58:23 -08:00
glenrgordon
da99d892f4 Eliminate two memory leaks (#16597)
In WindowsTerminal, there was a leak of a BSTR with every call to
ITextRangeProvider::GetText, and a failure to call VariantClear in
ITextRange::GetAttributeValue when the value stored in the variant is
VT_BSTR. These were fixed by switching to wil::unique_bstr and
wil::unique_variant.
2024-01-25 15:57:37 +00:00
James Holderness
f5898886be Add support for the DECST8C escape sequence (#16534)
## Summary of the Pull Request

This PR adds support for the `DECST8C` escape sequence, which resets the
tab stops to every 8 columns.

## Detailed Description of the Pull Request / Additional comments

This is actually a private parameter variant of the ANSI `CTC` sequence
(Cursor Tabulation Control), which accepts a selective parameter which
specifies the type of tab operation to be performed. But the DEC variant
only defines a single parameter value (5), which resets all tab stops.
It also considers an omitted parameter to be the equivalent of 5, so we
support that too.

## Validation Steps Performed

I've extended the existing tab stop tests in `ScreenBufferTests` with
some basic coverage of this sequence.

I've also manually verified that the `DECTABSR` script in #14984 now
passes the `DECST8C` portion of the test.

## PR Checklist
- [x] Closes #16533
- [x] Tests added/passed
2024-01-24 12:02:16 +00:00
Dustin L. Howett
fe65d9ac8f Revert "Add magic incantation to tell Store we support Server" (#16594)
This reverts commit abab8705fe.

It went badly, as you might imagine.
2024-01-24 06:02:01 -06:00
James Holderness
6c192d15be Add support for more DSR queries. (#16525)
## Summary of the Pull Request

This PR adds support for more Device Status Report (`DSR`) queries,
specifically:

* Printer Status (`DSR ?15`)
* User Defined Keys (`DSR ?25`)
* Keyboard Status (`DSR ?26`)
* Locator Status (`DSR ?55`)
* Locator Identity (`DSR ?56`)
* Data Integrity (`DSR ?75`)
* Multiple Session Status (`DSR ?85`)

## Detailed Description of the Pull Request / Additional comments

For most of these, we just need to return a `DSR` sequence indicating
that the functionality isn't supported.

* `DSR ?13` indicates that a printer isn't connected.
* `DSR ?23` indicates the UDK extension isn't supported.
* `DSR ?53` indicates that a locator device isn't connected
* `DSR ?57;0` indicates the locator type is unknown or not connected.
* `DSR ?83` indicates that multiple sessions aren't supported.

For the keyboard, we report `DSR ?27;0;0;5`, indicating a PC keyboard
(the `5` parameter), a "ready" status (the second `0` parameter), and an
unknown language (the first `0` parameter). In the long term, there may
be some value in identifying the actual keyboard language, but for now
this should be good enough.

The data integrity report was originally used to detect communication
errors between the terminal and host, but that's not really applicable
for modern terminals, so we always just report `DSR ?70`, indicating
that there are no errors.

## Validation Steps Performed

I've added some more adapter tests and output engine tests covering the
new reports.

## PR Checklist
- [x] Closes #16518
- [x] Tests added/passed
2024-01-24 12:01:46 +00:00
Tushar Singh
da182e6c59 Avoid generating extra formatted copies when no action specific copyFormatting is set (#16480)
Avoid generating extra formatted copies when action's `copyFormatting`
is not present and globally set `copyFormatting` is used.

Previously, when the action's `copyFormatting` wasn't set we deferred
the decision of which formats needed to be copied to the
`TerminalPage::CopyToClipboard` handler. This meant we needed to copy
the text in all the available formats and pass it to the handler to copy
the required formats after querying the global `copyFormatting`.

To avoid making extra copies, we'll store the global `copyFormatting` in
TerminalSettings and pass it down to `TermControl`. If
`ControlCore::CopySelectionToClipboard()` doesn't receive action
specific `copyFormatting`, it will fall back to the global one _before
generating the texts_.

## Validation Steps Performed

- no `copyFormatting` set for the copy action: Copies formats according
to the global `copyFormatting`.
- `copyFormatting` is set for the copy action: Copies formats according
to the action's `copyFormatting`.
2024-01-24 12:01:38 +00:00
Leonard Hecker
521a300c17 Avoid timer ticks on frozen windows (#16587)
At the time of writing, closing the last tab of a window inexplicably
doesn't lead to the destruction of the remaining TermControl instance.
On top of that, on Win10 we don't destroy window threads due to bugs in
DesktopWindowXamlSource. In other words, we leak TermControl instances.

Additionally, the XAML timer class is "self-referential".
Releasing all references to an instance will not stop the timer.
Only calling Stop() explicitly will achieve that.

The result is that the message loop of a frozen window thread has so
far received 1-2 messages per second due to the blink timer not being
stopped. This may have filled the message queue and lead to bugs as
described in #16332 where keyboard input stopped working.
2024-01-23 18:00:27 +01:00
James Holderness
10fb5448cc Change the SUB control glyph to U+2426 (#16559)
Up to now we've using `U+2E2E` (reverse question mark) to represent the
`SUB` control glyph. This PR changes the glyph to `U+2426` (substitute
form two), which is also rendered as a reverse question mark, but is
more semantically correct.

The original `SUB` control rendering was implemented in PR #15075.

I've manually confirmed that `printf "\x1A"` is now shown as a reverse
question mark in OpenConsole when using the Cascadia Code font. That
would not previously have worked, because `U+2E2E` is not supported by
Cascadia Code.

Closes #16558

(cherry picked from commit 92f9ff948b)
Service-Card-Id: 91559316
Service-Version: 1.19
2024-01-22 16:50:25 -06:00
Dustin L. Howett
a24afcd1e6 Remove EDP auditing completely (#16460)
This pull request started out very differently. I was going to move all
the EDP code from the internal `conint` project into the public, because
EDP is [fully documented]!

Well, it doesn't have any headers in the SDK.

Or import libraries.

And it's got a deprecation notice:

> [!NOTE]
> Starting in July 2022, Microsoft is deprecating Windows Information
> Protection (WIP) and the APIs that support WIP. Microsoft will
continue
> to support WIP on supported versions of Windows. New versions of
Windows
> won't include new capabilities for WIP, and it won't be supported in
> future versions of Windows.

So I'm blasting it out the airlock instead.

[fully documented]:
https://learn.microsoft.com/en-us/windows/win32/devnotes/windows-information-protection-api

(cherry picked from commit c4c06dadad)
Service-Card-Id: 91327265
Service-Version: 1.19
2024-01-22 16:50:24 -06:00
James Holderness
92f9ff948b Change the SUB control glyph to U+2426 (#16559)
Up to now we've using `U+2E2E` (reverse question mark) to represent the
`SUB` control glyph. This PR changes the glyph to `U+2426` (substitute
form two), which is also rendered as a reverse question mark, but is
more semantically correct.

The original `SUB` control rendering was implemented in PR #15075.

I've manually confirmed that `printf "\x1A"` is now shown as a reverse
question mark in OpenConsole when using the Cascadia Code font. That
would not previously have worked, because `U+2E2E` is not supported by
Cascadia Code.

Closes #16558
2024-01-16 09:54:27 -08:00
Mike Griese
1726176c85 Merge remote-tracking branch 'origin/main' into dev/migrie/fhl/7718-notifications-reboot 2023-08-28 08:53:09 -05:00
Mike Griese
3b02c96bd5 summon didn't work but the rest did 2023-08-24 17:00:56 -05:00
Mike Griese
dc448b4781 Merge branch 'dev/migrie/fhl/7718-notifications' into dev/migrie/fhl/7718-notifications-reboot 2023-08-24 16:51:35 -05:00
Mike Griese
75ea5f3aab Merge remote-tracking branch 'origin/main' into dev/migrie/fhl/7718-notifications
# Conflicts:
#	src/cascadia/TerminalControl/ControlCore.h
#	src/cascadia/TerminalControl/ControlCore.idl
#	src/terminal/adapter/ITerminalApi.hpp
2023-03-03 12:05:48 -06:00
Mike Griese
6ac5137ba8 unpackaged and elevated hate him 2022-12-01 12:45:55 -06:00
Mike Griese
7b524b0d31 simple nits from review 2022-12-01 09:53:53 -06:00
Mike Griese
654416cdc1 Merge remote-tracking branch 'origin/main' into dev/migrie/fhl/7718-notifications 2022-11-30 15:52:02 -06:00
Dustin L. Howett
cae6f04cfb Migrate spelling-0.0.21 changes from main 2022-11-28 14:14:48 -06:00
Mike Griese
9208222884 I knew I forgot runformat 2022-11-28 14:14:48 -06:00
Mike Griese
8b67ed7779 more more austinmode 2022-11-28 13:56:14 -06:00
Mike Griese
c4f623aaf0 derp 2022-11-28 10:06:45 -06:00
Mike Griese
3d83cc348a austinmode 2022-11-22 15:51:15 -06:00
Mike Griese
8e170eb643 oops 2022-11-22 11:47:19 -06:00
Mike Griese
0f339d2498 revert some dead code 2022-11-22 08:44:36 -06:00
Mike Griese
054f173995 cleanup 2022-11-22 08:41:49 -06:00
Mike Griese
1fd87fecdf only send when inactive 2022-11-22 06:43:32 -06:00
Mike Griese
e9b2e5184a Plumbing is always the most work 2022-11-21 16:43:20 -06:00
Mike Griese
f0f75dcdd0 Actually parse parameters from the notification 2022-11-21 14:59:24 -06:00
Mike Griese
006da6a549 As a test, hook this up to BELs 2022-11-21 14:36:18 -06:00
Mike Griese
67d854821f Merge branch 'main' into dev/migrie/fhl/7718-notifications 2022-11-21 13:22:46 -06:00
Mike Griese
65bc163da4 stash, I never finished this before my kid was born 2022-10-31 16:02:00 -05:00
Mike Griese
ce375fa7f3 send notifications, and get callbacks (in an entirely new instance. Huh.) 2022-09-21 06:54:23 -05:00
141 changed files with 3868 additions and 2185 deletions

View File

@@ -117,6 +117,7 @@ uiatextrange
UIs
und
unregister
urxvt
versioned
vsdevcmd
walkthrough

View File

@@ -183,6 +183,7 @@ chh
chshdng
CHT
Cic
CLASSSTRING
CLE
cleartype
CLICKACTIVE
@@ -319,6 +320,7 @@ ctlseqs
CTRLEVENT
CTRLFREQUENCY
CTRLKEYSHORTCUTS
Ctrls
CTRLVOLUME
Ctxt
CUF
@@ -401,6 +403,7 @@ DECECM
DECEKBD
DECERA
DECFI
DECFNK
DECFRA
DECIC
DECID
@@ -443,6 +446,7 @@ DECSLPP
DECSLRM
DECSMKR
DECSR
DECST
DECSTBM
DECSTGLT
DECSTR
@@ -481,6 +485,7 @@ directio
DIRECTX
DISABLEDELAYEDEXPANSION
DISABLENOSCROLL
DISPATCHNOTIFY
DISPLAYATTRIBUTE
DISPLAYATTRIBUTEPROPERTY
DISPLAYCHANGE
@@ -1111,8 +1116,8 @@ msix
msrc
MSVCRTD
MTSM
munged
munges
Munged
murmurhash
muxes
myapplet
@@ -1868,7 +1873,12 @@ uiautomationcore
uielem
UIELEMENTENABLEDONLY
UINTs
ul
ulcch
uld
uldb
uldash
ulwave
Unadvise
unattend
UNCPRIORITY

View File

@@ -247,7 +247,7 @@ extends:
- stage: Publish
displayName: Publish
dependsOn: [Build, Package]
dependsOn: [Build]
jobs:
- template: ./build/pipelines/templates-v2/job-publish-symbols.yml@self
parameters:

View File

@@ -3,9 +3,9 @@
<!-- This file is read by XES, which we use in our Release builds. -->
<PropertyGroup Label="Version">
<XesUseOneStoreVersioning>true</XesUseOneStoreVersioning>
<XesBaseYearForStoreVersion>2023</XesBaseYearForStoreVersion>
<XesBaseYearForStoreVersion>2024</XesBaseYearForStoreVersion>
<VersionMajor>1</VersionMajor>
<VersionMinor>20</VersionMinor>
<VersionMinor>21</VersionMinor>
<VersionInfoProductName>Windows Terminal</VersionInfoProductName>
</PropertyGroup>
</Project>

View File

@@ -9,7 +9,7 @@
<package id="Microsoft.VisualStudio.Setup.Configuration.Native" version="2.3.2262" targetFramework="native" developmentDependency="true" />
<package id="Microsoft.UI.Xaml" version="2.8.4" targetFramework="native" />
<package id="Microsoft.Web.WebView2" version="1.0.1661.34" targetFramework="native" />
<package id="Microsoft.Windows.ImplementationLibrary" version="1.0.230824.2" targetFramework="native" developmentDependency="true" />
<package id="Microsoft.Windows.ImplementationLibrary" version="1.0.240122.1" targetFramework="native" developmentDependency="true" />
<!-- Managed packages -->
<package id="Appium.WebDriver" version="3.0.0.2" targetFramework="net45" />

View File

@@ -404,6 +404,18 @@ til::CoordType ROW::AdjustToGlyphStart(til::CoordType column) const noexcept
return _adjustBackward(_clampedColumn(column));
}
// Returns the (exclusive) ending column of the glyph at the given column.
// In other words, if you have 3 wide glyphs
// AA BB CC
// 01 23 45 <-- column
// Examples:
// - `AdjustToGlyphEnd(4)` returns 6.
// - `AdjustToGlyphEnd(3)` returns 4.
til::CoordType ROW::AdjustToGlyphEnd(til::CoordType column) const noexcept
{
return _adjustForward(_clampedColumnInclusive(column));
}
// Routine Description:
// - clears char data in column in row
// Arguments:
@@ -939,36 +951,10 @@ uint16_t ROW::size() const noexcept
return _columnCount;
}
til::CoordType ROW::MeasureLeft() const noexcept
// Routine Description:
// - Retrieves the column that is one after the last non-space character in the row.
til::CoordType ROW::GetLastNonSpaceColumn() const noexcept
{
const auto text = GetText();
const auto beg = text.begin();
const auto end = text.end();
auto it = beg;
for (; it != end; ++it)
{
if (*it != L' ')
{
break;
}
}
return gsl::narrow_cast<til::CoordType>(it - beg);
}
til::CoordType ROW::MeasureRight() const noexcept
{
if (_wrapForced)
{
auto width = _columnCount;
if (_doubleBytePadded)
{
width--;
}
return width;
}
const auto text = GetText();
const auto beg = text.begin();
const auto end = text.end();
@@ -988,7 +974,42 @@ til::CoordType ROW::MeasureRight() const noexcept
//
// An example: The row is 10 cells wide and `it` points to the second character.
// `it - beg` would return 1, but it's possible it's actually 1 wide glyph and 8 whitespace.
return gsl::narrow_cast<til::CoordType>(_columnCount - (end - it));
return gsl::narrow_cast<til::CoordType>(GetReadableColumnCount() - (end - it));
}
til::CoordType ROW::MeasureLeft() const noexcept
{
const auto text = GetText();
const auto beg = text.begin();
const auto end = text.end();
auto it = beg;
for (; it != end; ++it)
{
if (*it != L' ')
{
break;
}
}
return gsl::narrow_cast<til::CoordType>(it - beg);
}
// Routine Description:
// - Retrieves the column that is one after the last valid character in the row.
til::CoordType ROW::MeasureRight() const noexcept
{
if (_wrapForced)
{
auto width = _columnCount;
if (_doubleBytePadded)
{
width--;
}
return width;
}
return GetLastNonSpaceColumn();
}
bool ROW::ContainsText() const noexcept

View File

@@ -137,6 +137,7 @@ public:
til::CoordType NavigateToPrevious(til::CoordType column) const noexcept;
til::CoordType NavigateToNext(til::CoordType column) const noexcept;
til::CoordType AdjustToGlyphStart(til::CoordType column) const noexcept;
til::CoordType AdjustToGlyphEnd(til::CoordType column) const noexcept;
void ClearCell(til::CoordType column);
OutputCellIterator WriteCells(OutputCellIterator it, til::CoordType columnBegin, std::optional<bool> wrap = std::nullopt, std::optional<til::CoordType> limitRight = std::nullopt);
@@ -151,6 +152,7 @@ public:
TextAttribute GetAttrByColumn(til::CoordType column) const;
std::vector<uint16_t> GetHyperlinks() const;
uint16_t size() const noexcept;
til::CoordType GetLastNonSpaceColumn() const noexcept;
til::CoordType MeasureLeft() const noexcept;
til::CoordType MeasureRight() const noexcept;
bool ContainsText() const noexcept;

View File

@@ -126,6 +126,8 @@ void TextBuffer::_reserve(til::size screenBufferSize, const TextAttribute& defau
// The compiler doesn't understand the likelihood of our branches. (PGO does, but that's imperfect.)
__declspec(noinline) void TextBuffer::_commit(const std::byte* row)
{
assert(row >= _commitWatermark);
const auto rowEnd = row + _bufferRowStride;
const auto remaining = gsl::narrow_cast<uintptr_t>(_bufferEnd - _commitWatermark);
const auto minimum = gsl::narrow_cast<uintptr_t>(rowEnd - _commitWatermark);
@@ -146,7 +148,7 @@ void TextBuffer::_decommit() noexcept
_commitWatermark = _buffer.get();
}
// Constructs ROWs up to (excluding) the ROW pointed to by `until`.
// Constructs ROWs between [_commitWatermark,until).
void TextBuffer::_construct(const std::byte* until) noexcept
{
for (; _commitWatermark < until; _commitWatermark += _bufferRowStride)
@@ -158,8 +160,7 @@ void TextBuffer::_construct(const std::byte* until) noexcept
}
}
// Destroys all previously constructed ROWs.
// Be careful! This doesn't reset any of the members, in particular the _commitWatermark.
// Destructs ROWs between [_buffer,_commitWatermark).
void TextBuffer::_destroy() const noexcept
{
for (auto it = _buffer.get(); it < _commitWatermark; it += _bufferRowStride)
@@ -168,9 +169,8 @@ void TextBuffer::_destroy() const noexcept
}
}
// This function is "direct" because it trusts the caller to properly wrap the "offset"
// parameter modulo the _height of the buffer, etc. But keep in mind that a offset=0
// is the GetScratchpadRow() and not the GetRowByOffset(0). That one is offset=1.
// This function is "direct" because it trusts the caller to properly
// wrap the "offset" parameter modulo the _height of the buffer.
ROW& TextBuffer::_getRowByOffsetDirect(size_t offset)
{
const auto row = _buffer.get() + _bufferRowStride * offset;
@@ -184,6 +184,7 @@ ROW& TextBuffer::_getRowByOffsetDirect(size_t offset)
return *reinterpret_cast<ROW*>(row);
}
// See GetRowByOffset().
ROW& TextBuffer::_getRow(til::CoordType y) const
{
// Rows are stored circularly, so the index you ask for is offset by the start position and mod the total of rows.
@@ -197,6 +198,7 @@ ROW& TextBuffer::_getRow(til::CoordType y) const
}
// We add 1 to the row offset, because row "0" is the one returned by GetScratchpadRow().
// See GetScratchpadRow() for more explanation.
#pragma warning(suppress : 26492) // Don't use const_cast to cast away const or volatile (type.3).
return const_cast<TextBuffer*>(this)->_getRowByOffsetDirect(gsl::narrow_cast<size_t>(offset) + 1);
}
@@ -238,6 +240,9 @@ ROW& TextBuffer::GetScratchpadRow()
// Returns a row filled with whitespace and the given attributes, for you to freely use.
ROW& TextBuffer::GetScratchpadRow(const TextAttribute& attributes)
{
// The scratchpad row is mapped to the underlying index 0, whereas all regular rows are mapped to
// index 1 and up. We do it this way instead of the other way around (scratchpad row at index _height),
// because that would force us to MEM_COMMIT the entire buffer whenever this function is called.
auto& r = _getRowByOffsetDirect(0);
r.Reset(attributes);
return r;
@@ -902,15 +907,14 @@ til::point TextBuffer::GetLastNonSpaceCharacter(const Viewport* viewOptional) co
// If the X coordinate turns out to be -1, the row was empty, we need to search backwards for the real end of text.
const auto viewportTop = viewport.Top();
auto fDoBackUp = (coordEndOfText.x < 0 && coordEndOfText.y > viewportTop); // this row is empty, and we're not at the top
while (fDoBackUp)
// while (this row is empty, and we're not at the top)
while (coordEndOfText.x < 0 && coordEndOfText.y > viewportTop)
{
coordEndOfText.y--;
const auto& backupRow = GetRowByOffset(coordEndOfText.y);
// We need to back up to the previous row if this line is empty, AND there are more rows
coordEndOfText.x = backupRow.MeasureRight() - 1;
fDoBackUp = (coordEndOfText.x < 0 && coordEndOfText.y > viewportTop);
}
// don't allow negative results
@@ -1146,6 +1150,39 @@ void TextBuffer::Reset() noexcept
_initialAttributes = _currentAttributes;
}
void TextBuffer::ClearScrollback(const til::CoordType start, const til::CoordType height)
{
if (start <= 0)
{
return;
}
if (height <= 0)
{
_decommit();
return;
}
// Our goal is to move the viewport to the absolute start of the underlying memory buffer so that we can
// MEM_DECOMMIT the remaining memory. _firstRow is used to make the TextBuffer behave like a circular buffer.
// The start parameter is relative to the _firstRow. The trick to get the content to the absolute start
// is to simply add _firstRow ourselves and then reset it to 0. This causes ScrollRows() to write into
// the absolute start while reading from relative coordinates. This works because GetRowByOffset()
// operates modulo the buffer height and so the possibly-too-large startAbsolute won't be an issue.
const auto startAbsolute = _firstRow + start;
_firstRow = 0;
ScrollRows(startAbsolute, height, -startAbsolute);
const auto end = _estimateOffsetOfLastCommittedRow();
for (auto y = height; y <= end; ++y)
{
GetMutableRowByOffset(y).Reset(_initialAttributes);
}
ScrollMarks(-start);
ClearMarksInRange(til::point{ 0, height }, til::point{ _width, _height });
}
// Routine Description:
// - This is the legacy screen resize with minimal changes
// Arguments:
@@ -1916,135 +1953,6 @@ void TextBuffer::_ExpandTextRow(til::inclusive_rect& textRow) const
}
}
// Routine Description:
// - Retrieves the text data from the selected region and presents it in a clipboard-ready format (given little post-processing).
// Arguments:
// - includeCRLF - inject CRLF pairs to the end of each line
// - trimTrailingWhitespace - remove the trailing whitespace at the end of each line
// - textRects - the rectangular regions from which the data will be extracted from the buffer (i.e.: selection rects)
// - GetAttributeColors - function used to map TextAttribute to RGB COLORREFs. If null, only extract the text.
// - formatWrappedRows - if set we will apply formatting (CRLF inclusion and whitespace trimming) on wrapped rows
// Return Value:
// - The text, background color, and foreground color data of the selected region of the text buffer.
const TextBuffer::TextAndColor TextBuffer::GetText(const bool includeCRLF,
const bool trimTrailingWhitespace,
const std::vector<til::inclusive_rect>& selectionRects,
std::function<std::pair<COLORREF, COLORREF>(const TextAttribute&)> GetAttributeColors,
const bool formatWrappedRows) const
{
TextAndColor data;
const auto copyTextColor = GetAttributeColors != nullptr;
// preallocate our vectors to reduce reallocs
const auto rows = selectionRects.size();
data.text.reserve(rows);
if (copyTextColor)
{
data.FgAttr.reserve(rows);
data.BkAttr.reserve(rows);
}
// for each row in the selection
for (size_t i = 0; i < rows; i++)
{
const auto iRow = selectionRects.at(i).top;
const auto highlight = Viewport::FromInclusive(selectionRects.at(i));
// retrieve the data from the screen buffer
auto it = GetCellDataAt(highlight.Origin(), highlight);
// allocate a string buffer
std::wstring selectionText;
std::vector<COLORREF> selectionFgAttr;
std::vector<COLORREF> selectionBkAttr;
// preallocate to avoid reallocs
selectionText.reserve(gsl::narrow<size_t>(highlight.Width()) + 2); // + 2 for \r\n if we munged it
if (copyTextColor)
{
selectionFgAttr.reserve(gsl::narrow<size_t>(highlight.Width()) + 2);
selectionBkAttr.reserve(gsl::narrow<size_t>(highlight.Width()) + 2);
}
// copy char data into the string buffer, skipping trailing bytes
while (it)
{
const auto& cell = *it;
if (cell.DbcsAttr() != DbcsAttribute::Trailing)
{
const auto chars = cell.Chars();
selectionText.append(chars);
if (copyTextColor)
{
const auto cellData = cell.TextAttr();
const auto [CellFgAttr, CellBkAttr] = GetAttributeColors(cellData);
for (size_t j = 0; j < chars.size(); ++j)
{
selectionFgAttr.push_back(CellFgAttr);
selectionBkAttr.push_back(CellBkAttr);
}
}
}
++it;
}
// We apply formatting to rows if the row was NOT wrapped or formatting of wrapped rows is allowed
const auto shouldFormatRow = formatWrappedRows || !GetRowByOffset(iRow).WasWrapForced();
if (trimTrailingWhitespace)
{
if (shouldFormatRow)
{
// remove the spaces at the end (aka trim the trailing whitespace)
while (!selectionText.empty() && selectionText.back() == UNICODE_SPACE)
{
selectionText.pop_back();
if (copyTextColor)
{
selectionFgAttr.pop_back();
selectionBkAttr.pop_back();
}
}
}
}
// apply CR/LF to the end of the final string, unless we're the last line.
// a.k.a if we're earlier than the bottom, then apply CR/LF.
if (includeCRLF && i < selectionRects.size() - 1)
{
if (shouldFormatRow)
{
// then we can assume a CR/LF is proper
selectionText.push_back(UNICODE_CARRIAGERETURN);
selectionText.push_back(UNICODE_LINEFEED);
if (copyTextColor)
{
// can't see CR/LF so just use black FG & BK
const auto Blackness = RGB(0x00, 0x00, 0x00);
selectionFgAttr.push_back(Blackness);
selectionFgAttr.push_back(Blackness);
selectionBkAttr.push_back(Blackness);
selectionBkAttr.push_back(Blackness);
}
}
}
data.text.emplace_back(std::move(selectionText));
if (copyTextColor)
{
data.FgAttr.emplace_back(std::move(selectionFgAttr));
data.BkAttr.emplace_back(std::move(selectionBkAttr));
}
}
return data;
}
size_t TextBuffer::SpanLength(const til::point coordStart, const til::point coordEnd) const
{
const auto bufferSize = GetSize();
@@ -2083,186 +1991,292 @@ std::wstring TextBuffer::GetPlainText(const til::point& start, const til::point&
}
// Routine Description:
// - Generates a CF_HTML compliant structure based on the passed in text and color data
// - Given a copy request and a row, retrieves the row bounds [begin, end) and
// a boolean indicating whether a line break should be added to this row.
// Arguments:
// - rows - the text and color data we will format & encapsulate
// - backgroundColor - default background color for characters, also used in padding
// - req - the copy request
// - iRow - the row index
// - row - the row
// Return Value:
// - The row bounds and a boolean for line break
std::tuple<til::CoordType, til::CoordType, bool> TextBuffer::_RowCopyHelper(const TextBuffer::CopyRequest& req, const til::CoordType iRow, const ROW& row) const
{
til::CoordType rowBeg = 0;
til::CoordType rowEnd = 0;
if (req.blockSelection)
{
const auto lineRendition = row.GetLineRendition();
const auto minX = req.bufferCoordinates ? req.minX : ScreenToBufferLine(til::point{ req.minX, iRow }, lineRendition).x;
const auto maxX = req.bufferCoordinates ? req.maxX : ScreenToBufferLine(til::point{ req.maxX, iRow }, lineRendition).x;
rowBeg = minX;
rowEnd = maxX + 1; // +1 to get an exclusive end
}
else
{
const auto lineRendition = row.GetLineRendition();
const auto beg = req.bufferCoordinates ? req.beg : ScreenToBufferLine(req.beg, lineRendition);
const auto end = req.bufferCoordinates ? req.end : ScreenToBufferLine(req.end, lineRendition);
rowBeg = iRow != beg.y ? 0 : beg.x;
rowEnd = iRow != end.y ? row.GetReadableColumnCount() : end.x + 1; // +1 to get an exclusive end
}
// Our selection mechanism doesn't stick to glyph boundaries at the moment.
// We need to adjust begin and end points manually to avoid partially
// selected glyphs.
rowBeg = row.AdjustToGlyphStart(rowBeg);
rowEnd = row.AdjustToGlyphEnd(rowEnd);
// When `formatWrappedRows` is set, apply formatting on all rows (wrapped
// and non-wrapped), but when it's false, format non-wrapped rows only.
const auto shouldFormatRow = req.formatWrappedRows || !row.WasWrapForced();
// trim trailing whitespace
if (shouldFormatRow && req.trimTrailingWhitespace)
{
rowEnd = std::min(rowEnd, row.GetLastNonSpaceColumn());
}
// line breaks
const auto addLineBreak = shouldFormatRow && req.includeLineBreak;
return { rowBeg, rowEnd, addLineBreak };
}
// Routine Description:
// - Retrieves the text data from the buffer and presents it in a clipboard-ready format.
// Arguments:
// - req - the copy request having the bounds of the selected region and other related configuration flags.
// Return Value:
// - The text data from the selected region of the text buffer. Empty if the copy request is invalid.
std::wstring TextBuffer::GetPlainText(const CopyRequest& req) const
{
if (req.beg > req.end)
{
return {};
}
std::wstring selectedText;
for (auto iRow = req.beg.y; iRow <= req.end.y; ++iRow)
{
const auto& row = GetRowByOffset(iRow);
const auto& [rowBeg, rowEnd, addLineBreak] = _RowCopyHelper(req, iRow, row);
// save selected text
selectedText += row.GetText(rowBeg, rowEnd);
if (addLineBreak && iRow != req.end.y)
{
selectedText += L"\r\n";
}
}
return selectedText;
}
// Routine Description:
// - Generates a CF_HTML compliant structure from the selected region of the buffer
// Arguments:
// - req - the copy request having the bounds of the selected region and other related configuration flags.
// - fontHeightPoints - the unscaled font height
// - fontFaceName - the name of the font used
// - backgroundColor - default background color for characters, also used in padding
// - isIntenseBold - true if being intense is treated as being bold
// - GetAttributeColors - function to get the colors of the text attributes as they're rendered
// Return Value:
// - string containing the generated HTML
std::string TextBuffer::GenHTML(const TextAndColor& rows,
// - string containing the generated HTML. Empty if the copy request is invalid.
std::string TextBuffer::GenHTML(const CopyRequest& req,
const int fontHeightPoints,
const std::wstring_view fontFaceName,
const COLORREF backgroundColor)
const COLORREF backgroundColor,
const bool isIntenseBold,
std::function<std::tuple<COLORREF, COLORREF, COLORREF>(const TextAttribute&)> GetAttributeColors) const noexcept
{
// GH#5347 - Don't provide a title for the generated HTML, as many
// web applications will paste the title first, followed by the HTML
// content, which is unexpected.
if (req.beg > req.end)
{
return {};
}
try
{
std::ostringstream htmlBuilder;
std::string htmlBuilder;
// First we have to add some standard
// HTML boiler plate required for CF_HTML
// as part of the HTML Clipboard format
const std::string htmlHeader =
"<!DOCTYPE><HTML><HEAD></HEAD><BODY>";
htmlBuilder << htmlHeader;
// First we have to add some standard HTML boiler plate required for
// CF_HTML as part of the HTML Clipboard format
constexpr std::string_view htmlHeader = "<!DOCTYPE><HTML><HEAD></HEAD><BODY>";
htmlBuilder += htmlHeader;
htmlBuilder << "<!--StartFragment -->";
htmlBuilder += "<!--StartFragment -->";
// apply global style in div element
{
htmlBuilder << "<DIV STYLE=\"";
htmlBuilder << "display:inline-block;";
htmlBuilder << "white-space:pre;";
htmlBuilder += "<DIV STYLE=\"";
htmlBuilder += "display:inline-block;";
htmlBuilder += "white-space:pre;";
fmt::format_to(std::back_inserter(htmlBuilder), FMT_COMPILE("background-color:{};"), Utils::ColorToHexString(backgroundColor));
htmlBuilder << "background-color:";
htmlBuilder << Utils::ColorToHexString(backgroundColor);
htmlBuilder << ";";
htmlBuilder << "font-family:";
htmlBuilder << "'";
htmlBuilder << ConvertToA(CP_UTF8, fontFaceName);
htmlBuilder << "',";
// even with different font, add monospace as fallback
htmlBuilder << "monospace;";
fmt::format_to(std::back_inserter(htmlBuilder), FMT_COMPILE("font-family:'{}',monospace;"), til::u16u8(fontFaceName));
htmlBuilder << "font-size:";
htmlBuilder << fontHeightPoints;
htmlBuilder << "pt;";
fmt::format_to(std::back_inserter(htmlBuilder), FMT_COMPILE("font-size:{}pt;"), fontHeightPoints);
// note: MS Word doesn't support padding (in this way at least)
htmlBuilder << "padding:";
htmlBuilder << 4; // todo: customizable padding
htmlBuilder << "px;";
// todo: customizable padding
htmlBuilder += "padding:4px;";
htmlBuilder << "\">";
htmlBuilder += "\">";
}
// copy text and info color from buffer
auto hasWrittenAnyText = false;
std::optional<COLORREF> fgColor = std::nullopt;
std::optional<COLORREF> bkColor = std::nullopt;
for (size_t row = 0; row < rows.text.size(); row++)
for (auto iRow = req.beg.y; iRow <= req.end.y; ++iRow)
{
size_t startOffset = 0;
const auto& row = GetRowByOffset(iRow);
const auto [rowBeg, rowEnd, addLineBreak] = _RowCopyHelper(req, iRow, row);
const auto rowBegU16 = gsl::narrow_cast<uint16_t>(rowBeg);
const auto rowEndU16 = gsl::narrow_cast<uint16_t>(rowEnd);
const auto runs = row.Attributes().slice(rowBegU16, rowEndU16).runs();
if (row != 0)
auto x = rowBegU16;
for (const auto& [attr, length] : runs)
{
htmlBuilder << "<BR>";
const auto nextX = gsl::narrow_cast<uint16_t>(x + length);
const auto [fg, bg, ul] = GetAttributeColors(attr);
const auto fgHex = Utils::ColorToHexString(fg);
const auto bgHex = Utils::ColorToHexString(bg);
const auto ulHex = Utils::ColorToHexString(ul);
const auto ulStyle = attr.GetUnderlineStyle();
const auto isUnderlined = ulStyle != UnderlineStyle::NoUnderline;
const auto isCrossedOut = attr.IsCrossedOut();
const auto isOverlined = attr.IsOverlined();
htmlBuilder += "<SPAN STYLE=\"";
fmt::format_to(std::back_inserter(htmlBuilder), FMT_COMPILE("color:{};"), fgHex);
fmt::format_to(std::back_inserter(htmlBuilder), FMT_COMPILE("background-color:{};"), bgHex);
if (isIntenseBold && attr.IsIntense())
{
htmlBuilder += "font-weight:bold;";
}
if (attr.IsItalic())
{
htmlBuilder += "font-style:italic;";
}
if (isCrossedOut || isOverlined)
{
fmt::format_to(std::back_inserter(htmlBuilder),
FMT_COMPILE("text-decoration:{} {} {};"),
isCrossedOut ? "line-through" : "",
isOverlined ? "overline" : "",
fgHex);
}
if (isUnderlined)
{
// Since underline, overline and strikethrough use the same css property,
// we cannot apply different colors to them at the same time. However, we
// can achieve the desired result by creating a nested <span> and applying
// underline style and color to it.
htmlBuilder += "\"><SPAN STYLE=\"";
switch (ulStyle)
{
case UnderlineStyle::NoUnderline:
break;
case UnderlineStyle::DoublyUnderlined:
fmt::format_to(std::back_inserter(htmlBuilder), FMT_COMPILE("text-decoration:underline double {};"), ulHex);
break;
case UnderlineStyle::CurlyUnderlined:
fmt::format_to(std::back_inserter(htmlBuilder), FMT_COMPILE("text-decoration:underline wavy {};"), ulHex);
break;
case UnderlineStyle::DottedUnderlined:
fmt::format_to(std::back_inserter(htmlBuilder), FMT_COMPILE("text-decoration:underline dotted {};"), ulHex);
break;
case UnderlineStyle::DashedUnderlined:
fmt::format_to(std::back_inserter(htmlBuilder), FMT_COMPILE("text-decoration:underline dashed {};"), ulHex);
break;
case UnderlineStyle::SinglyUnderlined:
default:
fmt::format_to(std::back_inserter(htmlBuilder), FMT_COMPILE("text-decoration:underline {};"), ulHex);
break;
}
}
htmlBuilder += "\">";
// text
std::string unescapedText;
THROW_IF_FAILED(til::u16u8(row.GetText(x, nextX), unescapedText));
for (const auto c : unescapedText)
{
switch (c)
{
case '<':
htmlBuilder += "&lt;";
break;
case '>':
htmlBuilder += "&gt;";
break;
case '&':
htmlBuilder += "&amp;";
break;
default:
htmlBuilder += c;
}
}
if (isUnderlined)
{
// close the nested span we created for underline
htmlBuilder += "</SPAN>";
}
htmlBuilder += "</SPAN>";
// advance to next run of text
x = nextX;
}
for (size_t col = 0; col < rows.text.at(row).length(); col++)
// never add line break to the last row.
if (addLineBreak && iRow < req.end.y)
{
const auto writeAccumulatedChars = [&](bool includeCurrent) {
if (col >= startOffset)
{
const auto unescapedText = ConvertToA(CP_UTF8, std::wstring_view(rows.text.at(row)).substr(startOffset, col - startOffset + includeCurrent));
for (const auto c : unescapedText)
{
switch (c)
{
case '<':
htmlBuilder << "&lt;";
break;
case '>':
htmlBuilder << "&gt;";
break;
case '&':
htmlBuilder << "&amp;";
break;
default:
htmlBuilder << c;
}
}
startOffset = col;
}
};
if (rows.text.at(row).at(col) == '\r' || rows.text.at(row).at(col) == '\n')
{
// do not include \r nor \n as they don't have color attributes
// and are not HTML friendly. For line break use '<BR>' instead.
writeAccumulatedChars(false);
break;
}
auto colorChanged = false;
if (!fgColor.has_value() || rows.FgAttr.at(row).at(col) != fgColor.value())
{
fgColor = rows.FgAttr.at(row).at(col);
colorChanged = true;
}
if (!bkColor.has_value() || rows.BkAttr.at(row).at(col) != bkColor.value())
{
bkColor = rows.BkAttr.at(row).at(col);
colorChanged = true;
}
if (colorChanged)
{
writeAccumulatedChars(false);
if (hasWrittenAnyText)
{
htmlBuilder << "</SPAN>";
}
htmlBuilder << "<SPAN STYLE=\"";
htmlBuilder << "color:";
htmlBuilder << Utils::ColorToHexString(fgColor.value());
htmlBuilder << ";";
htmlBuilder << "background-color:";
htmlBuilder << Utils::ColorToHexString(bkColor.value());
htmlBuilder << ";";
htmlBuilder << "\">";
}
hasWrittenAnyText = true;
// if this is the last character in the row, flush the whole row
if (col == rows.text.at(row).length() - 1)
{
writeAccumulatedChars(true);
}
htmlBuilder += "<BR>";
}
}
if (hasWrittenAnyText)
{
// last opened span wasn't closed in loop above, so close it now
htmlBuilder << "</SPAN>";
}
htmlBuilder += "</DIV>";
htmlBuilder << "</DIV>";
htmlBuilder << "<!--EndFragment -->";
htmlBuilder += "<!--EndFragment -->";
constexpr std::string_view HtmlFooter = "</BODY></HTML>";
htmlBuilder << HtmlFooter;
htmlBuilder += HtmlFooter;
// once filled with values, there will be exactly 157 bytes in the clipboard header
constexpr size_t ClipboardHeaderSize = 157;
// these values are byte offsets from start of clipboard
const auto htmlStartPos = ClipboardHeaderSize;
const auto htmlEndPos = ClipboardHeaderSize + gsl::narrow<size_t>(htmlBuilder.tellp());
const auto htmlEndPos = ClipboardHeaderSize + gsl::narrow<size_t>(htmlBuilder.length());
const auto fragStartPos = ClipboardHeaderSize + gsl::narrow<size_t>(htmlHeader.length());
const auto fragEndPos = htmlEndPos - HtmlFooter.length();
// header required by HTML 0.9 format
std::ostringstream clipHeaderBuilder;
clipHeaderBuilder << "Version:0.9\r\n";
clipHeaderBuilder << std::setfill('0');
clipHeaderBuilder << "StartHTML:" << std::setw(10) << htmlStartPos << "\r\n";
clipHeaderBuilder << "EndHTML:" << std::setw(10) << htmlEndPos << "\r\n";
clipHeaderBuilder << "StartFragment:" << std::setw(10) << fragStartPos << "\r\n";
clipHeaderBuilder << "EndFragment:" << std::setw(10) << fragEndPos << "\r\n";
clipHeaderBuilder << "StartSelection:" << std::setw(10) << fragStartPos << "\r\n";
clipHeaderBuilder << "EndSelection:" << std::setw(10) << fragEndPos << "\r\n";
std::string clipHeaderBuilder;
clipHeaderBuilder += "Version:0.9\r\n";
fmt::format_to(std::back_inserter(clipHeaderBuilder), FMT_COMPILE("StartHTML:{:0>10}\r\n"), htmlStartPos);
fmt::format_to(std::back_inserter(clipHeaderBuilder), FMT_COMPILE("EndHTML:{:0>10}\r\n"), htmlEndPos);
fmt::format_to(std::back_inserter(clipHeaderBuilder), FMT_COMPILE("StartFragment:{:0>10}\r\n"), fragStartPos);
fmt::format_to(std::back_inserter(clipHeaderBuilder), FMT_COMPILE("EndFragment:{:0>10}\r\n"), fragEndPos);
fmt::format_to(std::back_inserter(clipHeaderBuilder), FMT_COMPILE("StartSelection:{:0>10}\r\n"), fragStartPos);
fmt::format_to(std::back_inserter(clipHeaderBuilder), FMT_COMPILE("EndSelection:{:0>10}\r\n"), fragEndPos);
return clipHeaderBuilder.str() + htmlBuilder.str();
return clipHeaderBuilder + htmlBuilder;
}
catch (...)
{
@@ -2272,25 +2286,36 @@ std::string TextBuffer::GenHTML(const TextAndColor& rows,
}
// Routine Description:
// - Generates an RTF document based on the passed in text and color data
// - Generates an RTF document from the selected region of the buffer
// RTF 1.5 Spec: https://www.biblioscape.com/rtf15_spec.htm
// RTF 1.9.1 Spec: https://msopenspecs.azureedge.net/files/Archive_References/[MSFT-RTF].pdf
// Arguments:
// - rows - the text and color data we will format & encapsulate
// - backgroundColor - default background color for characters, also used in padding
// - req - the copy request having the bounds of the selected region and other related configuration flags.
// - fontHeightPoints - the unscaled font height
// - fontFaceName - the name of the font used
// - htmlTitle - value used in title tag of html header. Used to name the application
// - backgroundColor - default background color for characters, also used in padding
// - isIntenseBold - true if being intense is treated as being bold
// - GetAttributeColors - function to get the colors of the text attributes as they're rendered
// Return Value:
// - string containing the generated RTF
std::string TextBuffer::GenRTF(const TextAndColor& rows, const int fontHeightPoints, const std::wstring_view fontFaceName, const COLORREF backgroundColor)
// - string containing the generated RTF. Empty if the copy request is invalid.
std::string TextBuffer::GenRTF(const CopyRequest& req,
const int fontHeightPoints,
const std::wstring_view fontFaceName,
const COLORREF backgroundColor,
const bool isIntenseBold,
std::function<std::tuple<COLORREF, COLORREF, COLORREF>(const TextAttribute&)> GetAttributeColors) const noexcept
{
if (req.beg > req.end)
{
return {};
}
try
{
std::ostringstream rtfBuilder;
std::string rtfBuilder;
// start rtf
rtfBuilder << "{";
rtfBuilder += "{";
// Standard RTF header.
// This is similar to the header generated by WordPad.
@@ -2306,10 +2331,11 @@ std::string TextBuffer::GenRTF(const TextAndColor& rows, const int fontHeightPoi
// Some features are blocked by default to maintain compatibility
// with older programs (Eg. Word 97-2003). `nouicompat` disables this
// behavior, and unblocks these features. See: Spec 1.9.1, Pg. 51.
rtfBuilder << "\\rtf1\\ansi\\ansicpg1252\\deff0\\nouicompat";
rtfBuilder += "\\rtf1\\ansi\\ansicpg1252\\deff0\\nouicompat";
// font table
rtfBuilder << "{\\fonttbl{\\f0\\fmodern\\fcharset0 " << ConvertToA(CP_UTF8, fontFaceName) << ";}}";
// Brace escape: add an extra brace (of same kind) after a brace to escape it within the format string.
fmt::format_to(std::back_inserter(rtfBuilder), FMT_COMPILE("{{\\fonttbl{{\\f0\\fmodern\\fcharset0 {};}}}}"), til::u16u8(fontFaceName));
// map to keep track of colors:
// keys are colors represented by COLORREF
@@ -2317,8 +2343,8 @@ std::string TextBuffer::GenRTF(const TextAndColor& rows, const int fontHeightPoi
std::unordered_map<COLORREF, size_t> colorMap;
// RTF color table
std::ostringstream colorTableBuilder;
colorTableBuilder << "{\\colortbl ;";
std::string colorTableBuilder;
colorTableBuilder += "{\\colortbl ;";
const auto getColorTableIndex = [&](const COLORREF color) -> size_t {
// Exclude the 0 index for the default color, and start with 1.
@@ -2326,103 +2352,127 @@ std::string TextBuffer::GenRTF(const TextAndColor& rows, const int fontHeightPoi
const auto [it, inserted] = colorMap.emplace(color, colorMap.size() + 1);
if (inserted)
{
colorTableBuilder << "\\red" << static_cast<int>(GetRValue(color))
<< "\\green" << static_cast<int>(GetGValue(color))
<< "\\blue" << static_cast<int>(GetBValue(color))
<< ";";
const auto red = static_cast<int>(GetRValue(color));
const auto green = static_cast<int>(GetGValue(color));
const auto blue = static_cast<int>(GetBValue(color));
fmt::format_to(std::back_inserter(colorTableBuilder), FMT_COMPILE("\\red{}\\green{}\\blue{};"), red, green, blue);
}
return it->second;
};
// content
std::ostringstream contentBuilder;
contentBuilder << "\\viewkind4\\uc4";
std::string contentBuilder;
// \viewkindN: View mode of the document to be used. N=4 specifies that the document is in Normal view. (maybe unnecessary?)
// \ucN: Number of unicode fallback characters after each codepoint. (global)
contentBuilder += "\\viewkind4\\uc1";
// paragraph styles
// \fs specifies font size in half-points i.e. \fs20 results in a font size
// of 10 pts. That's why, font size is multiplied by 2 here.
contentBuilder << "\\pard\\slmult1\\f0\\fs" << std::to_string(2 * fontHeightPoints)
// Set the background color for the page. But, the
// standard way (\cbN) to do this isn't supported in Word.
// However, the following control words sequence works
// in Word (and other RTF editors also) for applying the
// text background color. See: Spec 1.9.1, Pg. 23.
<< "\\chshdng0\\chcbpat" << getColorTableIndex(backgroundColor)
<< " ";
// \pard: paragraph description
// \slmultN: line-spacing multiple
// \fN: font to be used for the paragraph, where N is the font index in the font table
contentBuilder += "\\pard\\slmult1\\f0";
std::optional<COLORREF> fgColor = std::nullopt;
std::optional<COLORREF> bkColor = std::nullopt;
for (size_t row = 0; row < rows.text.size(); ++row)
// \fsN: specifies font size in half-points. E.g. \fs20 results in a font
// size of 10 pts. That's why, font size is multiplied by 2 here.
fmt::format_to(std::back_inserter(contentBuilder), FMT_COMPILE("\\fs{}"), std::to_string(2 * fontHeightPoints));
// Set the background color for the page. But the standard way (\cbN) to do
// this isn't supported in Word. However, the following control words sequence
// works in Word (and other RTF editors also) for applying the text background
// color. See: Spec 1.9.1, Pg. 23.
fmt::format_to(std::back_inserter(contentBuilder), FMT_COMPILE("\\chshdng0\\chcbpat{}"), getColorTableIndex(backgroundColor));
for (auto iRow = req.beg.y; iRow <= req.end.y; ++iRow)
{
size_t startOffset = 0;
const auto& row = GetRowByOffset(iRow);
const auto [rowBeg, rowEnd, addLineBreak] = _RowCopyHelper(req, iRow, row);
const auto rowBegU16 = gsl::narrow_cast<uint16_t>(rowBeg);
const auto rowEndU16 = gsl::narrow_cast<uint16_t>(rowEnd);
const auto runs = row.Attributes().slice(rowBegU16, rowEndU16).runs();
if (row != 0)
auto x = rowBegU16;
for (auto& [attr, length] : runs)
{
contentBuilder << "\\line "; // new line
}
const auto nextX = gsl::narrow_cast<uint16_t>(x + length);
const auto [fg, bg, ul] = GetAttributeColors(attr);
const auto fgIdx = getColorTableIndex(fg);
const auto bgIdx = getColorTableIndex(bg);
const auto ulIdx = getColorTableIndex(ul);
const auto ulStyle = attr.GetUnderlineStyle();
for (size_t col = 0; col < rows.text.at(row).length(); ++col)
{
const auto writeAccumulatedChars = [&](bool includeCurrent) {
if (col >= startOffset)
{
const auto text = std::wstring_view{ rows.text.at(row) }.substr(startOffset, col - startOffset + includeCurrent);
_AppendRTFText(contentBuilder, text);
// start an RTF group that can be closed later to restore the
// default attribute.
contentBuilder += "{";
startOffset = col;
}
};
fmt::format_to(std::back_inserter(contentBuilder), FMT_COMPILE("\\cf{}"), fgIdx);
fmt::format_to(std::back_inserter(contentBuilder), FMT_COMPILE("\\chshdng0\\chcbpat{}"), bgIdx);
if (rows.text.at(row).at(col) == '\r' || rows.text.at(row).at(col) == '\n')
if (isIntenseBold && attr.IsIntense())
{
// do not include \r nor \n as they don't have color attributes.
// For line break use \line instead.
writeAccumulatedChars(false);
contentBuilder += "\\b";
}
if (attr.IsItalic())
{
contentBuilder += "\\i";
}
if (attr.IsCrossedOut())
{
contentBuilder += "\\strike";
}
switch (ulStyle)
{
case UnderlineStyle::NoUnderline:
break;
case UnderlineStyle::DoublyUnderlined:
fmt::format_to(std::back_inserter(contentBuilder), FMT_COMPILE("\\uldb\\ulc{}"), ulIdx);
break;
case UnderlineStyle::CurlyUnderlined:
fmt::format_to(std::back_inserter(contentBuilder), FMT_COMPILE("\\ulwave\\ulc{}"), ulIdx);
break;
case UnderlineStyle::DottedUnderlined:
fmt::format_to(std::back_inserter(contentBuilder), FMT_COMPILE("\\uld\\ulc{}"), ulIdx);
break;
case UnderlineStyle::DashedUnderlined:
fmt::format_to(std::back_inserter(contentBuilder), FMT_COMPILE("\\uldash\\ulc{}"), ulIdx);
break;
case UnderlineStyle::SinglyUnderlined:
default:
fmt::format_to(std::back_inserter(contentBuilder), FMT_COMPILE("\\ul\\ulc{}"), ulIdx);
break;
}
auto colorChanged = false;
if (!fgColor.has_value() || rows.FgAttr.at(row).at(col) != fgColor.value())
{
fgColor = rows.FgAttr.at(row).at(col);
colorChanged = true;
}
// RTF commands and the text data must be separated by a space.
// Otherwise, if the text begins with a space then that space will
// be interpreted as part of the last command, and will be lost.
contentBuilder += " ";
if (!bkColor.has_value() || rows.BkAttr.at(row).at(col) != bkColor.value())
{
bkColor = rows.BkAttr.at(row).at(col);
colorChanged = true;
}
const auto unescapedText = row.GetText(x, nextX); // including character at nextX
_AppendRTFText(contentBuilder, unescapedText);
if (colorChanged)
{
writeAccumulatedChars(false);
contentBuilder << "\\chshdng0\\chcbpat" << getColorTableIndex(bkColor.value())
<< "\\cf" << getColorTableIndex(fgColor.value())
<< " ";
}
contentBuilder += "}"; // close RTF group
// if this is the last character in the row, flush the whole row
if (col == rows.text.at(row).length() - 1)
{
writeAccumulatedChars(true);
}
// advance to next run of text
x = nextX;
}
// never add line break to the last row.
if (addLineBreak && iRow < req.end.y)
{
contentBuilder += "\\line";
}
}
// end colortbl
colorTableBuilder << "}";
// add color table to the final RTF
rtfBuilder << colorTableBuilder.str();
rtfBuilder += colorTableBuilder + "}";
// add the text content to the final RTF
rtfBuilder << contentBuilder.str();
rtfBuilder += contentBuilder + "}";
// end rtf
rtfBuilder << "}";
return rtfBuilder.str();
return rtfBuilder;
}
catch (...)
{
@@ -2431,7 +2481,7 @@ std::string TextBuffer::GenRTF(const TextAndColor& rows, const int fontHeightPoi
}
}
void TextBuffer::_AppendRTFText(std::ostringstream& contentBuilder, const std::wstring_view& text)
void TextBuffer::_AppendRTFText(std::string& contentBuilder, const std::wstring_view& text)
{
for (const auto codeUnit : text)
{
@@ -2442,16 +2492,18 @@ void TextBuffer::_AppendRTFText(std::ostringstream& contentBuilder, const std::w
case L'\\':
case L'{':
case L'}':
contentBuilder << "\\" << gsl::narrow<char>(codeUnit);
break;
contentBuilder += "\\";
[[fallthrough]];
default:
contentBuilder << gsl::narrow<char>(codeUnit);
contentBuilder += gsl::narrow_cast<char>(codeUnit);
}
}
else
{
// Windows uses unsigned wchar_t - RTF uses signed ones.
contentBuilder << "\\u" << std::to_string(til::bit_cast<int16_t>(codeUnit)) << "?";
// '?' is the fallback ascii character.
const auto codeUnitRTFStr = std::to_string(til::bit_cast<int16_t>(codeUnit));
fmt::format_to(std::back_inserter(contentBuilder), FMT_COMPILE("\\u{}?"), codeUnitRTFStr);
}
}
}

View File

@@ -194,6 +194,7 @@ public:
til::point BufferToScreenPosition(const til::point position) const;
void Reset() noexcept;
void ClearScrollback(const til::CoordType start, const til::CoordType height);
void ResizeTraditional(const til::size newSize);
@@ -229,33 +230,94 @@ public:
std::wstring GetCustomIdFromId(uint16_t id) const;
void CopyHyperlinkMaps(const TextBuffer& OtherBuffer);
class TextAndColor
{
public:
std::vector<std::wstring> text;
std::vector<std::vector<COLORREF>> FgAttr;
std::vector<std::vector<COLORREF>> BkAttr;
};
size_t SpanLength(const til::point coordStart, const til::point coordEnd) const;
const TextAndColor GetText(const bool includeCRLF,
const bool trimTrailingWhitespace,
const std::vector<til::inclusive_rect>& textRects,
std::function<std::pair<COLORREF, COLORREF>(const TextAttribute&)> GetAttributeColors = nullptr,
const bool formatWrappedRows = false) const;
std::wstring GetPlainText(const til::point& start, const til::point& end) const;
static std::string GenHTML(const TextAndColor& rows,
const int fontHeightPoints,
const std::wstring_view fontFaceName,
const COLORREF backgroundColor);
struct CopyRequest
{
// beg and end coordinates are inclusive
til::point beg;
til::point end;
static std::string GenRTF(const TextAndColor& rows,
const int fontHeightPoints,
const std::wstring_view fontFaceName,
const COLORREF backgroundColor);
til::CoordType minX;
til::CoordType maxX;
bool blockSelection = false;
bool trimTrailingWhitespace = true;
bool includeLineBreak = true;
bool formatWrappedRows = false;
// whether beg, end coordinates are in buffer coordinates or screen coordinates
bool bufferCoordinates = false;
CopyRequest() = default;
constexpr CopyRequest(const TextBuffer& buffer, const til::point& beg, const til::point& end, const bool blockSelection, const bool includeLineBreak, const bool trimTrailingWhitespace, const bool formatWrappedRows, const bool bufferCoordinates = false) noexcept :
beg{ std::max(beg, til::point{ 0, 0 }) },
end{ std::min(end, til::point{ buffer._width - 1, buffer._height - 1 }) },
minX{ std::min(this->beg.x, this->end.x) },
maxX{ std::max(this->beg.x, this->end.x) },
blockSelection{ blockSelection },
includeLineBreak{ includeLineBreak },
trimTrailingWhitespace{ trimTrailingWhitespace },
formatWrappedRows{ formatWrappedRows },
bufferCoordinates{ bufferCoordinates }
{
}
static CopyRequest FromConfig(const TextBuffer& buffer,
const til::point& beg,
const til::point& end,
const bool singleLine,
const bool blockSelection,
const bool trimBlockSelection,
const bool bufferCoordinates = false) noexcept
{
return {
buffer,
beg,
end,
blockSelection,
/* includeLineBreak */
// - SingleLine mode collapses all rows into one line, unless we're in
// block selection mode.
// - Block selection should preserve the visual structure by including
// line breaks on all rows (together with `formatWrappedRows`).
// (Selects like a box, pastes like a box)
!singleLine || blockSelection,
/* trimTrailingWhitespace */
// Trim trailing whitespace if we're not in single line mode and — either
// we're not in block selection mode or, we're in block selection mode and
// trimming is allowed.
!singleLine && (!blockSelection || trimBlockSelection),
/* formatWrappedRows */
// In block selection, we should apply formatting to wrapped rows as well.
// (Otherwise, they're only applied to non-wrapped rows.)
blockSelection,
bufferCoordinates
};
}
};
std::wstring GetPlainText(const CopyRequest& req) const;
std::string GenHTML(const CopyRequest& req,
const int fontHeightPoints,
const std::wstring_view fontFaceName,
const COLORREF backgroundColor,
const bool isIntenseBold,
std::function<std::tuple<COLORREF, COLORREF, COLORREF>(const TextAttribute&)> GetAttributeColors) const noexcept;
std::string GenRTF(const CopyRequest& req,
const int fontHeightPoints,
const std::wstring_view fontFaceName,
const COLORREF backgroundColor,
const bool isIntenseBold,
std::function<std::tuple<COLORREF, COLORREF, COLORREF>(const TextAttribute&)> GetAttributeColors) const noexcept;
struct PositionInformation
{
@@ -303,8 +365,9 @@ private:
til::point _GetWordEndForSelection(const til::point target, const std::wstring_view wordDelimiters) const;
void _PruneHyperlinks();
void _trimMarksOutsideBuffer();
std::tuple<til::CoordType, til::CoordType, bool> _RowCopyHelper(const CopyRequest& req, const til::CoordType iRow, const ROW& row) const;
static void _AppendRTFText(std::ostringstream& contentBuilder, const std::wstring_view& text);
static void _AppendRTFText(std::string& contentBuilder, const std::wstring_view& text);
Microsoft::Console::Render::Renderer& _renderer;

View File

@@ -39,7 +39,6 @@
<Dependencies>
<TargetDeviceFamily Name="Windows.Desktop" MinVersion="10.0.19041.0" MaxVersionTested="10.0.22621.0" />
<TargetDeviceFamily Name="Windows.DesktopServer" MinVersion="10.0.19041.0" MaxVersionTested="10.0.22621.0" />
</Dependencies>
<Resources>

View File

@@ -40,7 +40,6 @@
<Dependencies>
<TargetDeviceFamily Name="Windows.Desktop" MinVersion="10.0.19041.0" MaxVersionTested="10.0.22621.0" />
<TargetDeviceFamily Name="Windows.DesktopServer" MinVersion="10.0.19041.0" MaxVersionTested="10.0.22621.0" />
</Dependencies>
<Resources>

View File

@@ -40,7 +40,6 @@
<Dependencies>
<TargetDeviceFamily Name="Windows.Desktop" MinVersion="10.0.19041.0" MaxVersionTested="10.0.22621.0" />
<TargetDeviceFamily Name="Windows.DesktopServer" MinVersion="10.0.19041.0" MaxVersionTested="10.0.22621.0" />
</Dependencies>
<Resources>

View File

@@ -68,6 +68,8 @@ Author(s):
// Manually include til after we include Windows.Foundation to give it winrt superpowers
#include "til.h"
#include <SafeDispatcherTimer.h>
// Common includes for most tests:
#include "../../inc/conattrs.hpp"
#include "../../types/inc/utils.hpp"

View File

@@ -461,4 +461,35 @@ namespace winrt::Microsoft::Terminal::Remoting::implementation
co_await winrt::resume_background();
_monarch.RequestSendContent(args);
}
// Attempt to summon an existing window. This static function does NOT
// pre-register as the monarch. This is used for activations from a
// notification, where this process should NEVER become its own window.
bool WindowManager::SummonForNotification(const uint64_t windowId)
{
auto monarch = create_instance<Remoting::IMonarch>(Monarch_clsid,
CLSCTX_LOCAL_SERVER);
if (monarch == nullptr)
{
return false;
}
SummonWindowSelectionArgs args{};
args.WindowID(windowId);
// Summon the window...
// * On its current desktop
// * Without a dropdown
// * On the monitor it is already on
// * Do not toggle, just make visible.
const Remoting::SummonWindowBehavior summonArgs{};
summonArgs.MoveToCurrentDesktop(false);
summonArgs.DropdownDuration(0);
summonArgs.ToMonitor(Remoting::MonitorBehavior::InPlace);
summonArgs.ToggleVisibility(false);
args.SummonBehavior(summonArgs);
monarch.SummonWindow(args);
return true;
}
}

View File

@@ -47,6 +47,8 @@ namespace winrt::Microsoft::Terminal::Remoting::implementation
winrt::fire_and_forget RequestMoveContent(winrt::hstring window, winrt::hstring content, uint32_t tabIndex, Windows::Foundation::IReference<Windows::Foundation::Rect> windowBounds);
winrt::fire_and_forget RequestSendContent(Remoting::RequestReceiveContentArgs args);
static bool SummonForNotification(const uint64_t windowId);
TYPED_EVENT(FindTargetWindowRequested, winrt::Windows::Foundation::IInspectable, winrt::Microsoft::Terminal::Remoting::FindTargetWindowArgs);
TYPED_EVENT(WindowCreated, winrt::Windows::Foundation::IInspectable, winrt::Windows::Foundation::IInspectable);

View File

@@ -29,6 +29,8 @@ namespace Microsoft.Terminal.Remoting
void RequestMoveContent(String window, String content, UInt32 tabIndex, Windows.Foundation.IReference<Windows.Foundation.Rect> bounds);
void RequestSendContent(RequestReceiveContentArgs args);
static Boolean SummonForNotification(UInt64 windowId);
event Windows.Foundation.TypedEventHandler<Object, FindTargetWindowArgs> FindTargetWindowRequested;
event Windows.Foundation.TypedEventHandler<Object, Object> WindowCreated;

View File

@@ -1065,10 +1065,7 @@ namespace winrt::TerminalApp::implementation
{
if (termControl.HasSelection())
{
const auto selections{ termControl.SelectedText(true) };
// concatenate the selection into a single line
auto searchText = std::accumulate(selections.begin(), selections.end(), std::wstring());
std::wstring searchText{ termControl.SelectedText(true) };
// make it compact by replacing consecutive whitespaces with a single space
searchText = std::regex_replace(searchText, std::wregex(LR"(\s+)"), L" ");

View File

@@ -124,8 +124,7 @@ namespace winrt::TerminalApp::implementation
return appLogic->GetSettings();
}
AppLogic::AppLogic() :
_reloadState{ std::chrono::milliseconds(100), []() { ApplicationState::SharedInstance().Reload(); } }
AppLogic::AppLogic()
{
// For your own sanity, it's better to do setup outside the ctor.
// If you do any setup in the ctor that ends up throwing an exception,
@@ -327,10 +326,6 @@ namespace winrt::TerminalApp::implementation
{
_reloadSettings->Run();
}
else if (ApplicationState::SharedInstance().IsStatePath(modifiedBasename))
{
_reloadState();
}
});
}

View File

@@ -91,7 +91,6 @@ namespace winrt::TerminalApp::implementation
::TerminalApp::AppCommandlineArgs _settingsAppArgs;
std::shared_ptr<ThrottledFuncTrailing<>> _reloadSettings;
til::throttled_func_trailing<> _reloadState;
std::vector<Microsoft::Terminal::Settings::Model::SettingsLoadWarnings> _warnings{};

View File

@@ -1,6 +1,4 @@
#include "pch.h"
#include "ColorHelper.h"
#include <limits>
using namespace winrt::TerminalApp;

View File

@@ -1,7 +1,6 @@
#pragma once
#include "pch.h"
#include <winrt/windows.ui.core.h>
#include <winrt/Windows.UI.h>
namespace winrt::TerminalApp
{

View File

@@ -50,6 +50,7 @@ namespace winrt::Microsoft::TerminalApp::implementation
void TerminalOutput(const winrt::event_token& token) noexcept { _wrappedConnection.TerminalOutput(token); };
winrt::event_token StateChanged(const TypedEventHandler<ITerminalConnection, IInspectable>& handler) { return _wrappedConnection.StateChanged(handler); };
void StateChanged(const winrt::event_token& token) noexcept { _wrappedConnection.StateChanged(token); };
winrt::guid SessionId() const noexcept { return {}; }
ConnectionState State() const noexcept { return _wrappedConnection.State(); }
private:
@@ -98,6 +99,15 @@ namespace winrt::Microsoft::TerminalApp::implementation
_wrappedConnection = nullptr;
}
guid DebugTapConnection::SessionId() const noexcept
{
if (const auto c = _wrappedConnection.get())
{
return c.SessionId();
}
return {};
}
ConnectionState DebugTapConnection::State() const noexcept
{
if (auto strongConnection{ _wrappedConnection.get() })

View File

@@ -19,6 +19,8 @@ namespace winrt::Microsoft::TerminalApp::implementation
void WriteInput(const hstring& data);
void Resize(uint32_t rows, uint32_t columns);
void Close();
winrt::guid SessionId() const noexcept;
winrt::Microsoft::Terminal::TerminalConnection::ConnectionState State() const noexcept;
void SetInputTap(const Microsoft::Terminal::TerminalConnection::ITerminalConnection& inputTap);

View File

@@ -237,7 +237,9 @@
</ClCompile>
<ClCompile Include="Pane.cpp" />
<ClCompile Include="Pane.LayoutSizeNode.cpp" />
<ClCompile Include="ColorHelper.cpp" />
<ClCompile Include="ColorHelper.cpp">
<PrecompiledHeader>NotUsing</PrecompiledHeader>
</ClCompile>
<ClCompile Include="DebugTapConnection.cpp" />
<ClCompile Include="pch.cpp">
<PrecompiledHeader>Create</PrecompiledHeader>

View File

@@ -14,6 +14,7 @@
#include <inc/WindowingBehavior.h>
#include <LibraryResources.h>
#include <WtExeUtils.h>
#include <TerminalCore/ControlKeyStates.hpp>
#include <til/latch.h>
@@ -45,6 +46,9 @@ using namespace ::Microsoft::Console;
using namespace ::Microsoft::Terminal::Core;
using namespace std::chrono_literals;
using namespace winrt::Windows::UI::Notifications;
using namespace winrt::Windows::Data::Xml::Dom;
#define HOOKUP_ACTION(action) _actionDispatch->action({ this, &TerminalPage::_Handle##action });
namespace winrt
@@ -1210,7 +1214,7 @@ namespace winrt::TerminalApp::implementation
TerminalConnection::ITerminalConnection connection{ nullptr };
auto connectionType = profile.ConnectionType();
winrt::guid sessionGuid{};
Windows::Foundation::Collections::ValueSet valueSet;
if (connectionType == TerminalConnection::AzureConnection::ConnectionType() &&
TerminalConnection::AzureConnection::IsAzureConnectionAvailable())
@@ -1226,23 +1230,16 @@ namespace winrt::TerminalApp::implementation
connection = TerminalConnection::ConptyConnection{};
}
auto valueSet = TerminalConnection::ConptyConnection::CreateSettings(azBridgePath.native(),
L".",
L"Azure",
false,
L"",
nullptr,
settings.InitialRows(),
settings.InitialCols(),
winrt::guid(),
profile.Guid());
if constexpr (Feature_VtPassthroughMode::IsEnabled())
{
valueSet.Insert(L"passthroughMode", Windows::Foundation::PropertyValue::CreateBoolean(settings.VtPassthrough()));
}
connection.Initialize(valueSet);
valueSet = TerminalConnection::ConptyConnection::CreateSettings(azBridgePath.native(),
L".",
L"Azure",
false,
L"",
nullptr,
settings.InitialRows(),
settings.InitialCols(),
winrt::guid(),
profile.Guid());
}
else
@@ -1267,38 +1264,38 @@ namespace winrt::TerminalApp::implementation
// process until later, on another thread, after we've already
// restored the CWD to its original value.
auto newWorkingDirectory{ _evaluatePathForCwd(settings.StartingDirectory()) };
auto conhostConn = TerminalConnection::ConptyConnection();
auto valueSet = TerminalConnection::ConptyConnection::CreateSettings(settings.Commandline(),
newWorkingDirectory,
settings.StartingTitle(),
settings.ReloadEnvironmentVariables(),
_WindowProperties.VirtualEnvVars(),
environment,
settings.InitialRows(),
settings.InitialCols(),
winrt::guid(),
profile.Guid());
valueSet.Insert(L"passthroughMode", Windows::Foundation::PropertyValue::CreateBoolean(settings.VtPassthrough()));
connection = TerminalConnection::ConptyConnection{};
valueSet = TerminalConnection::ConptyConnection::CreateSettings(settings.Commandline(),
newWorkingDirectory,
settings.StartingTitle(),
settings.ReloadEnvironmentVariables(),
_WindowProperties.VirtualEnvVars(),
environment,
settings.InitialRows(),
settings.InitialCols(),
winrt::guid(),
profile.Guid());
if (inheritCursor)
{
valueSet.Insert(L"inheritCursor", Windows::Foundation::PropertyValue::CreateBoolean(true));
}
conhostConn.Initialize(valueSet);
sessionGuid = conhostConn.Guid();
connection = conhostConn;
}
if constexpr (Feature_VtPassthroughMode::IsEnabled())
{
valueSet.Insert(L"passthroughMode", Windows::Foundation::PropertyValue::CreateBoolean(settings.VtPassthrough()));
}
connection.Initialize(valueSet);
TraceLoggingWrite(
g_hTerminalAppProvider,
"ConnectionCreated",
TraceLoggingDescription("Event emitted upon the creation of a connection"),
TraceLoggingGuid(connectionType, "ConnectionTypeGuid", "The type of the connection"),
TraceLoggingGuid(profile.Guid(), "ProfileGuid", "The profile's GUID"),
TraceLoggingGuid(sessionGuid, "SessionGuid", "The WT_SESSION's GUID"),
TraceLoggingGuid(connection.SessionId(), "SessionGuid", "The WT_SESSION's GUID"),
TraceLoggingKeyword(MICROSOFT_KEYWORD_MEASURES),
TelemetryPrivacyDataTag(PDT_ProductAndServiceUsage));
@@ -1678,6 +1675,7 @@ namespace winrt::TerminalApp::implementation
{
term.CompletionsChanged({ get_weak(), &TerminalPage::_ControlCompletionsChangedHandler });
}
winrt::weak_ref<TermControl> weakTerm{ term };
term.ContextMenu().Opening([weak = get_weak(), weakTerm](auto&& sender, auto&& /*args*/) {
if (const auto& page{ weak.get() })
@@ -1691,6 +1689,7 @@ namespace winrt::TerminalApp::implementation
page->_PopulateContextMenu(weakTerm.get(), sender.try_as<MUX::Controls::CommandBarFlyout>(), true);
}
});
term.SendNotification({ get_weak(), &TerminalPage::_SendNotificationHandler });
}
// Method Description:
@@ -2593,12 +2592,9 @@ namespace winrt::TerminalApp::implementation
auto dataPack = DataPackage();
dataPack.RequestedOperation(DataPackageOperation::Copy);
// The EventArgs.Formats() is an override for the global setting "copyFormatting"
// iff it is set
auto useGlobal = copiedData.Formats() == nullptr;
auto copyFormats = useGlobal ?
_settings.GlobalSettings().CopyFormatting() :
copiedData.Formats().Value();
const auto copyFormats = copiedData.Formats() != nullptr ?
copiedData.Formats().Value() :
static_cast<CopyFormat>(0);
// copy text to dataPack
dataPack.SetText(copiedData.Text());
@@ -2631,6 +2627,75 @@ namespace winrt::TerminalApp::implementation
CATCH_LOG();
}
static wil::unique_close_clipboard_call _openClipboard(HWND hwnd)
{
bool success = false;
// OpenClipboard may fail to acquire the internal lock --> retry.
for (DWORD sleep = 10;; sleep *= 2)
{
if (OpenClipboard(hwnd))
{
success = true;
break;
}
// 10 iterations
if (sleep > 10000)
{
break;
}
Sleep(sleep);
}
return wil::unique_close_clipboard_call{ success };
}
static winrt::hstring _extractClipboard()
{
// This handles most cases of pasting text as the OS converts most formats to CF_UNICODETEXT automatically.
if (const auto handle = GetClipboardData(CF_UNICODETEXT))
{
const wil::unique_hglobal_locked lock{ handle };
const auto str = static_cast<const wchar_t*>(lock.get());
if (!str)
{
return {};
}
const auto maxLen = GlobalSize(handle) / sizeof(wchar_t);
const auto len = wcsnlen(str, maxLen);
return winrt::hstring{ str, gsl::narrow_cast<uint32_t>(len) };
}
// We get CF_HDROP when a user copied a file with Ctrl+C in Explorer and pastes that into the terminal (among others).
if (const auto handle = GetClipboardData(CF_HDROP))
{
const wil::unique_hglobal_locked lock{ handle };
const auto drop = static_cast<HDROP>(lock.get());
if (!drop)
{
return {};
}
const auto cap = DragQueryFileW(drop, 0, nullptr, 0);
if (cap == 0)
{
return {};
}
auto buffer = winrt::impl::hstring_builder{ cap };
const auto len = DragQueryFileW(drop, 0, buffer.data(), cap + 1);
if (len == 0)
{
return {};
}
return buffer.to_hstring();
}
return {};
}
// Function Description:
// - This function is called when the `TermControl` requests that we send
// it the clipboard's content.
@@ -2650,53 +2715,14 @@ namespace winrt::TerminalApp::implementation
const auto weakThis = get_weak();
const auto dispatcher = Dispatcher();
const auto globalSettings = _settings.GlobalSettings();
winrt::hstring text;
// GetClipboardData might block for up to 30s for delay-rendered contents.
co_await winrt::resume_background();
winrt::hstring text;
if (const auto clipboard = _openClipboard(nullptr))
{
// According to various reports on the internet, OpenClipboard might
// fail to acquire the internal lock, for instance due to rdpclip.exe.
for (int attempts = 1;;)
{
if (OpenClipboard(nullptr))
{
break;
}
if (attempts > 5)
{
co_return;
}
attempts++;
Sleep(10 * attempts);
}
const auto clipboardCleanup = wil::scope_exit([]() {
CloseClipboard();
});
const auto data = GetClipboardData(CF_UNICODETEXT);
if (!data)
{
co_return;
}
const auto str = static_cast<const wchar_t*>(GlobalLock(data));
if (!str)
{
co_return;
}
const auto dataCleanup = wil::scope_exit([&]() {
GlobalUnlock(data);
});
const auto maxLength = GlobalSize(data) / sizeof(wchar_t);
const auto length = wcsnlen(str, maxLength);
text = winrt::hstring{ str, gsl::narrow_cast<uint32_t>(length) };
text = _extractClipboard();
}
if (globalSettings.TrimPaste())
@@ -2938,6 +2964,110 @@ namespace winrt::TerminalApp::implementation
_ShowWindowChangedHandlers(*this, args);
}
// Method Description:
// - Handler for a control's SendNotification event. `args` will contain the
// title and body of the notification requested by the client application.
// - This will only actually send a notification when the sender is
// - in an inactive window OR
// - in an inactive tab.
winrt::fire_and_forget TerminalPage::_SendNotificationHandler(const IInspectable sender,
const Microsoft::Terminal::Control::SendNotificationArgs args)
{
// This never works as expected when we're an elevated instance. The
// notification will end up launching an unelevated instance to handle
// it, and there's no good way to get back to the elevated one.
// Possibly revisit after GH #13276.
//
// We're using CanDragDrop, because TODO! I bet this works with UAC disabled
if (!CanDragDrop())
{
co_return;
}
auto weakThis = get_weak();
co_await resume_foreground(Dispatcher());
auto page{ weakThis.get() };
if (page)
{
// If the window is inactive, we always want to send the notification.
//
// Otherwise, we only want to send the notification for panes in inactive tabs.
if (_activated)
{
auto foundControl = false;
if (const auto activeTab{ _GetFocusedTabImpl() })
{
activeTab->GetRootPane()->WalkTree([&](auto&& pane) {
if (const auto& term{ pane->GetTerminalControl() })
{
if (term == sender)
{
foundControl = true;
return;
}
}
});
}
// The control that sent this is in the active tab. We
// should only send the notification if the window was
// inactive.
if (foundControl)
{
co_return;
}
}
_sendNotification(args.Title(), args.Body());
}
}
// Actually write the payload to a XML doc and load it into a ToastNotification.
void TerminalPage::_sendNotification(const std::wstring_view title,
const std::wstring_view body)
{
// ToastNotificationManager::CreateToastNotifier doesn't work in
// unpackaged scenarios without an AUMID. We probably don't have one if
// we're unpackaged. Unpackaged isn't a wholly supported scenario
// anyways, so let's just bail.
if (!IsPackaged())
{
return;
}
static winrt::hstring xmlTemplate{ L"\
<toast>\
<visual>\
<binding template=\"ToastGeneric\">\
<text></text>\
<text></text>\
</binding>\
</visual>\
</toast>" };
XmlDocument doc;
doc.LoadXml(xmlTemplate);
// Populate with text and values
auto payload{ fmt::format(L"window={}&tabIndex=0", WindowProperties().WindowId()) };
doc.DocumentElement().SetAttribute(L"launch", payload);
doc.SelectSingleNode(L"//text[1]").InnerText(title);
doc.SelectSingleNode(L"//text[2]").InnerText(body);
// Construct the notification
ToastNotification notification{ doc };
// lazy-init
if (!_toastNotifier)
{
_toastNotifier = ToastNotificationManager::CreateToastNotifier();
}
// And show it!
_toastNotifier.Show(notification);
}
// Method Description:
// - Paste text from the Windows Clipboard to the focused terminal
void TerminalPage::_PasteText()

View File

@@ -287,6 +287,9 @@ namespace winrt::TerminalApp::implementation
__declspec(noinline) SuggestionsControl _loadSuggestionsElementSlowPath();
bool _suggestionsControlIs(winrt::Windows::UI::Xaml::Visibility visibility);
// todo! maybe move to TerminalWindow
winrt::Windows::UI::Notifications::ToastNotifier _toastNotifier{ nullptr };
winrt::Windows::Foundation::IAsyncOperation<winrt::Windows::UI::Xaml::Controls::ContentDialogResult> _ShowDialogHelper(const std::wstring_view& name);
void _ShowAboutDialog();
@@ -543,6 +546,9 @@ namespace winrt::TerminalApp::implementation
winrt::Microsoft::Terminal::Control::TermControl _senderOrActiveControl(const winrt::Windows::Foundation::IInspectable& sender);
winrt::com_ptr<TerminalTab> _senderOrFocusedTab(const IInspectable& sender);
winrt::fire_and_forget _SendNotificationHandler(const IInspectable sender, const winrt::Microsoft::Terminal::Control::SendNotificationArgs args);
void _sendNotification(const std::wstring_view title, const std::wstring_view body);
#pragma region ActionHandlers
// These are all defined in AppActionHandlers.cpp
#define ON_ALL_ACTIONS(action) DECLARE_ACTION_HANDLER(action);

View File

@@ -121,12 +121,7 @@ namespace winrt::TerminalApp::implementation
void TerminalTab::_BellIndicatorTimerTick(const Windows::Foundation::IInspectable& /*sender*/, const Windows::Foundation::IInspectable& /*e*/)
{
ShowBellIndicator(false);
// Just do a sanity check that the timer still exists before we stop it
if (_bellIndicatorTimer.has_value())
{
_bellIndicatorTimer->Stop();
_bellIndicatorTimer = std::nullopt;
}
_bellIndicatorTimer.Stop();
}
// Method Description:
@@ -356,14 +351,13 @@ namespace winrt::TerminalApp::implementation
{
ASSERT_UI_THREAD();
if (!_bellIndicatorTimer.has_value())
if (!_bellIndicatorTimer)
{
DispatcherTimer bellIndicatorTimer;
bellIndicatorTimer.Interval(std::chrono::milliseconds(2000));
bellIndicatorTimer.Tick({ get_weak(), &TerminalTab::_BellIndicatorTimerTick });
bellIndicatorTimer.Start();
_bellIndicatorTimer.emplace(std::move(bellIndicatorTimer));
_bellIndicatorTimer.Interval(std::chrono::milliseconds(2000));
_bellIndicatorTimer.Tick({ get_weak(), &TerminalTab::_BellIndicatorTimerTick });
}
_bellIndicatorTimer.Start();
}
// Method Description:

View File

@@ -152,7 +152,7 @@ namespace winrt::TerminalApp::implementation
void _Setup();
std::optional<Windows::UI::Xaml::DispatcherTimer> _bellIndicatorTimer;
SafeDispatcherTimer _bellIndicatorTimer;
void _BellIndicatorTimerTick(const Windows::Foundation::IInspectable& sender, const Windows::Foundation::IInspectable& e);
void _MakeTabViewItem() override;

View File

@@ -34,5 +34,5 @@ public:
private:
winrt::Microsoft::UI::Xaml::Controls::TeachingTip _tip;
winrt::Windows::UI::Xaml::DispatcherTimer _timer;
SafeDispatcherTimer _timer;
};

View File

@@ -27,6 +27,8 @@
#include <winrt/Windows.ApplicationModel.h>
#include <winrt/Windows.ApplicationModel.DataTransfer.h>
#include <winrt/Windows.ApplicationModel.Activation.h>
#include <winrt/Windows.Data.Xml.Dom.h>
#include <winrt/Windows.Foundation.h>
#include <winrt/Windows.Foundation.Collections.h>
#include <winrt/Windows.Foundation.Metadata.h>
@@ -35,6 +37,7 @@
#include <winrt/Windows.System.h>
#include <winrt/Windows.UI.Core.h>
#include <winrt/Windows.UI.Input.h>
#include <winrt/Windows.UI.Notifications.h>
#include <winrt/Windows.UI.Text.h>
#include <winrt/Windows.UI.ViewManagement.h>
#include <winrt/Windows.UI.Xaml.Automation.Peers.h>
@@ -83,6 +86,8 @@ TRACELOGGING_DECLARE_PROVIDER(g_hTerminalAppProvider);
// Manually include til after we include Windows.Foundation to give it winrt superpowers
#include "til.h"
#include <SafeDispatcherTimer.h>
#include <cppwinrt_utils.h>
#include <wil/cppwinrt_helpers.h> // must go after the CoreDispatcher type is defined

View File

@@ -77,8 +77,14 @@ namespace winrt::Microsoft::Terminal::TerminalConnection::implementation
{
if (settings)
{
_initialRows = gsl::narrow<til::CoordType>(winrt::unbox_value_or<uint32_t>(settings.TryLookup(L"initialRows").try_as<Windows::Foundation::IPropertyValue>(), _initialRows));
_initialCols = gsl::narrow<til::CoordType>(winrt::unbox_value_or<uint32_t>(settings.TryLookup(L"initialCols").try_as<Windows::Foundation::IPropertyValue>(), _initialCols));
_initialRows = unbox_prop_or<uint32_t>(settings, L"initialRows", _initialRows);
_initialCols = unbox_prop_or<uint32_t>(settings, L"initialCols", _initialCols);
_sessionId = unbox_prop_or<guid>(settings, L"sessionId", _sessionId);
}
if (_sessionId == guid{})
{
_sessionId = Utils::CreateGuid();
}
}

View File

@@ -8,12 +8,12 @@
#include <mutex>
#include <condition_variable>
#include "ConnectionStateHolder.h"
#include "BaseTerminalConnection.h"
#include "AzureClient.h"
namespace winrt::Microsoft::Terminal::TerminalConnection::implementation
{
struct AzureConnection : AzureConnectionT<AzureConnection>, ConnectionStateHolder<AzureConnection>
struct AzureConnection : AzureConnectionT<AzureConnection>, BaseTerminalConnection<AzureConnection>
{
static winrt::guid ConnectionType() noexcept;
static bool IsAzureConnectionAvailable() noexcept;

View File

@@ -4,13 +4,28 @@
namespace winrt::Microsoft::Terminal::TerminalConnection::implementation
{
template<typename T>
struct ConnectionStateHolder
struct BaseTerminalConnection
{
public:
ConnectionState State() const noexcept { return _connectionState; }
winrt::guid SessionId() const noexcept
{
return _sessionId;
}
ConnectionState State() const noexcept
{
return _connectionState;
}
TYPED_EVENT(StateChanged, ITerminalConnection, winrt::Windows::Foundation::IInspectable);
protected:
template<typename U>
U unbox_prop_or(const Windows::Foundation::Collections::ValueSet& blob, std::wstring_view key, U defaultValue)
{
return winrt::unbox_value_or<U>(blob.TryLookup(key).try_as<Windows::Foundation::IPropertyValue>(), defaultValue);
}
#pragma warning(push)
#pragma warning(disable : 26447) // Analyzer is still upset about noexcepts throwing even with function level try.
// Method Description:
@@ -86,6 +101,8 @@ namespace winrt::Microsoft::Terminal::TerminalConnection::implementation
return _isStateOneOf(ConnectionState::Connected);
}
winrt::guid _sessionId{};
private:
std::atomic<ConnectionState> _connectionState{ ConnectionState::NotConnected };
mutable std::mutex _stateMutex;

View File

@@ -85,18 +85,12 @@ namespace winrt::Microsoft::Terminal::TerminalConnection::implementation
auto environment = _initialEnv;
{
// Convert connection Guid to string and ignore the enclosing '{}'.
auto wsGuid{ Utils::GuidToString(_guid) };
wsGuid.pop_back();
const auto guidSubStr = std::wstring_view{ wsGuid }.substr(1);
// Ensure every connection has the unique identifier in the environment.
environment.as_map().insert_or_assign(L"WT_SESSION", guidSubStr.data());
// Convert connection Guid to string and ignore the enclosing '{}'.
environment.as_map().insert_or_assign(L"WT_SESSION", Utils::GuidToPlainString(_sessionId));
// The profile Guid does include the enclosing '{}'
const auto profileGuid{ Utils::GuidToString(_profileGuid) };
environment.as_map().insert_or_assign(L"WT_PROFILE_ID", profileGuid.data());
environment.as_map().insert_or_assign(L"WT_PROFILE_ID", Utils::GuidToString(_profileGuid));
// WSLENV is a colon-delimited list of environment variables (+flags) that should appear inside WSL
// https://devblogs.microsoft.com/commandline/share-environment-vars-between-wsl-and-windows/
@@ -171,7 +165,7 @@ namespace winrt::Microsoft::Terminal::TerminalConnection::implementation
g_hTerminalConnectionProvider,
"ConPtyConnected",
TraceLoggingDescription("Event emitted when ConPTY connection is started"),
TraceLoggingGuid(_guid, "SessionGuid", "The WT_SESSION's GUID"),
TraceLoggingGuid(_sessionId, "SessionGuid", "The WT_SESSION's GUID"),
TraceLoggingWideString(_clientName.c_str(), "Client", "The attached client process"),
TraceLoggingKeyword(MICROSOFT_KEYWORD_MEASURES),
TelemetryPrivacyDataTag(PDT_ProductAndServiceUsage));
@@ -189,7 +183,6 @@ namespace winrt::Microsoft::Terminal::TerminalConnection::implementation
TERMINAL_STARTUP_INFO startupInfo) :
_rows{ 25 },
_cols{ 80 },
_guid{ Utils::CreateGuid() },
_inPipe{ hIn },
_outPipe{ hOut }
{
@@ -249,12 +242,6 @@ namespace winrt::Microsoft::Terminal::TerminalConnection::implementation
return vs;
}
template<typename T>
T unbox_prop_or(const Windows::Foundation::Collections::ValueSet& blob, std::wstring_view key, T defaultValue)
{
return winrt::unbox_value_or<T>(blob.TryLookup(key).try_as<Windows::Foundation::IPropertyValue>(), defaultValue);
}
void ConptyConnection::Initialize(const Windows::Foundation::Collections::ValueSet& settings)
{
if (settings)
@@ -268,7 +255,7 @@ namespace winrt::Microsoft::Terminal::TerminalConnection::implementation
_startingTitle = unbox_prop_or<winrt::hstring>(settings, L"startingTitle", _startingTitle);
_rows = unbox_prop_or<uint32_t>(settings, L"initialRows", _rows);
_cols = unbox_prop_or<uint32_t>(settings, L"initialCols", _cols);
_guid = unbox_prop_or<winrt::guid>(settings, L"guid", _guid);
_sessionId = unbox_prop_or<winrt::guid>(settings, L"sessionId", _sessionId);
_environment = settings.TryLookup(L"environment").try_as<Windows::Foundation::Collections::ValueSet>();
if constexpr (Feature_VtPassthroughMode::IsEnabled())
{
@@ -299,17 +286,12 @@ namespace winrt::Microsoft::Terminal::TerminalConnection::implementation
_initialEnv = til::env::from_current_environment();
}
}
if (_guid == guid{})
{
_guid = Utils::CreateGuid();
}
}
}
winrt::guid ConptyConnection::Guid() const noexcept
{
return _guid;
if (_sessionId == guid{})
{
_sessionId = Utils::CreateGuid();
}
}
winrt::hstring ConptyConnection::Commandline() const
@@ -382,7 +364,7 @@ namespace winrt::Microsoft::Terminal::TerminalConnection::implementation
g_hTerminalConnectionProvider,
"ConPtyConnectedToDefterm",
TraceLoggingDescription("Event emitted when ConPTY connection is started, for a defterm session"),
TraceLoggingGuid(_guid, "SessionGuid", "The WT_SESSION's GUID"),
TraceLoggingGuid(_sessionId, "SessionGuid", "The WT_SESSION's GUID"),
TraceLoggingWideString(_clientName.c_str(), "Client", "The attached client process"),
TraceLoggingKeyword(MICROSOFT_KEYWORD_MEASURES),
TelemetryPrivacyDataTag(PDT_ProductAndServiceUsage));
@@ -686,7 +668,7 @@ namespace winrt::Microsoft::Terminal::TerminalConnection::implementation
TraceLoggingWrite(g_hTerminalConnectionProvider,
"ReceivedFirstByte",
TraceLoggingDescription("An event emitted when the connection receives the first byte"),
TraceLoggingGuid(_guid, "SessionGuid", "The WT_SESSION's GUID"),
TraceLoggingGuid(_sessionId, "SessionGuid", "The WT_SESSION's GUID"),
TraceLoggingFloat64(delta.count(), "Duration"),
TraceLoggingKeyword(MICROSOFT_KEYWORD_MEASURES),
TelemetryPrivacyDataTag(PDT_ProductAndServicePerformance));

View File

@@ -4,14 +4,14 @@
#pragma once
#include "ConptyConnection.g.h"
#include "ConnectionStateHolder.h"
#include "BaseTerminalConnection.h"
#include "ITerminalHandoff.h"
#include <til/env.h>
namespace winrt::Microsoft::Terminal::TerminalConnection::implementation
{
struct ConptyConnection : ConptyConnectionT<ConptyConnection>, ConnectionStateHolder<ConptyConnection>
struct ConptyConnection : ConptyConnectionT<ConptyConnection>, BaseTerminalConnection<ConptyConnection>
{
ConptyConnection(const HANDLE hSig,
const HANDLE hIn,
@@ -36,7 +36,6 @@ namespace winrt::Microsoft::Terminal::TerminalConnection::implementation
void ReparentWindow(const uint64_t newParent);
winrt::guid Guid() const noexcept;
winrt::hstring Commandline() const;
winrt::hstring StartingTitle() const;
WORD ShowWindow() const noexcept;
@@ -77,7 +76,6 @@ namespace winrt::Microsoft::Terminal::TerminalConnection::implementation
hstring _startingTitle{};
bool _initialVisibility{ true };
Windows::Foundation::Collections::ValueSet _environment{ nullptr };
guid _guid{}; // A unique session identifier for connected client
hstring _clientName{}; // The name of the process hosted by this ConPTY connection (as of launch).
bool _receivedFirstByte{ false };

View File

@@ -10,7 +10,6 @@ namespace Microsoft.Terminal.TerminalConnection
[default_interface] runtimeclass ConptyConnection : ITerminalConnection
{
ConptyConnection();
Guid Guid { get; };
String Commandline { get; };
String StartingTitle { get; };
UInt16 ShowWindow { get; };

View File

@@ -18,6 +18,7 @@ namespace winrt::Microsoft::Terminal::TerminalConnection::implementation
void Initialize(const Windows::Foundation::Collections::ValueSet& /*settings*/) const noexcept {};
winrt::guid SessionId() const noexcept { return {}; }
ConnectionState State() const noexcept { return ConnectionState::Connected; }
WINRT_CALLBACK(TerminalOutput, TerminalOutputHandler);

View File

@@ -25,8 +25,9 @@ namespace Microsoft.Terminal.TerminalConnection
void Close();
event TerminalOutputHandler TerminalOutput;
event Windows.Foundation.TypedEventHandler<ITerminalConnection, Object> StateChanged;
Guid SessionId { get; };
ConnectionState State { get; };
};
}

View File

@@ -17,6 +17,7 @@
<Import Project="$(OpenConsoleDir)src\cppwinrt.build.pre.props" />
<ItemGroup>
<ClInclude Include="AzureClientID.h" />
<ClInclude Include="BaseTerminalConnection.h" />
<ClInclude Include="ConnectionInformation.h">
<DependentUpon>ConnectionInformation.idl</DependentUpon>
</ClInclude>

View File

@@ -26,6 +26,7 @@
<ClInclude Include="AzureConnection.h" />
<ClInclude Include="AzureClientID.h" />
<ClInclude Include="CTerminalHandoff.h" />
<ClInclude Include="BaseTerminalConnection.h" />
</ItemGroup>
<ItemGroup>
<Midl Include="ITerminalConnection.idl" />
@@ -34,11 +35,9 @@
<Midl Include="ConptyConnection.idl" />
<Midl Include="ConnectionInformation.idl" />
</ItemGroup>
<ItemGroup>
<None Include="packages.config" />
</ItemGroup>
<ItemGroup>
<Natvis Include="$(SolutionDir)tools\ConsoleTypes.natvis" />
<Natvis Include="$(MSBuildThisFileDirectory)..\..\natvis\wil.natvis" />
</ItemGroup>
<ItemGroup>
<PRIResource Include="Resources\en-US\Resources.resw" />

View File

@@ -126,6 +126,9 @@ namespace winrt::Microsoft::Terminal::Control::implementation
auto pfnCompletionsChanged = [=](auto&& menuJson, auto&& replaceLength) { _terminalCompletionsChanged(menuJson, replaceLength); };
_terminal->CompletionsChangedCallback(pfnCompletionsChanged);
auto pfnSendNotification = std::bind(&ControlCore::_terminalSendNotification, this, std::placeholders::_1, std::placeholders::_2);
_terminal->SetSendNotificationCallback(pfnSendNotification);
// MSFT 33353327: Initialize the renderer in the ctor instead of Initialize().
// We need the renderer to be ready to accept new engines before the SwapChainPanel is ready to go.
// If we wait, a screen reader may try to get the AutomationPeer (aka the UIA Engine), and we won't be able to attach
@@ -1250,44 +1253,23 @@ namespace winrt::Microsoft::Terminal::Control::implementation
return false;
}
// use action's copyFormatting if it's present, else fallback to globally
// set copyFormatting.
const auto copyFormats = formats != nullptr ? formats.Value() : _settings->CopyFormatting();
const auto copyHtml = WI_IsFlagSet(copyFormats, CopyFormat::HTML);
const auto copyRtf = WI_IsFlagSet(copyFormats, CopyFormat::RTF);
// extract text from buffer
// RetrieveSelectedTextFromBuffer will lock while it's reading
const auto bufferData = _terminal->RetrieveSelectedTextFromBuffer(singleLine);
// convert text: vector<string> --> string
std::wstring textData;
for (const auto& text : bufferData.text)
{
textData += text;
}
const auto bgColor = _terminal->GetAttributeColors({}).second;
// convert text to HTML format
// GH#5347 - Don't provide a title for the generated HTML, as many
// web applications will paste the title first, followed by the HTML
// content, which is unexpected.
const auto htmlData = formats == nullptr || WI_IsFlagSet(formats.Value(), CopyFormat::HTML) ?
TextBuffer::GenHTML(bufferData,
_actualFont.GetUnscaledSize().height,
_actualFont.GetFaceName(),
bgColor) :
"";
// convert to RTF format
const auto rtfData = formats == nullptr || WI_IsFlagSet(formats.Value(), CopyFormat::RTF) ?
TextBuffer::GenRTF(bufferData,
_actualFont.GetUnscaledSize().height,
_actualFont.GetFaceName(),
bgColor) :
"";
const auto& [textData, htmlData, rtfData] = _terminal->RetrieveSelectedTextFromBuffer(singleLine, copyHtml, copyRtf);
// send data up for clipboard
_CopyToClipboardHandlers(*this,
winrt::make<CopyToClipboardEventArgs>(winrt::hstring{ textData },
winrt::to_hstring(htmlData),
winrt::to_hstring(rtfData),
formats));
copyFormats));
return true;
}
@@ -1606,30 +1588,41 @@ namespace winrt::Microsoft::Terminal::Control::implementation
_midiAudio.PlayNote(reinterpret_cast<HWND>(_owningHwnd), noteNumber, velocity, std::chrono::duration_cast<std::chrono::milliseconds>(duration));
}
void ControlCore::_terminalSendNotification(const std::wstring_view title,
const std::wstring_view body)
{
const auto e = winrt::make_self<implementation::SendNotificationArgs>(title, body);
_SendNotificationHandlers(*this, *e);
}
bool ControlCore::HasSelection() const
{
const auto lock = _terminal->LockForReading();
return _terminal->IsSelectionActive();
}
// Method Description:
// - Checks if the currently active selection spans multiple lines
// Return Value:
// - true if selection is multi-line
bool ControlCore::HasMultiLineSelection() const
{
const auto lock = _terminal->LockForReading();
assert(_terminal->IsSelectionActive()); // should only be called when selection is active
return _terminal->GetSelectionAnchor().y != _terminal->GetSelectionEnd().y;
}
bool ControlCore::CopyOnSelect() const
{
return _settings->CopyOnSelect();
}
Windows::Foundation::Collections::IVector<winrt::hstring> ControlCore::SelectedText(bool trimTrailingWhitespace) const
winrt::hstring ControlCore::SelectedText(bool trimTrailingWhitespace) const
{
// RetrieveSelectedTextFromBuffer will lock while it's reading
const auto lock = _terminal->LockForReading();
const auto internalResult{ _terminal->RetrieveSelectedTextFromBuffer(trimTrailingWhitespace).text };
auto result = winrt::single_threaded_vector<winrt::hstring>();
for (const auto& row : internalResult)
{
result.Append(winrt::hstring{ row });
}
return result;
const auto internalResult{ _terminal->RetrieveSelectedTextFromBuffer(!trimTrailingWhitespace) };
return winrt::hstring{ internalResult.plainText };
}
::Microsoft::Console::Render::IRenderData* ControlCore::GetRenderData() const

View File

@@ -156,7 +156,8 @@ namespace winrt::Microsoft::Terminal::Control::implementation
int BufferHeight() const;
bool HasSelection() const;
Windows::Foundation::Collections::IVector<winrt::hstring> SelectedText(bool trimTrailingWhitespace) const;
bool HasMultiLineSelection() const;
winrt::hstring SelectedText(bool trimTrailingWhitespace) const;
bool BracketedPasteEnabled() const noexcept;
@@ -279,6 +280,8 @@ namespace winrt::Microsoft::Terminal::Control::implementation
TYPED_EVENT(RestartTerminalRequested, IInspectable, IInspectable);
TYPED_EVENT(Attached, IInspectable, IInspectable);
TYPED_EVENT(SendNotification, IInspectable, Control::SendNotificationArgs);
// clang-format on
private:
@@ -371,6 +374,8 @@ namespace winrt::Microsoft::Terminal::Control::implementation
winrt::fire_and_forget _terminalCompletionsChanged(std::wstring_view menuJson, unsigned int replaceLength);
void _terminalSendNotification(const std::wstring_view title,
const std::wstring_view body);
#pragma endregion
MidiAudio _midiAudio;

View File

@@ -190,5 +190,7 @@ namespace Microsoft.Terminal.Control
event Windows.Foundation.TypedEventHandler<Object, CompletionsChangedEventArgs> CompletionsChanged;
event Windows.Foundation.TypedEventHandler<Object, SendNotificationArgs> SendNotification;
};
}

View File

@@ -20,3 +20,4 @@
#include "KeySentEventArgs.g.cpp"
#include "CharSentEventArgs.g.cpp"
#include "StringSentEventArgs.g.cpp"
#include "SendNotificationArgs.g.cpp"

View File

@@ -20,6 +20,7 @@
#include "KeySentEventArgs.g.h"
#include "CharSentEventArgs.g.h"
#include "StringSentEventArgs.g.h"
#include "SendNotificationArgs.g.h"
namespace winrt::Microsoft::Terminal::Control::implementation
{
@@ -251,6 +252,20 @@ namespace winrt::Microsoft::Terminal::Control::implementation
WINRT_PROPERTY(winrt::hstring, Text);
};
struct SendNotificationArgs : public SendNotificationArgsT<SendNotificationArgs>
{
public:
SendNotificationArgs(const std::wstring_view title,
const std::wstring_view body) :
_Title(title),
_Body(body)
{
}
WINRT_PROPERTY(winrt::hstring, Title);
WINRT_PROPERTY(winrt::hstring, Body);
};
}
namespace winrt::Microsoft::Terminal::Control::factory_implementation

View File

@@ -121,4 +121,10 @@ namespace Microsoft.Terminal.Control
{
String Text { get; };
}
runtimeclass SendNotificationArgs
{
String Title { get; };
String Body { get; };
}
}

View File

@@ -106,9 +106,12 @@ try
{
try
{
const auto lock = publicTerminal->_terminal->LockForWriting();
const auto bufferData = publicTerminal->_terminal->RetrieveSelectedTextFromBuffer(false);
LOG_IF_FAILED(publicTerminal->_CopyTextToSystemClipboard(bufferData, true));
Terminal::TextCopyData bufferData;
{
const auto lock = publicTerminal->_terminal->LockForWriting();
bufferData = publicTerminal->_terminal->RetrieveSelectedTextFromBuffer(false, true, true);
}
LOG_IF_FAILED(publicTerminal->_CopyTextToSystemClipboard(bufferData.plainText, bufferData.html, bufferData.rtf));
publicTerminal->_ClearSelection();
}
CATCH_LOG();
@@ -666,20 +669,14 @@ try
return nullptr;
}
TextBuffer::TextAndColor bufferData;
std::wstring selectedText;
{
const auto lock = publicTerminal->_terminal->LockForWriting();
bufferData = publicTerminal->_terminal->RetrieveSelectedTextFromBuffer(false);
auto bufferData = publicTerminal->_terminal->RetrieveSelectedTextFromBuffer(false);
selectedText = std::move(bufferData.plainText);
publicTerminal->_ClearSelection();
}
// convert text: vector<string> --> string
std::wstring selectedText;
for (const auto& text : bufferData.text)
{
selectedText += text;
}
auto returnText = wil::make_cotaskmem_string_nothrow(selectedText.c_str());
return returnText.release();
}
@@ -963,22 +960,16 @@ void __stdcall TerminalKillFocus(void* terminal)
// Routine Description:
// - Copies the text given onto the global system clipboard.
// Arguments:
// - rows - Rows of text data to copy
// - fAlsoCopyFormatting - true if the color and formatting should also be copied, false otherwise
HRESULT HwndTerminal::_CopyTextToSystemClipboard(const TextBuffer::TextAndColor& rows, const bool fAlsoCopyFormatting)
// - text - selected text in plain-text format
// - htmlData - selected text in HTML format
// - rtfData - selected text in RTF format
HRESULT HwndTerminal::_CopyTextToSystemClipboard(const std::wstring& text, const std::string& htmlData, const std::string& rtfData) const
try
{
RETURN_HR_IF_NULL(E_NOT_VALID_STATE, _terminal);
std::wstring finalString;
// Concatenate strings into one giant string to put onto the clipboard.
for (const auto& str : rows.text)
{
finalString += str;
}
// allocate the final clipboard data
const auto cchNeeded = finalString.size() + 1;
const auto cchNeeded = text.size() + 1;
const auto cbNeeded = sizeof(wchar_t) * cchNeeded;
wil::unique_hglobal globalHandle(GlobalAlloc(GMEM_MOVEABLE | GMEM_DDESHARE, cbNeeded));
RETURN_LAST_ERROR_IF_NULL(globalHandle.get());
@@ -988,7 +979,7 @@ try
// The pattern gets a bit strange here because there's no good wil built-in for global lock of this type.
// Try to copy then immediately unlock. Don't throw until after (so the hglobal won't be freed until we unlock).
const auto hr = StringCchCopyW(pwszClipboard, cchNeeded, finalString.data());
const auto hr = StringCchCopyW(pwszClipboard, cchNeeded, text.data());
GlobalUnlock(globalHandle.get());
RETURN_IF_FAILED(hr);
@@ -1003,21 +994,14 @@ try
RETURN_LAST_ERROR_IF(!EmptyClipboard());
RETURN_LAST_ERROR_IF_NULL(SetClipboardData(CF_UNICODETEXT, globalHandle.get()));
if (fAlsoCopyFormatting)
if (!htmlData.empty())
{
const auto& fontData = _actualFont;
const int iFontHeightPoints = fontData.GetUnscaledSize().height; // this renderer uses points already
COLORREF bgColor;
{
const auto lock = _terminal->LockForReading();
bgColor = _terminal->GetAttributeColors({}).second;
}
RETURN_IF_FAILED(_CopyToSystemClipboard(htmlData, L"HTML Format"));
}
auto HTMLToPlaceOnClip = TextBuffer::GenHTML(rows, iFontHeightPoints, fontData.GetFaceName(), bgColor);
_CopyToSystemClipboard(HTMLToPlaceOnClip, L"HTML Format");
auto RTFToPlaceOnClip = TextBuffer::GenRTF(rows, iFontHeightPoints, fontData.GetFaceName(), bgColor);
_CopyToSystemClipboard(RTFToPlaceOnClip, L"Rich Text Format");
if (!rtfData.empty())
{
RETURN_IF_FAILED(_CopyToSystemClipboard(rtfData, L"Rich Text Format"));
}
}
@@ -1035,7 +1019,7 @@ CATCH_RETURN()
// Arguments:
// - stringToCopy - The string to copy
// - lpszFormat - the name of the format
HRESULT HwndTerminal::_CopyToSystemClipboard(std::string stringToCopy, LPCWSTR lpszFormat)
HRESULT HwndTerminal::_CopyToSystemClipboard(const std::string& stringToCopy, LPCWSTR lpszFormat) const
{
const auto cbData = stringToCopy.size() + 1; // +1 for '\0'
if (cbData)

View File

@@ -109,8 +109,8 @@ private:
void _UpdateFont(int newDpi);
void _WriteTextToConnection(const std::wstring_view text) noexcept;
HRESULT _CopyTextToSystemClipboard(const TextBuffer::TextAndColor& rows, const bool fAlsoCopyFormatting);
HRESULT _CopyToSystemClipboard(std::string stringToCopy, LPCWSTR lpszFormat);
HRESULT _CopyTextToSystemClipboard(const std::wstring& text, const std::string& htmlData, const std::string& rtfData) const;
HRESULT _CopyToSystemClipboard(const std::string& stringToCopy, LPCWSTR lpszFormat) const;
void _PasteTextFromClipboard() noexcept;
const unsigned int _NumberOfClicks(til::point clickPos, std::chrono::steady_clock::time_point clickTime) noexcept;

View File

@@ -3,6 +3,7 @@
import "IKeyBindings.idl";
import "IControlAppearance.idl";
import "EventArgs.idl";
namespace Microsoft.Terminal.Control
{
@@ -48,6 +49,7 @@ namespace Microsoft.Terminal.Control
Microsoft.Terminal.Control.IKeyBindings KeyBindings { get; };
Boolean CopyOnSelect { get; };
Microsoft.Terminal.Control.CopyFormat CopyFormatting { get; };
Boolean FocusFollowMouse { get; };
String Commandline { get; };

View File

@@ -46,7 +46,8 @@ namespace Microsoft.Terminal.Control
Int32 BufferHeight { get; };
Boolean HasSelection { get; };
IVector<String> SelectedText(Boolean trimTrailingWhitespace);
Boolean HasMultiLineSelection { get; };
String SelectedText(Boolean trimTrailingWhitespace);
Boolean BracketedPasteEnabled { get; };

View File

@@ -57,10 +57,7 @@ namespace winrt::Microsoft::Terminal::Control::implementation
_isInternalScrollBarUpdate{ false },
_autoScrollVelocity{ 0 },
_autoScrollingPointerPoint{ std::nullopt },
_autoScrollTimer{},
_lastAutoScrollUpdateTime{ std::nullopt },
_cursorTimer{},
_blinkTimer{},
_searchBox{ nullptr }
{
InitializeComponent();
@@ -107,6 +104,14 @@ namespace winrt::Microsoft::Terminal::Control::implementation
_revokers.PasteFromClipboard = _interactivity.PasteFromClipboard(winrt::auto_revoke, { get_weak(), &TermControl::_bubblePasteFromClipboard });
// Re-raise the event with us as the sender.
_core.SendNotification([weakThis = get_weak()](auto s, auto e) {
if (auto self{ weakThis.get() })
{
self->_SendNotificationHandlers(*self, e);
}
});
// Initialize the terminal only once the swapchainpanel is loaded - that
// way, we'll be able to query the real pixel size it got on layout
_layoutUpdatedRevoker = SwapChainPanel().LayoutUpdated(winrt::auto_revoke, [this](auto /*s*/, auto /*e*/) {
@@ -419,10 +424,9 @@ namespace winrt::Microsoft::Terminal::Control::implementation
// Currently we populate the search box only if a single line is selected.
// Empirically, multi-line selection works as well on sample scenarios,
// but since code paths differ, extra work is required to ensure correctness.
auto bufferText = _core.SelectedText(true);
if (bufferText.Size() == 1)
if (!_core.HasMultiLineSelection())
{
const auto selectedLine{ bufferText.GetAt(0) };
const auto selectedLine{ _core.SelectedText(true) };
_searchBox->PopulateTextbox(selectedLine);
}
}
@@ -1087,10 +1091,8 @@ namespace winrt::Microsoft::Terminal::Control::implementation
if (blinkTime != INFINITE)
{
// Create a timer
DispatcherTimer cursorTimer;
cursorTimer.Interval(std::chrono::milliseconds(blinkTime));
cursorTimer.Tick({ get_weak(), &TermControl::_CursorTimerTick });
_cursorTimer.emplace(std::move(cursorTimer));
_cursorTimer.Interval(std::chrono::milliseconds(blinkTime));
_cursorTimer.Tick({ get_weak(), &TermControl::_CursorTimerTick });
// As of GH#6586, don't start the cursor timer immediately, and
// don't show the cursor initially. We'll show the cursor and start
// the timer when the control is first focused.
@@ -1105,13 +1107,12 @@ namespace winrt::Microsoft::Terminal::Control::implementation
_core.CursorOn(_focused || _displayCursorWhileBlurred());
if (_displayCursorWhileBlurred())
{
_cursorTimer->Start();
_cursorTimer.Start();
}
}
else
{
// The user has disabled cursor blinking
_cursorTimer = std::nullopt;
_cursorTimer.Destroy();
}
// Set up blinking attributes
@@ -1120,16 +1121,14 @@ namespace winrt::Microsoft::Terminal::Control::implementation
if (animationsEnabled && blinkTime != INFINITE)
{
// Create a timer
DispatcherTimer blinkTimer;
blinkTimer.Interval(std::chrono::milliseconds(blinkTime));
blinkTimer.Tick({ get_weak(), &TermControl::_BlinkTimerTick });
blinkTimer.Start();
_blinkTimer.emplace(std::move(blinkTimer));
_blinkTimer.Interval(std::chrono::milliseconds(blinkTime));
_blinkTimer.Tick({ get_weak(), &TermControl::_BlinkTimerTick });
_blinkTimer.Start();
}
else
{
// The user has disabled blinking
_blinkTimer = std::nullopt;
_blinkTimer.Destroy();
}
// Now that the renderer is set up, update the appearance for initialization
@@ -1345,7 +1344,7 @@ namespace winrt::Microsoft::Terminal::Control::implementation
// Alt, so we should be ignoring the individual keydowns. The character
// will be sent through the TSFInputControl. See GH#1401 for more
// details
if (modifiers.IsAltPressed() &&
if (modifiers.IsAltPressed() && !modifiers.IsCtrlPressed() &&
(vkey >= VK_NUMPAD0 && vkey <= VK_NUMPAD9))
{
e.Handled(true);
@@ -1498,7 +1497,7 @@ namespace winrt::Microsoft::Terminal::Control::implementation
// Manually show the cursor when a key is pressed. Restarting
// the timer prevents flickering.
_core.CursorOn(_core.SelectionMode() != SelectionInteractionMode::Mark);
_cursorTimer->Start();
_cursorTimer.Start();
}
return handled;
@@ -1973,12 +1972,12 @@ namespace winrt::Microsoft::Terminal::Control::implementation
{
// When the terminal focuses, show the cursor immediately
_core.CursorOn(_core.SelectionMode() != SelectionInteractionMode::Mark);
_cursorTimer->Start();
_cursorTimer.Start();
}
if (_blinkTimer)
{
_blinkTimer->Start();
_blinkTimer.Start();
}
// Only update the appearance here if an unfocused config exists - if an
@@ -2021,13 +2020,13 @@ namespace winrt::Microsoft::Terminal::Control::implementation
if (_cursorTimer && !_displayCursorWhileBlurred())
{
_cursorTimer->Stop();
_cursorTimer.Stop();
_core.CursorOn(false);
}
if (_blinkTimer)
{
_blinkTimer->Stop();
_blinkTimer.Stop();
}
// Check if there is an unfocused config we should set the appearance to
@@ -2278,7 +2277,16 @@ namespace winrt::Microsoft::Terminal::Control::implementation
// Disconnect the TSF input control so it doesn't receive EditContext events.
TSFInputControl().Close();
// At the time of writing, closing the last tab of a window inexplicably
// does not lead to the destruction of the remaining TermControl instance(s).
// On Win10 we don't destroy window threads due to bugs in DesktopWindowXamlSource.
// In turn, we leak TermControl instances. This results in constant HWND messages
// while the thread is supposed to be idle. Stop these timers avoids this.
_autoScrollTimer.Stop();
_bellLightTimer.Stop();
_cursorTimer.Stop();
_blinkTimer.Stop();
if (!_detached)
{
@@ -3129,20 +3137,13 @@ namespace winrt::Microsoft::Terminal::Control::implementation
_bellDarkAnimation.Duration(winrt::Windows::Foundation::TimeSpan(std::chrono::milliseconds(TerminalWarningBellInterval)));
}
// Similar to the animation, only initialize the timer here
if (!_bellLightTimer)
{
_bellLightTimer = {};
_bellLightTimer.Interval(std::chrono::milliseconds(TerminalWarningBellInterval));
_bellLightTimer.Tick({ get_weak(), &TermControl::_BellLightOff });
}
Windows::Foundation::Numerics::float2 zeroSize{ 0, 0 };
// If the grid has 0 size or if the bell timer is
// already active, do nothing
if (RootGrid().ActualSize() != zeroSize && !_bellLightTimer.IsEnabled())
{
// Start the timer, when the timer ticks we switch off the light
_bellLightTimer.Interval(std::chrono::milliseconds(TerminalWarningBellInterval));
_bellLightTimer.Tick({ get_weak(), &TermControl::_BellLightOff });
_bellLightTimer.Start();
// Switch on the light and animate the intensity to fade out
@@ -3162,15 +3163,12 @@ namespace winrt::Microsoft::Terminal::Control::implementation
void TermControl::_BellLightOff(const Windows::Foundation::IInspectable& /* sender */,
const Windows::Foundation::IInspectable& /* e */)
{
if (_bellLightTimer)
{
// Stop the timer and switch off the light
_bellLightTimer.Stop();
// Stop the timer and switch off the light
_bellLightTimer.Stop();
if (!_IsClosing())
{
VisualBellLight::SetIsTarget(RootGrid(), false);
}
if (!_IsClosing())
{
VisualBellLight::SetIsTarget(RootGrid(), false);
}
}
@@ -3496,7 +3494,11 @@ namespace winrt::Microsoft::Terminal::Control::implementation
{
return _core.HasSelection();
}
Windows::Foundation::Collections::IVector<winrt::hstring> TermControl::SelectedText(bool trimTrailingWhitespace) const
bool TermControl::HasMultiLineSelection() const
{
return _core.HasMultiLineSelection();
}
winrt::hstring TermControl::SelectedText(bool trimTrailingWhitespace) const
{
return _core.SelectedText(trimTrailingWhitespace);
}
@@ -3729,9 +3731,9 @@ namespace winrt::Microsoft::Terminal::Control::implementation
{
// If we should be ALWAYS displaying the cursor, turn it on and start blinking.
_core.CursorOn(true);
if (_cursorTimer.has_value())
if (_cursorTimer)
{
_cursorTimer->Start();
_cursorTimer.Start();
}
}
else
@@ -3740,9 +3742,9 @@ namespace winrt::Microsoft::Terminal::Control::implementation
// blinking. (if we're focused, then we're already doing the right
// thing)
const auto focused = FocusState() != FocusState::Unfocused;
if (!focused && _cursorTimer.has_value())
if (!focused && _cursorTimer)
{
_cursorTimer->Stop();
_cursorTimer.Stop();
}
_core.CursorOn(focused);
}

View File

@@ -72,7 +72,8 @@ namespace winrt::Microsoft::Terminal::Control::implementation
int BufferHeight() const;
bool HasSelection() const;
Windows::Foundation::Collections::IVector<winrt::hstring> SelectedText(bool trimTrailingWhitespace) const;
bool HasMultiLineSelection() const;
winrt::hstring SelectedText(bool trimTrailingWhitespace) const;
bool BracketedPasteEnabled() const noexcept;
@@ -193,6 +194,7 @@ namespace winrt::Microsoft::Terminal::Control::implementation
TYPED_EVENT(KeySent, IInspectable, Control::KeySentEventArgs);
TYPED_EVENT(CharSent, IInspectable, Control::CharSentEventArgs);
TYPED_EVENT(StringSent, IInspectable, Control::StringSentEventArgs);
TYPED_EVENT(SendNotification, IInspectable, Control::SendNotificationArgs);
// clang-format on
WINRT_OBSERVABLE_PROPERTY(winrt::Windows::UI::Xaml::Media::Brush, BackgroundBrush, _PropertyChangedHandlers, nullptr);
@@ -236,16 +238,16 @@ namespace winrt::Microsoft::Terminal::Control::implementation
// viewport. View is then scrolled to 'follow' the cursor.
double _autoScrollVelocity;
std::optional<Windows::UI::Input::PointerPoint> _autoScrollingPointerPoint;
Windows::UI::Xaml::DispatcherTimer _autoScrollTimer;
SafeDispatcherTimer _autoScrollTimer;
std::optional<std::chrono::high_resolution_clock::time_point> _lastAutoScrollUpdateTime;
bool _pointerPressedInBounds{ false };
winrt::Windows::UI::Composition::ScalarKeyFrameAnimation _bellLightAnimation{ nullptr };
winrt::Windows::UI::Composition::ScalarKeyFrameAnimation _bellDarkAnimation{ nullptr };
Windows::UI::Xaml::DispatcherTimer _bellLightTimer{ nullptr };
SafeDispatcherTimer _bellLightTimer;
std::optional<Windows::UI::Xaml::DispatcherTimer> _cursorTimer;
std::optional<Windows::UI::Xaml::DispatcherTimer> _blinkTimer;
SafeDispatcherTimer _cursorTimer;
SafeDispatcherTimer _blinkTimer;
winrt::Windows::UI::Xaml::Controls::SwapChainPanel::LayoutUpdated_revoker _layoutUpdatedRevoker;
bool _showMarksInScrollbar{ false };

View File

@@ -58,6 +58,7 @@ namespace Microsoft.Terminal.Control
event Windows.Foundation.TypedEventHandler<Object, Object> TabColorChanged;
event Windows.Foundation.TypedEventHandler<Object, Object> ReadOnlyChanged;
event Windows.Foundation.TypedEventHandler<Object, Object> FocusFollowMouseRequested;
event Windows.Foundation.TypedEventHandler<Object, SendNotificationArgs> SendNotification;
event Windows.Foundation.TypedEventHandler<Object, CompletionsChangedEventArgs> CompletionsChanged;

View File

@@ -92,8 +92,8 @@ namespace winrt::Microsoft::Terminal::Control::implementation
winrt::Windows::Foundation::IInspectable XamlUiaTextRange::GetAttributeValue(int32_t textAttributeId) const
{
// Call the function off of the underlying UiaTextRange.
VARIANT result;
THROW_IF_FAILED(_uiaProvider->GetAttributeValue(textAttributeId, &result));
wil::unique_variant result;
THROW_IF_FAILED(_uiaProvider->GetAttributeValue(textAttributeId, result.addressof()));
// Convert the resulting VARIANT into a format that is consumable by XAML.
switch (result.vt)
@@ -189,9 +189,9 @@ namespace winrt::Microsoft::Terminal::Control::implementation
winrt::hstring XamlUiaTextRange::GetText(int32_t maxLength) const
{
BSTR returnVal;
THROW_IF_FAILED(_uiaProvider->GetText(maxLength, &returnVal));
return winrt::to_hstring(returnVal);
wil::unique_bstr returnVal;
THROW_IF_FAILED(_uiaProvider->GetText(maxLength, returnVal.put()));
return winrt::hstring{ returnVal.get(), SysStringLen(returnVal.get()) };
}
int32_t XamlUiaTextRange::Move(XamlAutomation::TextUnit unit,

View File

@@ -73,7 +73,8 @@ TRACELOGGING_DECLARE_PROVIDER(g_hTerminalControlProvider);
#include <til/mutex.h>
#include <til/winrt.h>
#include "ThrottledFunc.h"
#include <SafeDispatcherTimer.h>
#include <ThrottledFunc.h>
#include <cppwinrt_utils.h>
#include <wil/cppwinrt_helpers.h> // must go after the CoreDispatcher type is defined

View File

@@ -28,6 +28,7 @@ namespace Microsoft.Terminal.Core
Windows.Foundation.IReference<Microsoft.Terminal.Core.Color> StartingTabColor;
Boolean AutoMarkPrompts;
Boolean AllowNotifications;
};

View File

@@ -95,6 +95,7 @@ void Terminal::UpdateSettings(ICoreSettings settings)
_startingTitle = settings.StartingTitle();
_trimBlockSelection = settings.TrimBlockSelection();
_autoMarkPrompts = settings.AutoMarkPrompts();
_allowNotifications = settings.AllowNotifications();
_getTerminalInput().ForceDisableWin32InputMode(settings.ForceVTInput());
@@ -1160,6 +1161,11 @@ void Terminal::SetPlayMidiNoteCallback(std::function<void(const int, const int,
_pfnPlayMidiNote.swap(pfn);
}
void Terminal::SetSendNotificationCallback(std::function<void(std::wstring_view, std::wstring_view)> pfn) noexcept
{
_pfnSendNotification.swap(pfn);
}
void Terminal::BlinkCursor() noexcept
{
if (_selectionMode != SelectionInteractionMode::Mark)

View File

@@ -163,6 +163,7 @@ public:
void InvokeCompletions(std::wstring_view menuJson, unsigned int replaceLength) override;
void SendNotification(const std::wstring_view title, const std::wstring_view body) override;
#pragma endregion
void ClearMark();
@@ -237,6 +238,7 @@ public:
void SetShowWindowCallback(std::function<void(bool)> pfn) noexcept;
void SetPlayMidiNoteCallback(std::function<void(const int, const int, const std::chrono::microseconds)> pfn) noexcept;
void CompletionsChangedCallback(std::function<void(std::wstring_view, unsigned int)> pfn) noexcept;
void SetSendNotificationCallback(std::function<void(std::wstring_view, std::wstring_view)> pfn) noexcept;
void BlinkCursor() noexcept;
void SetCursorOn(const bool isOn) noexcept;
@@ -293,6 +295,13 @@ public:
End = 0x2
};
struct TextCopyData
{
std::wstring plainText;
std::string html;
std::string rtf;
};
void MultiClickSelection(const til::point viewportPos, SelectionExpansion expansionMode);
void SetSelectionAnchor(const til::point position);
void SetSelectionEnd(const til::point position, std::optional<SelectionExpansion> newExpansionMode = std::nullopt);
@@ -311,7 +320,7 @@ public:
til::point SelectionEndForRendering() const;
const SelectionEndpoint SelectionEndpointTarget() const noexcept;
const TextBuffer::TextAndColor RetrieveSelectedTextFromBuffer(bool trimTrailingWhitespace);
TextCopyData RetrieveSelectedTextFromBuffer(const bool singleLine, const bool html = false, const bool rtf = false) const;
#pragma endregion
#ifndef NDEBUG
@@ -338,6 +347,7 @@ private:
std::function<void(bool)> _pfnShowWindowChanged;
std::function<void(const int, const int, const std::chrono::microseconds)> _pfnPlayMidiNote;
std::function<void(std::wstring_view, unsigned int)> _pfnCompletionsChanged;
std::function<void(std::wstring_view, std::wstring_view)> _pfnSendNotification;
RenderSettings _renderSettings;
std::unique_ptr<::Microsoft::Console::VirtualTerminal::StateMachine> _stateMachine;
@@ -356,6 +366,7 @@ private:
bool _suppressApplicationTitle = false;
bool _trimBlockSelection = false;
bool _autoMarkPrompts = false;
bool _allowNotifications = true;
size_t _taskbarState = 0;
size_t _taskbarProgress = 0;

View File

@@ -511,3 +511,13 @@ void Terminal::NotifyBufferRotation(const int delta)
_NotifyScrollEvent();
}
}
void Terminal::SendNotification(const std::wstring_view title,
const std::wstring_view body)
{
// Only send notifications if enabled in the settings
if (_pfnSendNotification && _allowNotifications)
{
_pfnSendNotification(title, body);
}
}

View File

@@ -867,27 +867,53 @@ void Terminal::ClearSelection()
}
// Method Description:
// - get wstring text from highlighted portion of text buffer
// - Get text from highlighted portion of text buffer
// - Optionally, get the highlighted text in HTML and RTF formats
// Arguments:
// - singleLine: collapse all of the text to one line
// - singleLine: collapse all of the text to one line. (Turns off trailing whitespace trimming)
// - html: also get text in HTML format
// - rtf: also get text in RTF format
// Return Value:
// - wstring text from buffer. If extended to multiple lines, each line is separated by \r\n
const TextBuffer::TextAndColor Terminal::RetrieveSelectedTextFromBuffer(bool singleLine)
// - Plain and formatted selected text from buffer. Empty string represents no data for that format.
// - If extended to multiple lines, each line is separated by \r\n
Terminal::TextCopyData Terminal::RetrieveSelectedTextFromBuffer(const bool singleLine, const bool html, const bool rtf) const
{
const auto selectionRects = _GetSelectionRects();
TextCopyData data;
if (!IsSelectionActive())
{
return data;
}
const auto GetAttributeColors = [&](const auto& attr) {
return _renderSettings.GetAttributeColors(attr);
const auto [fg, bg] = _renderSettings.GetAttributeColors(attr);
const auto ul = _renderSettings.GetAttributeUnderlineColor(attr);
return std::tuple{ fg, bg, ul };
};
// GH#6740: Block selection should preserve the visual structure:
// - CRLFs need to be added - so the lines structure is preserved
// - We should apply formatting above to wrapped rows as well (newline should be added).
// GH#9706: Trimming of trailing white-spaces in block selection is configurable.
const auto includeCRLF = !singleLine || _blockSelection;
const auto trimTrailingWhitespace = !singleLine && (!_blockSelection || _trimBlockSelection);
const auto formatWrappedRows = _blockSelection;
return _activeBuffer().GetText(includeCRLF, trimTrailingWhitespace, selectionRects, GetAttributeColors, formatWrappedRows);
const auto& textBuffer = _activeBuffer();
const auto req = TextBuffer::CopyRequest::FromConfig(textBuffer, _selection->start, _selection->end, singleLine, _blockSelection, _trimBlockSelection);
data.plainText = textBuffer.GetPlainText(req);
if (html || rtf)
{
const auto bgColor = _renderSettings.GetAttributeColors({}).second;
const auto isIntenseBold = _renderSettings.GetRenderMode(::Microsoft::Console::Render::RenderSettings::Mode::IntenseIsBold);
const auto fontSizePt = _fontInfo.GetUnscaledSize().height; // already in points
const auto& fontName = _fontInfo.GetFaceName();
if (html)
{
data.html = textBuffer.GenHTML(req, fontSizePt, fontName, bgColor, isIntenseBold, GetAttributeColors);
}
if (rtf)
{
data.rtf = textBuffer.GenRTF(req, fontSizePt, fontName, bgColor, isIntenseBold, GetAttributeColors);
}
}
return data;
}
// Method Description:

View File

@@ -29,6 +29,7 @@ namespace winrt::Microsoft::Terminal::Settings::Editor::implementation
void DisplayPowerlineGlyphs(bool d) noexcept;
winrt::guid SessionId() const noexcept { return {}; }
winrt::Microsoft::Terminal::TerminalConnection::ConnectionState State() const noexcept { return winrt::Microsoft::Terminal::TerminalConnection::ConnectionState::Connected; }
WINRT_CALLBACK(TerminalOutput, winrt::Microsoft::Terminal::TerminalConnection::TerminalOutputHandler);

View File

@@ -117,7 +117,7 @@ namespace winrt::Microsoft::Terminal::Settings::Editor::implementation
private:
Model::Profile _profile;
winrt::guid _originalProfileGuid;
winrt::guid _originalProfileGuid{};
winrt::hstring _lastBgImagePath;
winrt::hstring _lastStartingDirectoryPath;
Editor::AppearanceViewModel _defaultAppearanceViewModel;

View File

@@ -35,7 +35,8 @@
Margin="{StaticResource StandardIndentMargin}"
Style="{StaticResource DisclaimerStyle}"
Visibility="{x:Bind Profile.IsBaseLayer}" />
<StackPanel Style="{StaticResource SettingsStackStyle}">
<StackPanel Grid.Row="1"
Style="{StaticResource SettingsStackStyle}">
<!-- Suppress Application Title -->
<local:SettingContainer x:Uid="Profile_SuppressApplicationTitle"
ClearSettingValue="{x:Bind Profile.ClearSuppressApplicationTitle}"

View File

@@ -41,13 +41,14 @@
Margin="{StaticResource StandardIndentMargin}"
Style="{StaticResource DisclaimerStyle}"
Visibility="{x:Bind Profile.IsBaseLayer}" />
<StackPanel Style="{StaticResource SettingsStackStyle}">
<StackPanel Grid.Row="1"
Style="{StaticResource SettingsStackStyle}">
<!-- Control Preview -->
<Border MaxWidth="{StaticResource StandardControlMaxWidth}">
<Border x:Name="ControlPreview"
Width="400"
Height="180"
Margin="0,0,0,12"
Margin="0,12,0,12"
HorizontalAlignment="Left"
BorderBrush="{ThemeResource SystemControlForegroundBaseMediumLowBrush}"
BorderThickness="1" />

View File

@@ -31,7 +31,8 @@
Margin="{StaticResource StandardIndentMargin}"
Style="{StaticResource DisclaimerStyle}"
Visibility="{x:Bind Profile.IsBaseLayer}" />
<StackPanel Style="{StaticResource SettingsStackStyle}">
<StackPanel Grid.Row="1"
Style="{StaticResource SettingsStackStyle}">
<!-- Name -->
<!--

View File

@@ -102,34 +102,14 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation
// The destructor ensures that the last write is flushed to disk before returning.
ApplicationState::~ApplicationState()
{
TraceLoggingWrite(g_hSettingsModelProvider,
"ApplicationState_Dtor_Start",
TraceLoggingDescription("Event at the start of the ApplicationState destructor"),
TraceLoggingLevel(WINEVENT_LEVEL_VERBOSE),
TraceLoggingKeyword(TIL_KEYWORD_TRACE));
Flush();
}
void ApplicationState::Flush()
{
// This will ensure that we not just cancel the last outstanding timer,
// but instead force it to run as soon as possible and wait for it to complete.
_throttler.flush();
TraceLoggingWrite(g_hSettingsModelProvider,
"ApplicationState_Dtor_End",
TraceLoggingDescription("Event at the end of the ApplicationState destructor"),
TraceLoggingLevel(WINEVENT_LEVEL_VERBOSE),
TraceLoggingKeyword(TIL_KEYWORD_TRACE));
}
// Re-read the state.json from disk.
void ApplicationState::Reload() const noexcept
{
_read();
}
bool ApplicationState::IsStatePath(const winrt::hstring& filename)
{
static const auto sharedPath{ _sharedPath.filename() };
static const auto elevatedPath{ _elevatedPath.filename() };
return filename == sharedPath || filename == elevatedPath;
}
// Method Description:
@@ -299,7 +279,7 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation
Json::Value ApplicationState::_toJsonWithBlob(Json::Value& root, FileSource parseSource) const noexcept
{
{
auto state = _state.lock_shared();
const auto state = _state.lock_shared();
// GH#11222: We only write properties that are of the same type (Local
// or Shared) which we requested. If we didn't want to serialize this
@@ -326,7 +306,7 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation
void ApplicationState::name(const type& value) noexcept \
{ \
{ \
auto state = _state.lock(); \
const auto state = _state.lock(); \
state->name.emplace(value); \
} \
\

View File

@@ -63,15 +63,12 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation
~ApplicationState();
// Methods
void Reload() const noexcept;
void Flush();
void Reset() noexcept;
void FromJson(const Json::Value& root, FileSource parseSource) const noexcept;
Json::Value ToJson(FileSource parseSource) const noexcept;
// General getters/setters
bool IsStatePath(const winrt::hstring& filename);
// State getters/setters
#define MTSM_APPLICATION_STATE_GEN(source, type, name, key, ...) \
type name() const noexcept; \

View File

@@ -28,11 +28,9 @@ namespace Microsoft.Terminal.Settings.Model
[default_interface] runtimeclass ApplicationState {
static ApplicationState SharedInstance();
void Reload();
void Flush();
void Reset();
Boolean IsStatePath(String filename);
String SettingsHash;
Windows.Foundation.Collections.IVector<WindowLayout> PersistedWindowLayouts;
Windows.Foundation.Collections.IVector<String> RecentCommands;

View File

@@ -410,7 +410,7 @@ bool SettingsLoader::FixupUserSettings()
{
struct CommandlinePatch
{
winrt::guid guid;
winrt::guid guid{};
std::wstring_view before;
std::wstring_view after;
};

View File

@@ -83,7 +83,7 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation
static constexpr bool debugFeaturesDefault{ true };
#endif
winrt::guid _defaultProfile;
winrt::guid _defaultProfile{};
bool _legacyReloadEnvironmentVariables{ true };
winrt::com_ptr<implementation::ActionMap> _actionMap{ winrt::make_self<implementation::ActionMap>() };

View File

@@ -98,7 +98,8 @@ Author(s):
X(bool, AutoMarkPrompts, "experimental.autoMarkPrompts", false) \
X(bool, ShowMarks, "experimental.showMarksOnScrollbar", false) \
X(bool, RepositionCursorWithMouse, "experimental.repositionCursorWithMouse", false) \
X(bool, ReloadEnvironmentVariables, "compatibility.reloadEnvironmentVariables", true)
X(bool, ReloadEnvironmentVariables, "compatibility.reloadEnvironmentVariables", true) \
X(bool, AllowNotifications, "allowNotifications", true)
// Intentionally omitted Profile settings:
// * Name

View File

@@ -94,6 +94,7 @@ namespace Microsoft.Terminal.Settings.Model
INHERITABLE_PROFILE_SETTING(Boolean, RightClickContextMenu);
INHERITABLE_PROFILE_SETTING(Boolean, RepositionCursorWithMouse);
INHERITABLE_PROFILE_SETTING(Boolean, AllowNotifications);
INHERITABLE_PROFILE_SETTING(Boolean, ReloadEnvironmentVariables);

View File

@@ -339,6 +339,7 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation
_RightClickContextMenu = profile.RightClickContextMenu();
_RepositionCursorWithMouse = profile.RepositionCursorWithMouse();
_AllowNotifications = profile.AllowNotifications();
_ReloadEnvironmentVariables = profile.ReloadEnvironmentVariables();
}
@@ -356,6 +357,7 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation
_WordDelimiters = globalSettings.WordDelimiters();
_CopyOnSelect = globalSettings.CopyOnSelect();
_CopyFormatting = globalSettings.CopyFormatting();
_FocusFollowMouse = globalSettings.FocusFollowMouse();
_ForceFullRepaintRendering = globalSettings.ForceFullRepaintRendering();
_SoftwareRendering = globalSettings.SoftwareRendering();

View File

@@ -91,6 +91,7 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation
INHERITABLE_SETTING(Model::TerminalSettings, uint32_t, CursorHeight, DEFAULT_CURSOR_HEIGHT);
INHERITABLE_SETTING(Model::TerminalSettings, hstring, WordDelimiters, DEFAULT_WORD_DELIMITERS);
INHERITABLE_SETTING(Model::TerminalSettings, bool, CopyOnSelect, false);
INHERITABLE_SETTING(Model::TerminalSettings, Microsoft::Terminal::Control::CopyFormat, CopyFormatting, 0);
INHERITABLE_SETTING(Model::TerminalSettings, bool, FocusFollowMouse, false);
INHERITABLE_SETTING(Model::TerminalSettings, bool, TrimBlockSelection, true);
INHERITABLE_SETTING(Model::TerminalSettings, bool, DetectURLs, true);
@@ -166,6 +167,7 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation
INHERITABLE_SETTING(Model::TerminalSettings, bool, ShowMarks, false);
INHERITABLE_SETTING(Model::TerminalSettings, bool, RightClickContextMenu, false);
INHERITABLE_SETTING(Model::TerminalSettings, bool, RepositionCursorWithMouse, false);
INHERITABLE_SETTING(Model::TerminalSettings, bool, AllowNotifications, true);
INHERITABLE_SETTING(Model::TerminalSettings, bool, ReloadEnvironmentVariables, true);

View File

@@ -23,6 +23,7 @@ namespace ControlUnitTests
void Resize(uint32_t /*rows*/, uint32_t /*columns*/) noexcept {}
void Close() noexcept {}
winrt::guid SessionId() const noexcept { return {}; }
winrt::Microsoft::Terminal::TerminalConnection::ConnectionState State() const noexcept { return winrt::Microsoft::Terminal::TerminalConnection::ConnectionState::Connected; }
WINRT_CALLBACK(TerminalOutput, winrt::Microsoft::Terminal::TerminalConnection::TerminalOutputHandler);

View File

@@ -19,6 +19,7 @@
<Import Project="$(OpenConsoleDir)src\cppwinrt.build.pre.props" />
<!-- ========================= Headers ======================== -->
<ItemGroup>
<ClInclude Include="inc\SafeDispatcherTimer.h" />
<ClInclude Include="pch.h" />
<ClInclude Include="inc\ScopedResourceLoader.h" />
<ClInclude Include="inc\LibraryResources.h" />
@@ -52,4 +53,4 @@
<!-- ========================= Globals ======================== -->
<Import Project="$(OpenConsoleDir)src\cppwinrt.build.post.props" />
<Import Project="$(OpenConsoleDir)src\common.nugetversions.targets" />
</Project>
</Project>

View File

@@ -2,21 +2,21 @@
<Project ToolsVersion="4.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<ItemGroup>
<Natvis Include="$(SolutionDir)tools\ConsoleTypes.natvis" />
<Natvis Include="$(MSBuildThisFileDirectory)..\..\natvis\wil.natvis" />
</ItemGroup>
<ItemGroup>
<ClCompile Include="pch.cpp" />
<ClCompile Include="LibraryResources.cpp" />
<ClCompile Include="ScopedResourceLoader.cpp" />
<ClCompile Include="Utils.cpp" />
</ItemGroup>
<ItemGroup>
<ClInclude Include="pch.h" />
<ClInclude Include="ScopedResourceLoader.h" />
<ClInclude Include="inc\LibraryResources.h" />
<ClInclude Include="inc\SafeDispatcherTimer.h" />
<ClInclude Include="inc\ScopedResourceLoader.h" />
<ClInclude Include="inc\ThrottledFunc.h" />
<ClInclude Include="inc\Utils.h" />
<ClInclude Include="inc\WtExeUtils.h" />
</ItemGroup>
<ItemGroup>
<None Include="packages.config" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,92 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.
#pragma once
// Par for the course, the XAML timer class is "self-referential". Releasing all references
// to an instance will not stop the timer. Only calling Stop() explicitly will achieve that.
struct SafeDispatcherTimer
{
SafeDispatcherTimer() = default;
SafeDispatcherTimer(SafeDispatcherTimer const&) = delete;
SafeDispatcherTimer& operator=(SafeDispatcherTimer const&) = delete;
SafeDispatcherTimer(SafeDispatcherTimer&&) = delete;
SafeDispatcherTimer& operator=(SafeDispatcherTimer&&) = delete;
~SafeDispatcherTimer()
{
Destroy();
}
explicit operator bool() const noexcept
{
return _timer != nullptr;
}
winrt::Windows::Foundation::TimeSpan Interval()
{
return _getTimer().Interval();
}
void Interval(winrt::Windows::Foundation::TimeSpan const& value)
{
_getTimer().Interval(value);
}
bool IsEnabled()
{
return _timer && _timer.IsEnabled();
}
void Tick(winrt::Windows::Foundation::EventHandler<winrt::Windows::Foundation::IInspectable> const& handler)
{
auto& timer = _getTimer();
if (_token)
{
timer.Tick(_token);
}
_token = timer.Tick(handler);
}
void Start()
{
_getTimer().Start();
}
void Stop() const
{
if (_timer)
{
_timer.Stop();
}
}
void Destroy()
{
if (!_timer)
{
return;
}
_timer.Stop();
if (_token)
{
_timer.Tick(_token);
}
_timer = nullptr;
_token = {};
}
private:
::winrt::Windows::UI::Xaml::DispatcherTimer& _getTimer()
{
if (!_timer)
{
_timer = ::winrt::Windows::UI::Xaml::DispatcherTimer{};
}
return _timer;
}
::winrt::Windows::UI::Xaml::DispatcherTimer _timer{ nullptr };
winrt::event_token _token;
};

View File

@@ -23,6 +23,8 @@ using namespace winrt::Microsoft::Terminal::Settings::Model;
using namespace ::Microsoft::Console;
using namespace ::Microsoft::Console::Types;
using namespace std::chrono_literals;
using namespace winrt::Windows::ApplicationModel;
using namespace winrt::Windows::UI::Notifications;
// This magic flag is "documented" at https://msdn.microsoft.com/en-us/library/windows/desktop/ms646301(v=vs.85).aspx
// "If the high-order bit is 1, the key is down; otherwise, it is up."
@@ -438,10 +440,7 @@ void AppHost::Close()
// After calling _window->Close() we should avoid creating more WinUI related actions.
// I suspect WinUI wouldn't like that very much. As such unregister all event handlers first.
_revokers = {};
if (_frameTimer)
{
_frameTimer.Tick(_frameTimerToken);
}
_frameTimer.Destroy();
_showHideWindowThrottler.reset();
_revokeWindowCallbacks();
@@ -538,23 +537,19 @@ void AppHost::LastTabClosed(const winrt::Windows::Foundation::IInspectable& /*se
{
_windowLogic.ClearPersistedWindowState();
}
// If the user closes the last tab, in the last window, _by closing the tab_
// (not by closing the whole window), we need to manually persist an empty
// window state here. That will cause the terminal to re-open with the usual
// settings (not the persisted state)
if (args.ClearPersistedState() &&
_windowManager.GetNumberOfPeasants() == 1)
{
_windowLogic.ClearPersistedWindowState();
}
// Remove ourself from the list of peasants so that we aren't included in
// any future requests. This will also mean we block until any existing
// event handler finishes.
_windowManager.SignalClose(_peasant);
PostQuitMessage(0);
if (Utils::IsWindows11())
{
PostQuitMessage(0);
}
else
{
PostMessageW(_window->GetInteropHandle(), WM_REFRIGERATE, 0, 0);
}
}
LaunchPosition AppHost::_GetWindowLaunchPosition()
@@ -1190,12 +1185,8 @@ void AppHost::_startFrameTimer()
// _updateFrameColor, which will actually handle setting the colors. If we
// already have a timer, just start that one.
if (_frameTimer == nullptr)
{
_frameTimer = winrt::Windows::UI::Xaml::DispatcherTimer();
_frameTimer.Interval(FrameUpdateInterval);
_frameTimerToken = _frameTimer.Tick({ this, &AppHost::_updateFrameColor });
}
_frameTimer.Tick({ this, &AppHost::_updateFrameColor });
_frameTimer.Interval(FrameUpdateInterval);
_frameTimer.Start();
}

View File

@@ -1,6 +1,8 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.
#pragma once
#include "pch.h"
#include "NonClientIslandWindow.h"
#include "NotificationIcon.h"
@@ -9,6 +11,8 @@
class AppHost : public std::enable_shared_from_this<AppHost>
{
public:
static constexpr DWORD WM_REFRIGERATE = WM_APP + 0;
AppHost(const winrt::TerminalApp::AppLogic& logic,
winrt::Microsoft::Terminal::Remoting::WindowRequestedArgs args,
const winrt::Microsoft::Terminal::Remoting::WindowManager& manager,
@@ -56,7 +60,7 @@ private:
std::shared_ptr<ThrottledFuncTrailing<bool>> _showHideWindowThrottler;
std::chrono::time_point<std::chrono::steady_clock> _started;
winrt::Windows::UI::Xaml::DispatcherTimer _frameTimer{ nullptr };
SafeDispatcherTimer _frameTimer;
uint32_t _launchShowWindowCommand{ SW_NORMAL };
@@ -67,6 +71,8 @@ private:
void _HandleCommandlineArgs(const winrt::Microsoft::Terminal::Remoting::WindowRequestedArgs& args);
void _HandleSessionRestore(const bool startedForContent);
// bool _HandleLaunchArgs();
winrt::Microsoft::Terminal::Settings::Model::LaunchPosition _GetWindowLaunchPosition();
void _HandleCreateWindow(const HWND hwnd, const til::rect& proposedRect);
@@ -165,7 +171,6 @@ private:
void _updateFrameColor(const winrt::Windows::Foundation::IInspectable&, const winrt::Windows::Foundation::IInspectable&);
winrt::event_token _GetWindowLayoutRequestedToken;
winrt::event_token _frameTimerToken;
// Helper struct. By putting these all into one struct, we can revoke them
// all at once, by assigning _revokers to a fresh Revokers instance. That'll

View File

@@ -26,6 +26,11 @@ NonClientIslandWindow::NonClientIslandWindow(const ElementTheme& requestedTheme)
{
}
NonClientIslandWindow::~NonClientIslandWindow()
{
Close();
}
void NonClientIslandWindow::Close()
{
// Avoid further callbacks into XAML/WinUI-land after we've Close()d the DesktopWindowXamlSource

View File

@@ -30,6 +30,7 @@ public:
static constexpr const int topBorderVisibleHeight = 1;
NonClientIslandWindow(const winrt::Windows::UI::Xaml::ElementTheme& requestedTheme) noexcept;
~NonClientIslandWindow() override;
void Refrigerate() noexcept override;

View File

@@ -5,19 +5,17 @@
#include "WindowEmperor.h"
#include "../inc/WindowingBehavior.h"
#include "../../types/inc/utils.hpp"
#include "../WinRTUtils/inc/WtExeUtils.h"
#include "resource.h"
#include "NotificationIcon.h"
#include <til/env.h>
using namespace winrt;
using namespace winrt::Microsoft::Terminal;
using namespace winrt::Microsoft::Terminal::Settings::Model;
using namespace winrt::Windows::Foundation;
using namespace winrt::Windows::ApplicationModel;
using namespace winrt::Windows::UI::Notifications;
using namespace ::Microsoft::Console;
using namespace std::chrono_literals;
using VirtualKeyModifiers = winrt::Windows::System::VirtualKeyModifiers;
@@ -38,26 +36,6 @@ WindowEmperor::WindowEmperor() noexcept :
});
_dispatcher = winrt::Windows::System::DispatcherQueue::GetForCurrentThread();
// BODGY
//
// There's a mysterious crash in XAML on Windows 10 if you just let the App
// get dtor'd. By all accounts, it doesn't make sense. To mitigate this, we
// need to intentionally leak a reference to our App. Crazily, if you just
// let the app get cleaned up with the rest of the process when the process
// exits, then it doesn't crash. But if you let it get explicitly dtor'd, it
// absolutely will crash on exit.
//
// GH#15410 has more details.
auto a{ _app };
::winrt::detach_abi(a);
}
WindowEmperor::~WindowEmperor()
{
_app.Close();
_app = nullptr;
}
void _buildArgsFromCommandline(std::vector<winrt::hstring>& args)
@@ -82,8 +60,89 @@ void _buildArgsFromCommandline(std::vector<winrt::hstring>& args)
}
}
bool WindowEmperor::HandleCommandlineArgs()
// Method Description:
// - Attempt to handle activated event args, which are a kind of "modern"
// activation, which we use for supporting toast notifications.
// - If we do find we were activated from a toast notification, we'll unpack the
// arguments from the toast. Then, we'll try to open up a connection to the
// monarch and ask the monarch to activate the right window.
// Arguments:
// - <none>
// Return Value:
// - <none>
bool WindowEmperor::_handleLaunchArgs()
try
{
// AppInstance::GetActivatedEventArgs will throw when unpackaged.
if (!IsPackaged())
{
return false;
}
// If someone clicks on a notification, then a fresh instance of
// windowsterminal.exe will spawn. We certainly don't want to create a new
// window for that - we only want to activate the window that created the
// actual notification. In the toast arg's payload will be the window id
// that sent the notification. We'll ask the window manager to try and
// activate that window ID, without even bothering to register as the
// monarch ourselves (if we can't find a monarch, then there are no windows
// running, so whoever sent it must have died.)
const auto activatedArgs = AppInstance::GetActivatedEventArgs();
if (activatedArgs != nullptr &&
activatedArgs.Kind() == Activation::ActivationKind::ToastNotification)
{
if (const auto& toastArgs{ activatedArgs.try_as<Activation::ToastNotificationActivatedEventArgs>() })
{
// Obtain the arguments from the notification
const auto args = toastArgs.Argument();
// Args is gonna look like
//
// "window=id&foo=bar&..."
//
// We need to first split on &, then split those pairs on =
// tabIndex code here is left as reference for parsing multiple
// arguments, despite it not being used currently.
uint32_t window;
// uint32_t tabIndex = 0;
const std::wstring_view argsView{ args };
const auto pairs = Utils::SplitString(argsView, L'&');
for (const auto& pair : pairs)
{
const auto pairParts = Utils::SplitString(pair, L'=');
if (pairParts.size() == 2)
{
if (til::at(pairParts, 0) == L"window")
{
window = std::wcstoul(pairParts[1].data(), nullptr, 10);
}
// else if (pairParts[0] == L"tabIndex")
// {
// // convert a wide string to a uint
// tabIndex = std::wcstoul(pairParts[1].data(), nullptr, 10);
// }
}
}
return winrt::Microsoft::Terminal::Remoting::WindowManager::SummonForNotification(window);
}
}
return false;
}
CATCH_LOG_RETURN_HR(false)
void WindowEmperor::HandleCommandlineArgs(int nCmdShow)
{
// Before handling any commandline arguments, check if this was a toast
// invocation. If it was, we can go ahead and totally ignore everything
// else.
if (_handleLaunchArgs())
{
TerminateProcess(GetCurrentProcess(), 0u);
}
std::vector<winrt::hstring> args;
_buildArgsFromCommandline(args);
const auto cwd{ wil::GetCurrentDirectoryW<std::wstring>() };
@@ -98,28 +157,32 @@ bool WindowEmperor::HandleCommandlineArgs()
}
}
// Get the requested initial state of the window from our startup info. For
// something like `start /min`, this will set the wShowWindow member to
// SW_SHOWMINIMIZED. We'll need to make sure is bubbled all the way through,
// so we can open a new window with the same state.
STARTUPINFOW si;
GetStartupInfoW(&si);
const uint32_t showWindow = WI_IsFlagSet(si.dwFlags, STARTF_USESHOWWINDOW) ? si.wShowWindow : SW_SHOW;
// GetEnvironmentStringsW() returns a double-null terminated string.
// The hstring(wchar_t*) constructor however only works for regular null-terminated strings.
// Due to that we need to manually search for the terminator.
winrt::hstring env;
{
const wil::unique_environstrings_ptr strings{ GetEnvironmentStringsW() };
const auto beg = strings.get();
auto end = beg;
const auto currentEnv{ til::env::from_current_environment() };
for (; *end; end += wcsnlen(end, SIZE_T_MAX) + 1)
{
}
Remoting::CommandlineArgs eventArgs{ { args }, { cwd }, showWindow, winrt::hstring{ currentEnv.to_string() } };
env = winrt::hstring{ beg, gsl::narrow<uint32_t>(end - beg) };
}
const Remoting::CommandlineArgs eventArgs{ args, cwd, gsl::narrow_cast<uint32_t>(nCmdShow), std::move(env) };
const auto isolatedMode{ _app.Logic().IsolatedMode() };
const auto result = _manager.ProposeCommandline(eventArgs, isolatedMode);
int exitCode = 0;
const bool makeWindow = result.ShouldCreateWindow();
if (makeWindow)
if (result.ShouldCreateWindow())
{
_createNewWindowThread(Remoting::WindowRequestedArgs{ result, eventArgs });
_becomeMonarch();
WaitForWindows();
}
else
{
@@ -127,11 +190,16 @@ bool WindowEmperor::HandleCommandlineArgs()
if (!res.Message.empty())
{
AppHost::s_DisplayMessageBox(res);
std::quick_exit(res.ExitCode);
}
exitCode = res.ExitCode;
}
return makeWindow;
// There's a mysterious crash in XAML on Windows 10 if you just let _app get destroyed (GH#15410).
// We also need to ensure that all UI threads exit before WindowEmperor leaves the scope on the main thread (MSFT:46744208).
// Both problems can be solved and the shutdown accelerated by using TerminateProcess.
// std::exit(), etc., cannot be used here, because those use ExitProcess for unpackaged applications.
TerminateProcess(GetCurrentProcess(), gsl::narrow_cast<UINT>(exitCode));
__assume(false);
}
void WindowEmperor::WaitForWindows()
@@ -142,6 +210,9 @@ void WindowEmperor::WaitForWindows()
TranslateMessage(&message);
DispatchMessage(&message);
}
_finalizeSessionPersistence();
TerminateProcess(GetCurrentProcess(), 0);
}
void WindowEmperor::_createNewWindowThread(const Remoting::WindowRequestedArgs& args)
@@ -584,21 +655,20 @@ LRESULT WindowEmperor::_messageHandler(UINT const message, WPARAM const wParam,
// we'll undoubtedly crash.
winrt::fire_and_forget WindowEmperor::_close()
{
{
auto fridge{ _oldThreads.lock() };
for (auto& window : *fridge)
{
window->ThrowAway();
}
fridge->clear();
}
// Important! Switch back to the main thread for the emperor. That way, the
// quit will go to the emperor's message pump.
co_await wil::resume_foreground(_dispatcher);
PostQuitMessage(0);
}
void WindowEmperor::_finalizeSessionPersistence() const
{
const auto state = ApplicationState::SharedInstance();
// Ensure to write the state.json before we TerminateProcess()
state.Flush();
}
#pragma endregion
#pragma region GlobalHotkeys

View File

@@ -24,12 +24,12 @@ class WindowEmperor : public std::enable_shared_from_this<WindowEmperor>
{
public:
WindowEmperor() noexcept;
~WindowEmperor();
void WaitForWindows();
bool HandleCommandlineArgs();
void HandleCommandlineArgs(int nCmdShow);
private:
bool _handleLaunchArgs();
void _createNewWindowThread(const winrt::Microsoft::Terminal::Remoting::WindowRequestedArgs& args);
[[nodiscard]] static LRESULT __stdcall _wndProc(HWND const window, UINT const message, WPARAM const wparam, LPARAM const lparam) noexcept;
@@ -79,6 +79,7 @@ private:
winrt::fire_and_forget _setupGlobalHotkeys();
winrt::fire_and_forget _close();
void _finalizeSessionPersistence() const;
void _createNotificationIcon();
void _destroyNotificationIcon();

View File

@@ -4,6 +4,8 @@
#include "pch.h"
#include "WindowThread.h"
using namespace winrt::Microsoft::Terminal::Remoting;
WindowThread::WindowThread(winrt::TerminalApp::AppLogic logic,
winrt::Microsoft::Terminal::Remoting::WindowRequestedArgs args,
winrt::Microsoft::Terminal::Remoting::WindowManager manager,
@@ -81,17 +83,6 @@ void WindowThread::RundownForExit()
_pumpRemainingXamlMessages();
}
void WindowThread::ThrowAway()
{
// raise the signal to unblock KeepWarm. We won't have a host, so we'll drop
// out of the message loop to eventually RundownForExit.
//
// This should only be called when the app is fully quitting. After this is
// called on any thread, on win10, we won't be able to call into XAML
// anymore.
_microwaveBuzzer.notify_one();
}
// Method Description:
// - Check if we should keep this window alive, to try it's message loop again.
// If we were refrigerated for later, then this will block the thread on the
@@ -108,29 +99,24 @@ bool WindowThread::KeepWarm()
return true;
}
// If we're refrigerated, then wait on the microwave signal, which will be
// raised when we get re-heated by another thread to reactivate us.
if (_warmWindow != nullptr)
// Even when the _host has been destroyed the HWND will continue receiving messages, in particular WM_DISPATCHNOTIFY at least once a second. This is important to Windows as it keeps your room warm.
MSG msg;
for (;;)
{
std::unique_lock lock(_microwave);
_microwaveBuzzer.wait(lock);
// If ThrowAway() was called, then the buzzer will be signalled without
// setting a new _host. In that case, the app is quitting, for real. We
// just want to exit with false.
const bool reheated = _host != nullptr;
if (reheated)
if (!GetMessageW(&msg, nullptr, 0, 0))
{
return false;
}
// We're using a single window message (WM_REFRIGERATE) to indicate both
// state transitions. In this case, the window is actually being woken up.
if (msg.message == AppHost::WM_REFRIGERATE)
{
_UpdateSettingsRequestedToken = _host->UpdateSettingsRequested([this]() { _UpdateSettingsRequestedHandlers(); });
// Re-initialize the host here, on the window thread
_host->Initialize();
return true;
}
return reheated;
}
else
{
return false;
DispatchMessageW(&msg);
}
}
@@ -154,24 +140,22 @@ void WindowThread::Refrigerate()
// Method Description:
// - "Reheat" this thread for reuse. We'll build a new AppHost, and pass in the
// existing window to it. We'll then trigger the _microwaveBuzzer, so KeepWarm
// (which is on the UI thread) will get unblocked, and we can initialize this
// window.
void WindowThread::Microwave(
winrt::Microsoft::Terminal::Remoting::WindowRequestedArgs args,
winrt::Microsoft::Terminal::Remoting::Peasant peasant)
// existing window to it. We'll then wake up the thread stuck in KeepWarm().
void WindowThread::Microwave(WindowRequestedArgs args, Peasant peasant)
{
const auto hwnd = _warmWindow->GetInteropHandle();
_peasant = std::move(peasant);
_args = std::move(args);
_host = std::make_shared<::AppHost>(_appLogic,
_args,
_manager,
_peasant,
std::move(_warmWindow));
_host = std::make_shared<AppHost>(_appLogic,
_args,
_manager,
_peasant,
std::move(_warmWindow));
// raise the signal to unblock KeepWarm and start the window message loop again.
_microwaveBuzzer.notify_one();
PostMessageW(hwnd, AppHost::WM_REFRIGERATE, 0, 0);
}
winrt::TerminalApp::TerminalWindow WindowThread::Logic()
@@ -198,6 +182,15 @@ int WindowThread::_messagePump()
while (GetMessageW(&message, nullptr, 0, 0))
{
// We're using a single window message (WM_REFRIGERATE) to indicate both
// state transitions. In this case, the window is actually being refrigerated.
// This will break us out of our main message loop we'll eventually start
// the loop in WindowThread::KeepWarm to await a call to Microwave().
if (message.message == AppHost::WM_REFRIGERATE)
{
break;
}
// GH#638 (Pressing F7 brings up both the history AND a caret browsing message)
// The Xaml input stack doesn't allow an application to suppress the "caret browsing"
// dialog experience triggered when you press F7. Official recommendation from the Xaml

View File

@@ -22,7 +22,6 @@ public:
void Microwave(
winrt::Microsoft::Terminal::Remoting::WindowRequestedArgs args,
winrt::Microsoft::Terminal::Remoting::Peasant peasant);
void ThrowAway();
uint64_t PeasantID();
@@ -43,8 +42,6 @@ private:
winrt::event_token _UpdateSettingsRequestedToken;
std::unique_ptr<::IslandWindow> _warmWindow{ nullptr };
std::mutex _microwave;
std::condition_variable _microwaveBuzzer;
int _messagePump();
void _pumpRemainingXamlMessages();

View File

@@ -83,7 +83,7 @@ static void EnsureNativeArchitecture()
}
}
int __stdcall wWinMain(HINSTANCE, HINSTANCE, LPWSTR, int)
int __stdcall wWinMain(HINSTANCE, HINSTANCE, LPWSTR, int nCmdShow)
{
TraceLoggingRegister(g_hWindowsTerminalProvider);
::Microsoft::Console::ErrorReporting::EnableFallbackFailureReporting(g_hWindowsTerminalProvider);
@@ -115,8 +115,5 @@ int __stdcall wWinMain(HINSTANCE, HINSTANCE, LPWSTR, int)
winrt::init_apartment(winrt::apartment_type::single_threaded);
const auto emperor = std::make_shared<::WindowEmperor>();
if (emperor->HandleCommandlineArgs())
{
emperor->WaitForWindows();
}
emperor->HandleCommandlineArgs(nCmdShow);
}

View File

@@ -48,7 +48,7 @@ Abstract:
#include <wil/cppwinrt.h>
// Needed just for XamlIslands to work at all:
#include <winrt/Windows.system.h>
#include <winrt/Windows.System.h>
#include <winrt/Windows.Foundation.Collections.h>
#include <winrt/Windows.UI.Xaml.Hosting.h>
#include <windows.ui.xaml.hosting.desktopwindowxamlsource.h>
@@ -59,11 +59,18 @@ Abstract:
// * Media for ScaleTransform
// * ApplicationModel for finding the path to wt.exe
// * Primitives for Popup (used by GetOpenPopupsForXamlRoot)
// * XML, Notifications, Activation: for notification activations
#include <winrt/Windows.ApplicationModel.h>
#include <winrt/Windows.ApplicationModel.Resources.Core.h>
#include <winrt/Windows.ApplicationModel.Activation.h>
#include <winrt/Windows.Data.Xml.Dom.h>
#include <winrt/Windows.UI.Composition.h>
#include <winrt/Windows.UI.Core.h>
#include <winrt/Windows.UI.Notifications.h>
#include <winrt/Windows.UI.Xaml.Controls.h>
#include <winrt/Windows.UI.Xaml.Controls.Primitives.h>
#include <winrt/Windows.UI.Xaml.Data.h>
#include <winrt/Windows.ui.xaml.media.h>
#include <winrt/Windows.UI.Xaml.Media.h>
#include <winrt/Windows.ApplicationModel.h>
#include <winrt/Windows.ApplicationModel.Resources.Core.h>
#include <winrt/Windows.UI.Composition.h>
@@ -91,5 +98,7 @@ TRACELOGGING_DECLARE_PROVIDER(g_hWindowsTerminalProvider);
#include "til.h"
#include "til/mutex.h"
#include <SafeDispatcherTimer.h>
#include <cppwinrt_utils.h>
#include <wil/cppwinrt_helpers.h> // must go after the CoreDispatcher type is defined

View File

@@ -49,7 +49,8 @@
X(bool, DetectURLs, true) \
X(bool, VtPassthrough, false) \
X(bool, AutoMarkPrompts) \
X(bool, RepositionCursorWithMouse, false)
X(bool, RepositionCursorWithMouse, false) \
X(bool, AllowNotifications, true)
// --------------------------- Control Settings ---------------------------
// All of these settings are defined in IControlSettings.
@@ -75,4 +76,5 @@
X(bool, UseAtlasEngine, true) \
X(bool, UseBackgroundImageForWindow, false) \
X(bool, ShowMarks, false) \
X(winrt::Microsoft::Terminal::Control::CopyFormat, CopyFormatting, 0) \
X(bool, RightClickContextMenu, false)

View File

@@ -116,7 +116,7 @@
<WarningLevel>Level4</WarningLevel>
<TreatSpecificWarningsAsErrors>4189;4100;4242;4389;4244</TreatSpecificWarningsAsErrors>
<!--<WarningLevel>EnableAllWarnings</WarningLevel>-->
<!--<TreatWarningAsError>true</TreatWarningAsError>-->
<TreatWarningAsError>true</TreatWarningAsError>
<!--
C4201: nonstandard extension used: nameless struct/union
Conhost code uses a lot of nameless structs/unions.

View File

@@ -62,7 +62,7 @@
<Import Project="$(MSBuildThisFileDirectory)..\build\rules\Microsoft.UI.Xaml.Additional.targets" Condition="'$(TerminalMUX)' == 'true'" />
<!-- WIL (so widely used that this one does not have a TerminalWIL opt-in property; it is automatic) -->
<Import Project="$(MSBuildThisFileDirectory)..\packages\Microsoft.Windows.ImplementationLibrary.1.0.230824.2\build\native\Microsoft.Windows.ImplementationLibrary.targets" Condition="Exists('$(MSBuildThisFileDirectory)..\packages\Microsoft.Windows.ImplementationLibrary.1.0.230824.2\build\native\Microsoft.Windows.ImplementationLibrary.targets')" />
<Import Project="$(MSBuildThisFileDirectory)..\packages\Microsoft.Windows.ImplementationLibrary.1.0.240122.1\build\native\Microsoft.Windows.ImplementationLibrary.targets" Condition="Exists('$(MSBuildThisFileDirectory)..\packages\Microsoft.Windows.ImplementationLibrary.1.0.240122.1\build\native\Microsoft.Windows.ImplementationLibrary.targets')" />
</ImportGroup>
@@ -93,7 +93,7 @@
<Error Condition="'$(TerminalMUX)' == 'true' AND !Exists('$(MSBuildThisFileDirectory)..\packages\Microsoft.Web.WebView2.1.0.1661.34\build\native\Microsoft.Web.WebView2.targets')" Text="$([System.String]::Format('$(ErrorText)', '$(MSBuildThisFileDirectory)..\packages\Microsoft.Web.WebView2.1.0.1661.34\build\native\Microsoft.Web.WebView2.targets'))" />
<!-- WIL (so widely used that this one does not have a TerminalWIL opt-in property; it is automatic) -->
<Error Condition="!Exists('$(MSBuildThisFileDirectory)..\packages\Microsoft.Windows.ImplementationLibrary.1.0.230824.2\build\native\Microsoft.Windows.ImplementationLibrary.targets')" Text="$([System.String]::Format('$(ErrorText)', '$(MSBuildThisFileDirectory)..\packages\Microsoft.Windows.ImplementationLibrary.1.0.230824.2\build\native\Microsoft.Windows.ImplementationLibrary.targets'))" />
<Error Condition="!Exists('$(MSBuildThisFileDirectory)..\packages\Microsoft.Windows.ImplementationLibrary.1.0.240122.1\build\native\Microsoft.Windows.ImplementationLibrary.targets')" Text="$([System.String]::Format('$(ErrorText)', '$(MSBuildThisFileDirectory)..\packages\Microsoft.Windows.ImplementationLibrary.1.0.240122.1\build\native\Microsoft.Windows.ImplementationLibrary.targets'))" />
</Target>

View File

@@ -177,4 +177,25 @@
</alwaysEnabledBrandingTokens>
</feature>
<feature>
<name>Feature_KeypadModeEnabled</name>
<description>Enables the DECKPAM, DECKPNM sequences to work as intended </description>
<id>16654</id>
<stage>AlwaysDisabled</stage>
<alwaysEnabledBrandingTokens>
<brandingToken>Dev</brandingToken>
</alwaysEnabledBrandingTokens>
</feature>
<feature>
<name>Feature_Notifications</name>
<description>Enables OSC777 notifications</description>
<id>16654</id>
<stage>AlwaysDisabled</stage>
<alwaysEnabledBrandingTokens>
<brandingToken>Dev</brandingToken>
<brandingToken>Canary</brandingToken>
</alwaysEnabledBrandingTokens>
</feature>
</featureStaging>

View File

@@ -50,8 +50,8 @@ public:
ULONG& events) noexcept override;
[[nodiscard]] HRESULT GetConsoleInputImpl(IConsoleInputObject& context,
INPUT_RECORD* outEvents,
size_t* eventReadCount,
InputEventQueue& outEvents,
const size_t eventReadCount,
INPUT_READ_HANDLE_DATA& readHandleState,
const bool IsUnicode,
const bool IsPeek,

View File

@@ -107,8 +107,8 @@ void VtApiRoutines::_SynchronizeCursor(std::unique_ptr<IWaitRoutine>& waiter) no
[[nodiscard]] HRESULT VtApiRoutines::GetConsoleInputImpl(
IConsoleInputObject& context,
INPUT_RECORD* outEvents,
size_t* eventReadCount,
InputEventQueue& outEvents,
const size_t eventReadCount,
INPUT_READ_HANDLE_DATA& readHandleState,
const bool IsUnicode,
const bool IsPeek,

View File

@@ -53,8 +53,8 @@ public:
ULONG& events) noexcept override;
[[nodiscard]] HRESULT GetConsoleInputImpl(IConsoleInputObject& context,
INPUT_RECORD* outEvents,
size_t* eventReadCount,
InputEventQueue& outEvents,
const size_t eventReadCount,
INPUT_READ_HANDLE_DATA& readHandleState,
const bool IsUnicode,
const bool IsPeek,

View File

@@ -51,8 +51,8 @@ using Microsoft::Console::Interactivity::ServiceLocator;
// block, this will be returned along with context in *ppWaiter.
// - Or an out of memory/math/string error message in NTSTATUS format.
[[nodiscard]] HRESULT ApiRoutines::GetConsoleInputImpl(IConsoleInputObject& inputBuffer,
INPUT_RECORD* outEvents,
size_t* eventReadCount,
InputEventQueue& outEvents,
const size_t eventReadCount,
INPUT_READ_HANDLE_DATA& readHandleState,
const bool IsUnicode,
const bool IsPeek,
@@ -62,30 +62,64 @@ using Microsoft::Console::Interactivity::ServiceLocator;
{
waiter.reset();
if (*eventReadCount == 0)
if (eventReadCount == 0)
{
return S_OK;
return STATUS_SUCCESS;
}
LockConsole();
auto Unlock = wil::scope_exit([&] { UnlockConsole(); });
const InputBuffer::ReadDescriptor readDesc{
.wide = IsUnicode,
.records = true,
.peek = IsPeek,
};
const auto count = inputBuffer.Read(readDesc, outEvents, *eventReadCount * sizeof(INPUT_RECORD));
if (count)
const auto Status = inputBuffer.Read(outEvents,
eventReadCount,
IsPeek,
true,
IsUnicode,
false);
if (CONSOLE_STATUS_WAIT == Status)
{
*eventReadCount = count / sizeof(INPUT_RECORD);
return S_OK;
// If we're told to wait until later, move all of our context
// to the read data object and send it back up to the server.
waiter = std::make_unique<DirectReadData>(&inputBuffer,
&readHandleState,
eventReadCount);
}
return Status;
}
CATCH_RETURN();
}
// Routine Description:
// - Writes events to the input buffer
// Arguments:
// - context - the input buffer to write to
// - events - the events to written
// - written - on output, the number of events written
// - append - true if events should be written to the end of the input
// buffer, false if they should be written to the front
// Return Value:
// - HRESULT indicating success or failure
[[nodiscard]] static HRESULT _WriteConsoleInputWImplHelper(InputBuffer& context,
const std::span<const INPUT_RECORD>& events,
size_t& written,
const bool append) noexcept
{
try
{
written = 0;
// add to InputBuffer
if (append)
{
written = context.Write(events);
}
else
{
written = context.Prepend(events);
}
// If we're told to wait until later, move all of our context
// to the read data object and send it back up to the server.
waiter = std::make_unique<DirectReadData>(&inputBuffer, &readHandleState, *eventReadCount);
return CONSOLE_STATUS_WAIT;
return S_OK;
}
CATCH_RETURN();
}
@@ -191,11 +225,7 @@ try
}
}
// TODO: append/prepend
UNREFERENCED_PARAMETER(append);
context.Write(events);
written = buffer.size();
return S_OK;
return _WriteConsoleInputWImplHelper(context, events, written, append);
}
CATCH_RETURN();
@@ -213,20 +243,18 @@ CATCH_RETURN();
const std::span<const INPUT_RECORD> buffer,
size_t& written,
const bool append) noexcept
try
{
written = 0;
LockConsole();
auto Unlock = wil::scope_exit([&] { UnlockConsole(); });
// TODO: append/prepend
UNREFERENCED_PARAMETER(append);
context.Write(buffer);
written = buffer.size();
return S_OK;
try
{
return _WriteConsoleInputWImplHelper(context, buffer, written, append);
}
CATCH_RETURN();
}
CATCH_RETURN();
// Routine Description:
// - This is used when the app is reading output as cells and needs them converted

View File

@@ -24,6 +24,12 @@ bool IsInProcessedInputMode()
return (gci.pInputBuffer->InputMode & ENABLE_PROCESSED_INPUT) != 0;
}
bool IsInVirtualTerminalInputMode()
{
const auto& gci = ServiceLocator::LocateGlobals().getConsoleInformation();
return WI_IsFlagSet(gci.pInputBuffer->InputMode, ENABLE_VIRTUAL_TERMINAL_INPUT);
}
BOOL IsSystemKey(const WORD wVirtualKeyCode)
{
switch (wVirtualKeyCode)
@@ -191,12 +197,23 @@ void HandleFocusEvent(const BOOL fSetFocus)
}
void HandleMenuEvent(const DWORD wParam)
try
{
const auto& gci = ServiceLocator::LocateGlobals().getConsoleInformation();
gci.pInputBuffer->Write(SynthesizeMenuEvent(wParam));
size_t EventsWritten = 0;
try
{
EventsWritten = gci.pInputBuffer->Write(SynthesizeMenuEvent(wParam));
if (EventsWritten != 1)
{
LOG_HR_MSG(E_FAIL, "PutInputInBuffer: EventsWritten != 1, 1 expected");
}
}
catch (...)
{
LOG_HR(wil::ResultFromCaughtException());
}
}
CATCH_LOG()
void HandleCtrlEvent(const DWORD EventType)
{

View File

@@ -12,7 +12,7 @@
#include "../interactivity/inc/ServiceLocator.hpp"
#include "../types/inc/GlyphWidth.hpp"
#pragma warning(disable : 4100)
#define INPUT_BUFFER_DEFAULT_INPUT_MODE (ENABLE_LINE_INPUT | ENABLE_PROCESSED_INPUT | ENABLE_ECHO_INPUT | ENABLE_MOUSE_INPUT)
using Microsoft::Console::Interactivity::ServiceLocator;
using Microsoft::Console::VirtualTerminal::TerminalInput;
@@ -24,7 +24,8 @@ using namespace Microsoft::Console;
// - None
// Return Value:
// - A new instance of InputBuffer
InputBuffer::InputBuffer()
InputBuffer::InputBuffer() :
InputMode{ INPUT_BUFFER_DEFAULT_INPUT_MODE }
{
// initialize buffer header
fInComposition = false;
@@ -167,7 +168,18 @@ void InputBuffer::Cache(std::wstring_view source)
// Moves up to `count`, previously cached events into `target`.
size_t InputBuffer::ConsumeCached(bool isUnicode, size_t count, InputEventQueue& target)
{
return 0;
_switchReadingMode(isUnicode ? ReadingMode::InputEventsW : ReadingMode::InputEventsA);
size_t i = 0;
while (i < count && !_cachedInputEvents.empty())
{
target.push_back(std::move(_cachedInputEvents.front()));
_cachedInputEvents.pop_front();
i++;
}
return i;
}
// Copies up to `count`, previously cached events into `target`.
@@ -199,6 +211,10 @@ void InputBuffer::Cache(bool isUnicode, InputEventQueue& source, size_t expected
if (source.size() > expectedSourceSize)
{
_cachedInputEvents.insert(
_cachedInputEvents.end(),
std::make_move_iterator(source.begin() + expectedSourceSize),
std::make_move_iterator(source.end()));
source.resize(expectedSourceSize);
}
}
@@ -219,6 +235,8 @@ void InputBuffer::_switchReadingModeSlowPath(ReadingMode mode)
_cachedTextW = std::wstring{};
_cachedTextReaderW = {};
_cachedInputEvents = std::deque<INPUT_RECORD>{};
_readingMode = mode;
}
@@ -259,6 +277,19 @@ void InputBuffer::StoreWritePartialByteSequence(const INPUT_RECORD& event) noexc
_writePartialByteSequence = event;
}
// Routine Description:
// - This routine resets the input buffer information fields to their initial values.
// Arguments:
// Return Value:
// Note:
// - The console lock must be held when calling this routine.
void InputBuffer::ReinitializeInputBuffer()
{
ServiceLocator::LocateGlobals().hInputEvent.ResetEvent();
InputMode = INPUT_BUFFER_DEFAULT_INPUT_MODE;
_storage.clear();
}
// Routine Description:
// - Wakes up readers waiting for data to read.
// Arguments:
@@ -291,7 +322,7 @@ void InputBuffer::TerminateRead(_In_ WaitTerminationReason Flag)
// - The console lock must be held when calling this routine.
size_t InputBuffer::GetNumberOfReadyEvents() const noexcept
{
return 0;
return _storage.size();
}
// Routine Description:
@@ -318,197 +349,260 @@ void InputBuffer::Flush()
// - The console lock must be held when calling this routine.
void InputBuffer::FlushAllButKeys()
{
auto newEnd = std::remove_if(_storage.begin(), _storage.end(), [](const INPUT_RECORD& event) {
return event.EventType != KEY_EVENT;
});
_storage.erase(newEnd, _storage.end());
}
static void transfer(InputBuffer::RecordVec& in, std::span<INPUT_RECORD> out)
// Routine Description:
// - This routine reads from the input buffer.
// - It can convert returned data to through the currently set Input CP, it can optionally return a wait condition
// if there isn't enough data in the buffer, and it can be set to not remove records as it reads them out.
// Note:
// - The console lock must be held when calling this routine.
// Arguments:
// - OutEvents - deque to store the read events
// - AmountToRead - the amount of events to try to read
// - Peek - If true, copy events to pInputRecord but don't remove them from the input buffer.
// - WaitForData - if true, wait until an event is input (if there aren't enough to fill client buffer). if false, return immediately
// - Unicode - true if the data in key events should be treated as unicode. false if they should be converted by the current input CP.
// - Stream - true if read should unpack KeyEvents that have a >1 repeat count. AmountToRead must be 1 if Stream is true.
// Return Value:
// - STATUS_SUCCESS if records were read into the client buffer and everything is OK.
// - CONSOLE_STATUS_WAIT if there weren't enough records to satisfy the request (and waits are allowed)
// - otherwise a suitable memory/math/string error in NTSTATUS form.
[[nodiscard]] NTSTATUS InputBuffer::Read(_Out_ InputEventQueue& OutEvents,
const size_t AmountToRead,
const bool Peek,
const bool WaitForData,
const bool Unicode,
const bool Stream)
try
{
const auto count = std::min(in.size(), out.size());
std::copy_n(in.begin(), count, out.begin());
if (count == out.size())
assert(OutEvents.empty());
const auto cp = ServiceLocator::LocateGlobals().getConsoleInformation().CP;
if (Peek)
{
in.clear();
PeekCached(Unicode, AmountToRead, OutEvents);
}
else
{
in.erase(in.begin(), in.begin() + count);
ConsumeCached(Unicode, AmountToRead, OutEvents);
}
}
static void transfer(InputBuffer::RecordVec& in, std::span<wchar_t> out)
{
size_t inUsed = 0;
size_t outUsed = 0;
for (auto& r : in)
{
if (outUsed == out.size())
{
break;
}
if (r.EventType == KEY_EVENT && r.Event.KeyEvent.uChar.UnicodeChar != 0)
{
out[outUsed++] = r.Event.KeyEvent.uChar.UnicodeChar;
}
inUsed++;
}
}
static void transfer(InputBuffer::TextVec& in, std::span<INPUT_RECORD> out)
{
// TODO: MSFT 14150722 - can these const values be generated at
// runtime without breaking compatibility?
static constexpr WORD altScanCode = 0x38;
static constexpr WORD leftShiftScanCode = 0x2A;
size_t inUsed = 0;
size_t outUsed = 0;
for (const auto wch : in)
{
if (outUsed + 4 > out.size())
{
break;
}
const auto keyState = OneCoreSafeVkKeyScanW(wch);
const auto vk = LOBYTE(keyState);
const auto sc = gsl::narrow<WORD>(OneCoreSafeMapVirtualKeyW(vk, MAPVK_VK_TO_VSC));
// The caller provides us with the result of VkKeyScanW() in keyState.
// The magic constants below are the expected (documented) return values from VkKeyScanW().
const auto modifierState = HIBYTE(keyState);
const auto shiftSet = WI_IsFlagSet(modifierState, 1);
const auto ctrlSet = WI_IsFlagSet(modifierState, 2);
const auto altSet = WI_IsFlagSet(modifierState, 4);
const auto altGrSet = WI_AreAllFlagsSet(modifierState, 4 | 2);
if (altGrSet)
{
out[outUsed++] = SynthesizeKeyEvent(true, 1, VK_MENU, altScanCode, 0, ENHANCED_KEY | LEFT_CTRL_PRESSED | RIGHT_ALT_PRESSED);
}
else if (shiftSet)
{
out[outUsed++] = SynthesizeKeyEvent(true, 1, VK_SHIFT, leftShiftScanCode, 0, SHIFT_PRESSED);
}
auto keyEvent = SynthesizeKeyEvent(true, 1, vk, sc, wch, 0);
WI_SetFlagIf(keyEvent.Event.KeyEvent.dwControlKeyState, SHIFT_PRESSED, shiftSet);
WI_SetFlagIf(keyEvent.Event.KeyEvent.dwControlKeyState, LEFT_CTRL_PRESSED, ctrlSet);
WI_SetFlagIf(keyEvent.Event.KeyEvent.dwControlKeyState, RIGHT_ALT_PRESSED, altSet);
out[outUsed++] = keyEvent;
keyEvent.Event.KeyEvent.bKeyDown = FALSE;
out[outUsed++] = keyEvent;
// handle yucky alt-gr keys
if (altGrSet)
{
out[outUsed++] = SynthesizeKeyEvent(false, 1, VK_MENU, altScanCode, 0, ENHANCED_KEY);
}
else if (shiftSet)
{
out[outUsed++] = SynthesizeKeyEvent(false, 1, VK_SHIFT, leftShiftScanCode, 0, 0);
}
inUsed++;
}
}
static void transfer(InputBuffer::TextVec& in, std::span<wchar_t> out)
{
const auto count = std::min(in.size(), out.size());
std::copy_n(in.begin(), count, out.begin());
if (count == out.size())
{
in.clear();
}
else
{
in.erase(in.begin(), in.begin() + count);
}
}
size_t InputBuffer::Read(ReadDescriptor desc, void* data, size_t capacityInBytes)
{
auto remaining = capacityInBytes;
auto it = _storage.begin();
const auto end = _storage.end();
for (; it != end; ++it)
while (it != end && OutEvents.size() < AmountToRead)
{
std::visit(
[&]<typename T>(T& arg) {
if constexpr (std::is_same_v<T, RecordVec>)
if (it->EventType == KEY_EVENT)
{
auto event = *it;
WORD repeat = 1;
// for stream reads we need to split any key events that have been coalesced
if (Stream)
{
repeat = std::max<WORD>(1, event.Event.KeyEvent.wRepeatCount);
event.Event.KeyEvent.wRepeatCount = 1;
}
if (Unicode)
{
do
{
if (desc.records)
{
transfer(arg, { static_cast<INPUT_RECORD*>(data), capacityInBytes / sizeof(INPUT_RECORD) });
}
else
{
transfer(arg, { static_cast<wchar_t*>(data), capacityInBytes / sizeof(wchar_t) });
}
}
else if constexpr (std::is_same_v<T, TextVec>)
OutEvents.push_back(event);
repeat--;
} while (repeat > 0 && OutEvents.size() < AmountToRead);
}
else
{
const auto wch = event.Event.KeyEvent.uChar.UnicodeChar;
char buffer[8];
const auto length = WideCharToMultiByte(cp, 0, &wch, 1, &buffer[0], sizeof(buffer), nullptr, nullptr);
THROW_LAST_ERROR_IF(length <= 0);
const std::string_view str{ &buffer[0], gsl::narrow_cast<size_t>(length) };
do
{
if (desc.records)
for (const auto& ch : str)
{
transfer(arg, { static_cast<INPUT_RECORD*>(data), capacityInBytes / sizeof(INPUT_RECORD) });
// char is signed and assigning it to UnicodeChar would cause sign-extension.
// unsigned char doesn't have this problem.
event.Event.KeyEvent.uChar.UnicodeChar = til::bit_cast<uint8_t>(ch);
OutEvents.push_back(event);
}
else
{
transfer(arg, { static_cast<wchar_t*>(data), capacityInBytes / sizeof(wchar_t) });
}
}
else
{
static_assert(sizeof(arg) == 0, "non-exhaustive visitor!");
}
},
*it);
repeat--;
} while (repeat > 0 && OutEvents.size() < AmountToRead);
}
if (repeat && !Peek)
{
it->Event.KeyEvent.wRepeatCount = repeat;
break;
}
}
else
{
OutEvents.push_back(*it);
}
++it;
}
if (!desc.peek)
if (!Peek)
{
_storage.erase(_storage.begin(), it);
}
return capacityInBytes - remaining;
}
Cache(Unicode, OutEvents, AmountToRead);
void InputBuffer::Write(const INPUT_RECORD& record)
if (OutEvents.empty())
{
return WaitForData ? CONSOLE_STATUS_WAIT : STATUS_SUCCESS;
}
if (_storage.empty())
{
ServiceLocator::LocateGlobals().hInputEvent.ResetEvent();
}
return STATUS_SUCCESS;
}
catch (...)
{
Write(std::span{ &record, 1 });
return NTSTATUS_FROM_HRESULT(wil::ResultFromCaughtException());
}
void InputBuffer::Write(const std::span<const INPUT_RECORD>& records)
try
// Routine Description:
// - Writes events to the beginning of the input buffer.
// Arguments:
// - inEvents - events to write to buffer.
// - eventsWritten - The number of events written to the buffer on exit.
// Return Value:
// S_OK if successful.
// Note:
// - The console lock must be held when calling this routine.
size_t InputBuffer::Prepend(const std::span<const INPUT_RECORD>& inEvents)
{
if (records.empty())
try
{
return;
if (inEvents.empty())
{
return STATUS_SUCCESS;
}
_vtInputShouldSuppress = true;
auto resetVtInputSuppress = wil::scope_exit([&]() { _vtInputShouldSuppress = false; });
// read all of the records out of the buffer, then write the
// prepend ones, then write the original set. We need to do it
// this way to handle any coalescing that might occur.
// get all of the existing records, "emptying" the buffer
std::deque<INPUT_RECORD> existingStorage;
existingStorage.swap(_storage);
// We will need this variable to pass to _WriteBuffer so it can attempt to determine wait status.
// However, because we swapped the storage out from under it with an empty deque, it will always
// return true after the first one (as it is filling the newly emptied backing deque.)
// Then after the second one, because we've inserted some input, it will always say false.
auto unusedWaitStatus = false;
// write the prepend records
size_t prependEventsWritten;
_WriteBuffer(inEvents, prependEventsWritten, unusedWaitStatus);
FAIL_FAST_IF(!(unusedWaitStatus));
for (const auto& event : existingStorage)
{
_storage.push_back(event);
}
// We need to set the wait event if there were 0 events in the
// input queue when we started.
// Because we did interesting manipulation of the wait queue
// in order to prepend, we can't trust what _WriteBuffer said
// and instead need to set the event if the original backing
// buffer (the one we swapped out at the top) was empty
// when this whole thing started.
if (existingStorage.empty())
{
ServiceLocator::LocateGlobals().hInputEvent.SetEvent();
}
WakeUpReadersWaitingForData();
return prependEventsWritten;
}
const auto initiallyEmpty = _storage.empty();
if (initiallyEmpty || _storage.back().index() != 0)
catch (...)
{
_storage.emplace_back(RecordVec{});
LOG_HR(wil::ResultFromCaughtException());
return 0;
}
auto& v = *std::get_if<RecordVec>(&_storage.back());
v.insert(v.end(), records.begin(), records.end());
if (initiallyEmpty)
{
ServiceLocator::LocateGlobals().hInputEvent.SetEvent();
}
WakeUpReadersWaitingForData();
}
CATCH_LOG()
void InputBuffer::Write(const std::wstring_view& text)
// Routine Description:
// - Writes event to the input buffer. Wakes up any readers that are
// waiting for additional input events.
// Arguments:
// - inEvent - input event to store in the buffer.
// Return Value:
// - The number of events that were written to input buffer.
// Note:
// - The console lock must be held when calling this routine.
// - any outside references to inEvent will ben invalidated after
// calling this method.
size_t InputBuffer::Write(const INPUT_RECORD& inEvent)
{
return Write(std::span{ &inEvent, 1 });
}
// Routine Description:
// - Writes events to the input buffer. Wakes up any readers that are
// waiting for additional input events.
// Arguments:
// - inEvents - input events to store in the buffer.
// Return Value:
// - The number of events that were written to input buffer.
// Note:
// - The console lock must be held when calling this routine.
size_t InputBuffer::Write(const std::span<const INPUT_RECORD>& inEvents)
{
try
{
if (inEvents.empty())
{
return 0;
}
_vtInputShouldSuppress = true;
auto resetVtInputSuppress = wil::scope_exit([&]() { _vtInputShouldSuppress = false; });
// Write to buffer.
size_t EventsWritten;
bool SetWaitEvent;
_WriteBuffer(inEvents, EventsWritten, SetWaitEvent);
if (SetWaitEvent)
{
ServiceLocator::LocateGlobals().hInputEvent.SetEvent();
}
// Alert any writers waiting for space.
WakeUpReadersWaitingForData();
return EventsWritten;
}
catch (...)
{
LOG_HR(wil::ResultFromCaughtException());
return 0;
}
}
void InputBuffer::WriteString(const std::wstring_view& text)
try
{
if (text.empty())
@@ -516,20 +610,15 @@ try
return;
}
const auto initiallyEmpty = _storage.empty();
const auto initiallyEmptyQueue = _storage.empty();
if (initiallyEmpty || _storage.back().index() != 1)
{
_storage.emplace_back(TextVec{});
}
_writeString(text);
auto& v = *std::get_if<TextVec>(&_storage.back());
v.insert(v.end(), text.begin(), text.end());
if (initiallyEmpty)
if (initiallyEmptyQueue && !_storage.empty())
{
ServiceLocator::LocateGlobals().hInputEvent.SetEvent();
}
WakeUpReadersWaitingForData();
}
CATCH_LOG()
@@ -539,12 +628,55 @@ CATCH_LOG()
// input buffer and the next application will suddenly get a "\x1b[I" sequence in their input. See GH#13238.
void InputBuffer::WriteFocusEvent(bool focused) noexcept
{
//Write(SynthesizeFocusEvent(focused));
if (IsInVirtualTerminalInputMode())
{
if (const auto out = _termInput.HandleFocus(focused))
{
_HandleTerminalInputCallback(*out);
}
}
else
{
// This is a mini-version of Write().
const auto wasEmpty = _storage.empty();
_storage.push_back(SynthesizeFocusEvent(focused));
if (wasEmpty)
{
ServiceLocator::LocateGlobals().hInputEvent.SetEvent();
}
WakeUpReadersWaitingForData();
}
}
// Returns true when mouse input started. You should then capture the mouse and produce further events.
bool InputBuffer::WriteMouseEvent(til::point position, const unsigned int button, const short keyState, const short wheelDelta)
{
if (IsInVirtualTerminalInputMode())
{
// This magic flag is "documented" at https://msdn.microsoft.com/en-us/library/windows/desktop/ms646301(v=vs.85).aspx
// "If the high-order bit is 1, the key is down; otherwise, it is up."
static constexpr short KeyPressed{ gsl::narrow_cast<short>(0x8000) };
const TerminalInput::MouseButtonState state{
WI_IsFlagSet(OneCoreSafeGetKeyState(VK_LBUTTON), KeyPressed),
WI_IsFlagSet(OneCoreSafeGetKeyState(VK_MBUTTON), KeyPressed),
WI_IsFlagSet(OneCoreSafeGetKeyState(VK_RBUTTON), KeyPressed)
};
// GH#6401: VT applications should be able to receive mouse events from outside the
// terminal buffer. This is likely to happen when the user drags the cursor offscreen.
// We shouldn't throw away perfectly good events when they're offscreen, so we just
// clamp them to be within the range [(0, 0), (W, H)].
const auto& gci = ServiceLocator::LocateGlobals().getConsoleInformation();
gci.GetActiveOutputBuffer().GetViewport().ToOrigin().Clamp(position);
if (const auto out = _termInput.HandleMouse(position, button, keyState, wheelDelta, state))
{
_HandleTerminalInputCallback(*out);
return true;
}
}
return false;
}
@@ -561,6 +693,135 @@ static bool IsPauseKey(const KEY_EVENT_RECORD& event)
return ctrlButNotAlt && event.wVirtualKeyCode == L'S';
}
// Routine Description:
// - Coalesces input events and transfers them to storage queue.
// Arguments:
// - inRecords - The events to store.
// - eventsWritten - The number of events written since this function
// was called.
// - setWaitEvent - on exit, true if buffer became non-empty.
// Return Value:
// - None
// Note:
// - The console lock must be held when calling this routine.
// - will throw on failure
void InputBuffer::_WriteBuffer(const std::span<const INPUT_RECORD>& inEvents, _Out_ size_t& eventsWritten, _Out_ bool& setWaitEvent)
{
auto& gci = ServiceLocator::LocateGlobals().getConsoleInformation();
eventsWritten = 0;
setWaitEvent = false;
const auto initiallyEmptyQueue = _storage.empty();
const auto initialInEventsSize = inEvents.size();
const auto vtInputMode = IsInVirtualTerminalInputMode();
for (const auto& inEvent : inEvents)
{
if (inEvent.EventType == KEY_EVENT && inEvent.Event.KeyEvent.bKeyDown)
{
// if output is suspended, any keyboard input releases it.
if (WI_IsFlagSet(gci.Flags, CONSOLE_SUSPENDED) && !IsSystemKey(inEvent.Event.KeyEvent.wVirtualKeyCode))
{
UnblockWriteConsole(CONSOLE_OUTPUT_SUSPENDED);
continue;
}
// intercept control-s
if (WI_IsFlagSet(InputMode, ENABLE_LINE_INPUT) && IsPauseKey(inEvent.Event.KeyEvent))
{
WI_SetFlag(gci.Flags, CONSOLE_SUSPENDED);
continue;
}
}
// If we're in vt mode, try and handle it with the vt input module.
// If it was handled, do nothing else for it.
// If there was one event passed in, try coalescing it with the previous event currently in the buffer.
// If it's not coalesced, append it to the buffer.
if (vtInputMode)
{
// GH#11682: TerminalInput::HandleKey can handle both KeyEvents and Focus events seamlessly
if (const auto out = _termInput.HandleKey(inEvent))
{
_HandleTerminalInputCallback(*out);
eventsWritten++;
continue;
}
}
// we only check for possible coalescing when storing one
// record at a time because this is the original behavior of
// the input buffer. Changing this behavior may break stuff
// that was depending on it.
if (initialInEventsSize == 1 && !_storage.empty() && _CoalesceEvent(inEvents[0]))
{
eventsWritten++;
return;
}
// At this point, the event was neither coalesced, nor processed by VT.
_storage.push_back(inEvent);
++eventsWritten;
}
if (initiallyEmptyQueue && !_storage.empty())
{
setWaitEvent = true;
}
}
// Routine Description::
// - If the last input event saved and the first input event in inRecords
// are both a keypress down event for the same key, update the repeat
// count of the saved event and drop the first from inRecords.
// Arguments:
// - inRecords - The incoming records to process.
// Return Value:
// true if events were coalesced, false if they were not.
// Note:
// - The size of inRecords must be 1.
// - Coalescing here means updating a record that already exists in
// the buffer with updated values from an incoming event, instead of
// storing the incoming event (which would make the original one
// redundant/out of date with the most current state).
bool InputBuffer::_CoalesceEvent(const INPUT_RECORD& inEvent) noexcept
{
auto& lastEvent = _storage.back();
if (lastEvent.EventType == MOUSE_EVENT && inEvent.EventType == MOUSE_EVENT)
{
const auto& inMouse = inEvent.Event.MouseEvent;
auto& lastMouse = lastEvent.Event.MouseEvent;
if (lastMouse.dwEventFlags == MOUSE_MOVED && inMouse.dwEventFlags == MOUSE_MOVED)
{
lastMouse.dwMousePosition = inMouse.dwMousePosition;
return true;
}
}
else if (lastEvent.EventType == KEY_EVENT && inEvent.EventType == KEY_EVENT)
{
const auto& inKey = inEvent.Event.KeyEvent;
auto& lastKey = lastEvent.Event.KeyEvent;
if (lastKey.bKeyDown && inKey.bKeyDown &&
(lastKey.wVirtualScanCode == inKey.wVirtualScanCode || WI_IsFlagSet(inKey.dwControlKeyState, NLS_IME_CONVERSION)) &&
lastKey.uChar.UnicodeChar == inKey.uChar.UnicodeChar &&
lastKey.dwControlKeyState == inKey.dwControlKeyState &&
// TODO:GH#8000 This behavior is an import from old conhost v1 and has been broken for decades.
// This is probably the outdated idea that any wide glyph is being represented by 2 characters (DBCS) and likely
// resulted from conhost originally being split into a ASCII/OEM and a DBCS variant with preprocessor flags.
// You can't update the repeat count of such a A,B pair, because they're stored as A,A,B,B (down-down, up-up).
// I believe the proper approach is to store pairs of characters as pairs, update their combined
// repeat count and only when they're being read de-coalesce them into their alternating form.
!IsGlyphFullWidth(inKey.uChar.UnicodeChar))
{
lastKey.wRepeatCount += inKey.wRepeatCount;
return true;
}
}
return false;
}
// Routine Description:
// - Returns true if this input buffer is in VT Input mode.
// Arguments:
@@ -572,6 +833,55 @@ bool InputBuffer::IsInVirtualTerminalInputMode() const
return WI_IsFlagSet(InputMode, ENABLE_VIRTUAL_TERMINAL_INPUT);
}
// Routine Description:
// - Handler for inserting key sequences into the buffer when the terminal emulation layer
// has determined a key can be converted appropriately into a sequence of inputs
// Arguments:
// - inEvents - Series of input records to insert into the buffer
// Return Value:
// - <none>
void InputBuffer::_HandleTerminalInputCallback(const TerminalInput::StringType& text)
{
try
{
if (text.empty())
{
return;
}
_writeString(text);
if (!_vtInputShouldSuppress)
{
ServiceLocator::LocateGlobals().hInputEvent.SetEvent();
WakeUpReadersWaitingForData();
}
}
catch (...)
{
LOG_HR(wil::ResultFromCaughtException());
}
}
void InputBuffer::_writeString(const std::wstring_view& text)
{
for (const auto& wch : text)
{
if (wch == UNICODE_NULL)
{
// Convert null byte back to input event with proper control state
const auto zeroKey = OneCoreSafeVkKeyScanW(0);
uint32_t ctrlState = 0;
WI_SetFlagIf(ctrlState, SHIFT_PRESSED, WI_IsFlagSet(zeroKey, 0x100));
WI_SetFlagIf(ctrlState, LEFT_CTRL_PRESSED, WI_IsFlagSet(zeroKey, 0x200));
WI_SetFlagIf(ctrlState, LEFT_ALT_PRESSED, WI_IsFlagSet(zeroKey, 0x400));
_storage.push_back(SynthesizeKeyEvent(true, 1, LOBYTE(zeroKey), 0, wch, ctrlState));
continue;
}
_storage.push_back(SynthesizeKeyEvent(true, 1, 0, 0, wch, 0));
}
}
TerminalInput& InputBuffer::GetTerminalInput()
{
return _termInput;

View File

@@ -3,11 +3,15 @@
#pragma once
#include "readData.hpp"
#include "../types/inc/IInputEvent.hpp"
#include "../server/ObjectHandle.h"
#include "../server/ObjectHeader.h"
#include "../terminal/input/terminalInput.hpp"
#include <deque>
namespace Microsoft::Console::Render
{
class Renderer;
@@ -17,6 +21,7 @@ namespace Microsoft::Console::Render
class InputBuffer final : public ConsoleObjectHeader
{
public:
DWORD InputMode;
ConsoleWaitQueue WaitQueue; // formerly ReadWaitQueue
bool fInComposition; // specifies if there's an ongoing text composition
@@ -36,36 +41,29 @@ public:
const INPUT_RECORD& FetchWritePartialByteSequence() noexcept;
void StoreWritePartialByteSequence(const INPUT_RECORD& event) noexcept;
void ReinitializeInputBuffer();
void WakeUpReadersWaitingForData();
void TerminateRead(_In_ WaitTerminationReason Flag);
size_t GetNumberOfReadyEvents() const noexcept;
void Flush();
void FlushAllButKeys();
struct ReadDescriptor
{
bool wide;
bool records;
bool peek;
};
size_t Read(ReadDescriptor desc, void* data, size_t capacityInBytes);
[[nodiscard]] NTSTATUS Read(_Out_ InputEventQueue& OutEvents,
const size_t AmountToRead,
const bool Peek,
const bool WaitForData,
const bool Unicode,
const bool Stream);
void Write(const INPUT_RECORD& record);
void Write(const std::span<const INPUT_RECORD>& records);
void Write(const std::wstring_view& text);
size_t Prepend(const std::span<const INPUT_RECORD>& inEvents);
size_t Write(const INPUT_RECORD& inEvent);
size_t Write(const std::span<const INPUT_RECORD>& inEvents);
void WriteString(const std::wstring_view& text);
void WriteFocusEvent(bool focused) noexcept;
bool WriteMouseEvent(til::point position, unsigned int button, short keyState, short wheelDelta);
bool IsInVirtualTerminalInputMode() const;
Microsoft::Console::VirtualTerminal::TerminalInput& GetTerminalInput();
// 1 INPUT_RECORD = 20 bytes = 10 wchar_t
// On 64-Bit architectures this results in std::list nodes of 1008 bytes (heap alloc headers are 16 bytes).
// Optimally this should use a single ring buffer and not a bunch of glued together container classes.
using RecordVec = til::small_vector<INPUT_RECORD, 48>;
using TextVec = til::small_vector<wchar_t, 480>;
using VecVariant = std::variant<RecordVec, TextVec>;
std::list<VecVariant> _storage;
private:
enum class ReadingMode : uint8_t
@@ -83,14 +81,23 @@ private:
std::deque<INPUT_RECORD> _cachedInputEvents;
ReadingMode _readingMode = ReadingMode::StringA;
std::deque<INPUT_RECORD> _storage;
INPUT_RECORD _writePartialByteSequence{};
bool _writePartialByteSequenceAvailable = false;
Microsoft::Console::VirtualTerminal::TerminalInput _termInput;
// This flag is used in _HandleTerminalInputCallback
// If the InputBuffer leads to a _HandleTerminalInputCallback call,
// we should suppress the wakeup functions.
// Otherwise, we should be calling them.
bool _vtInputShouldSuppress{ false };
void _switchReadingMode(ReadingMode mode);
void _switchReadingModeSlowPath(ReadingMode mode);
void _WriteBuffer(const std::span<const INPUT_RECORD>& inRecords, _Out_ size_t& eventsWritten, _Out_ bool& setWaitEvent);
bool _CoalesceEvent(const INPUT_RECORD& inEvent) noexcept;
void _HandleTerminalInputCallback(const Microsoft::Console::VirtualTerminal::TerminalInput::StringType& text);
void _writeString(const std::wstring_view& text);
#ifdef UNIT_TESTING
friend class InputBufferTests;

View File

@@ -23,8 +23,6 @@ Revision History:
class INPUT_READ_HANDLE_DATA
{
public:
DWORD InputMode = ENABLE_LINE_INPUT | ENABLE_PROCESSED_INPUT | ENABLE_ECHO_INPUT | ENABLE_MOUSE_INPUT;
INPUT_READ_HANDLE_DATA();
~INPUT_READ_HANDLE_DATA() = default;

Some files were not shown because too many files have changed in this diff Show More