mirror of
https://github.com/microsoft/terminal.git
synced 2026-04-13 17:51:00 +00:00
Compare commits
68 Commits
dev/lhecke
...
dev/migrie
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
70d905b658 | ||
|
|
9a6ded2e39 | ||
|
|
eac27dbe25 | ||
|
|
d3a98b3754 | ||
|
|
d234049640 | ||
|
|
4ab628d62f | ||
|
|
8309901fc9 | ||
|
|
015c5e8b93 | ||
|
|
71efdcb21b | ||
|
|
ec91be5995 | ||
|
|
5dda50767b | ||
|
|
b70fd5e9c6 | ||
|
|
ef96e225da | ||
|
|
c669afe2a0 | ||
|
|
ce30e7c89c | ||
|
|
bcca7aac1b | ||
|
|
a2bb3136bb | ||
|
|
dccc1f4240 | ||
|
|
86c30bdaa2 | ||
|
|
5f71cf3e94 | ||
|
|
a3ac337d88 | ||
|
|
5d2fa4782f | ||
|
|
63bfdb2e1e | ||
|
|
5575187b26 | ||
|
|
48a6d92255 | ||
|
|
e727aaf679 | ||
|
|
98146c9d1b | ||
|
|
e6ac014fc8 | ||
|
|
0d47c862c2 | ||
|
|
a39ac598cd | ||
|
|
b08dc61a9c | ||
|
|
ba6f1e905d | ||
|
|
bc452c61dc | ||
|
|
204794f9f3 | ||
|
|
4902b342ef | ||
|
|
03aa8a6231 | ||
|
|
e75a4be4fe | ||
|
|
da99d892f4 | ||
|
|
f5898886be | ||
|
|
fe65d9ac8f | ||
|
|
6c192d15be | ||
|
|
da182e6c59 | ||
|
|
521a300c17 | ||
|
|
10fb5448cc | ||
|
|
a24afcd1e6 | ||
|
|
92f9ff948b | ||
|
|
1726176c85 | ||
|
|
3b02c96bd5 | ||
|
|
dc448b4781 | ||
|
|
75ea5f3aab | ||
|
|
6ac5137ba8 | ||
|
|
7b524b0d31 | ||
|
|
654416cdc1 | ||
|
|
cae6f04cfb | ||
|
|
9208222884 | ||
|
|
8b67ed7779 | ||
|
|
c4f623aaf0 | ||
|
|
3d83cc348a | ||
|
|
8e170eb643 | ||
|
|
0f339d2498 | ||
|
|
054f173995 | ||
|
|
1fd87fecdf | ||
|
|
e9b2e5184a | ||
|
|
f0f75dcdd0 | ||
|
|
006da6a549 | ||
|
|
67d854821f | ||
|
|
65bc163da4 | ||
|
|
ce375fa7f3 |
1
.github/actions/spelling/allow/allow.txt
vendored
1
.github/actions/spelling/allow/allow.txt
vendored
@@ -117,6 +117,7 @@ uiatextrange
|
||||
UIs
|
||||
und
|
||||
unregister
|
||||
urxvt
|
||||
versioned
|
||||
vsdevcmd
|
||||
walkthrough
|
||||
|
||||
12
.github/actions/spelling/expect/expect.txt
vendored
12
.github/actions/spelling/expect/expect.txt
vendored
@@ -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
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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" />
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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 += "<";
|
||||
break;
|
||||
case '>':
|
||||
htmlBuilder += ">";
|
||||
break;
|
||||
case '&':
|
||||
htmlBuilder += "&";
|
||||
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 << "<";
|
||||
break;
|
||||
case '>':
|
||||
htmlBuilder << ">";
|
||||
break;
|
||||
case '&':
|
||||
htmlBuilder << "&";
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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" ");
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -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{};
|
||||
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
#include "pch.h"
|
||||
#include "ColorHelper.h"
|
||||
#include <limits>
|
||||
|
||||
using namespace winrt::TerminalApp;
|
||||
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
#pragma once
|
||||
#include "pch.h"
|
||||
|
||||
#include <winrt/windows.ui.core.h>
|
||||
#include <winrt/Windows.UI.h>
|
||||
|
||||
namespace winrt::TerminalApp
|
||||
{
|
||||
|
||||
@@ -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() })
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -34,5 +34,5 @@ public:
|
||||
|
||||
private:
|
||||
winrt::Microsoft::UI::Xaml::Controls::TeachingTip _tip;
|
||||
winrt::Windows::UI::Xaml::DispatcherTimer _timer;
|
||||
SafeDispatcherTimer _timer;
|
||||
};
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
@@ -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));
|
||||
|
||||
@@ -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 };
|
||||
|
||||
@@ -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; };
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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; };
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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" />
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -190,5 +190,7 @@ namespace Microsoft.Terminal.Control
|
||||
|
||||
event Windows.Foundation.TypedEventHandler<Object, CompletionsChangedEventArgs> CompletionsChanged;
|
||||
|
||||
event Windows.Foundation.TypedEventHandler<Object, SendNotificationArgs> SendNotification;
|
||||
|
||||
};
|
||||
}
|
||||
|
||||
@@ -20,3 +20,4 @@
|
||||
#include "KeySentEventArgs.g.cpp"
|
||||
#include "CharSentEventArgs.g.cpp"
|
||||
#include "StringSentEventArgs.g.cpp"
|
||||
#include "SendNotificationArgs.g.cpp"
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -121,4 +121,10 @@ namespace Microsoft.Terminal.Control
|
||||
{
|
||||
String Text { get; };
|
||||
}
|
||||
|
||||
runtimeclass SendNotificationArgs
|
||||
{
|
||||
String Title { get; };
|
||||
String Body { get; };
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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; };
|
||||
|
||||
@@ -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; };
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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 };
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -28,6 +28,7 @@ namespace Microsoft.Terminal.Core
|
||||
Windows.Foundation.IReference<Microsoft.Terminal.Core.Color> StartingTabColor;
|
||||
|
||||
Boolean AutoMarkPrompts;
|
||||
Boolean AllowNotifications;
|
||||
|
||||
};
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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}"
|
||||
|
||||
@@ -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" />
|
||||
|
||||
@@ -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 -->
|
||||
<!--
|
||||
|
||||
@@ -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); \
|
||||
} \
|
||||
\
|
||||
|
||||
@@ -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; \
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -410,7 +410,7 @@ bool SettingsLoader::FixupUserSettings()
|
||||
{
|
||||
struct CommandlinePatch
|
||||
{
|
||||
winrt::guid guid;
|
||||
winrt::guid guid{};
|
||||
std::wstring_view before;
|
||||
std::wstring_view after;
|
||||
};
|
||||
|
||||
@@ -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>() };
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
|
||||
92
src/cascadia/WinRTUtils/inc/SafeDispatcherTimer.h
Normal file
92
src/cascadia/WinRTUtils/inc/SafeDispatcherTimer.h
Normal 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;
|
||||
};
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
{
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user