Skip to content

Commit

Permalink
Implement buffer restore (#16598)
Browse files Browse the repository at this point in the history
This changeset allows Windows Terminal to dump its buffer contents as
UTF-16LE VT text onto disk and restore it later. This functionality is
enabled whenever `persistedWindowLayout` is being used.

Closes #961
Closes #16741

## Validation Steps Performed
* Open multiple windows with multiple tabs and restart the app
  Everything's restored ✅
* Reopen a tab with output from `RenderingTests.exe`
  Everything's restored ✅
* Closing tabs and windows with Ctrl+W deletes their buffer dumps ✅
* Closing tabs doesn't create buffer dumps ✅
  • Loading branch information
lhecker authored Mar 29, 2024
1 parent 1ede023 commit c4c5206
Show file tree
Hide file tree
Showing 72 changed files with 670 additions and 688 deletions.
2 changes: 2 additions & 0 deletions .github/actions/spelling/expect/expect.txt
Original file line number Diff line number Diff line change
Expand Up @@ -597,6 +597,7 @@ FECF
FEEF
fesb
FFAF
ffd
FFDE
FFFDb
fgbg
Expand Down Expand Up @@ -757,6 +758,7 @@ hdr
HDROP
hdrstop
HEIGHTSCROLL
hfind
hfont
hfontresource
hglobal
Expand Down
17 changes: 5 additions & 12 deletions src/buffer/out/TextColor.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,11 @@ bool TextColor::CanBeBrightened() const noexcept
return IsIndex16() || IsDefault();
}

ColorType TextColor::GetType() const noexcept
{
return _meta;
}

bool TextColor::IsLegacy() const noexcept
{
return (IsIndex16() || IsIndex256()) && _index < 16;
Expand Down Expand Up @@ -280,15 +285,3 @@ BYTE TextColor::GetLegacyIndex(const BYTE defaultIndex) const noexcept
return til::at(CompressedRgbToIndex16, compressedRgb);
}
}

// Method Description:
// - Return a COLORREF containing our stored value. Will return garbage if this
//attribute is not a RGB attribute.
// Arguments:
// - <none>
// Return Value:
// - a COLORREF containing our stored value
COLORREF TextColor::GetRGB() const noexcept
{
return RGB(_red, _green, _blue);
}
12 changes: 6 additions & 6 deletions src/buffer/out/TextColor.h
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,7 @@ struct TextColor
}

bool CanBeBrightened() const noexcept;
ColorType GetType() const noexcept;
bool IsLegacy() const noexcept;
bool IsIndex16() const noexcept;
bool IsIndex256() const noexcept;
Expand All @@ -133,12 +134,11 @@ struct TextColor
COLORREF GetColor(const std::array<COLORREF, TABLE_SIZE>& colorTable, const size_t defaultIndex, bool brighten = false) const noexcept;
BYTE GetLegacyIndex(const BYTE defaultIndex) const noexcept;

constexpr BYTE GetIndex() const noexcept
{
return _index;
}

COLORREF GetRGB() const noexcept;
constexpr BYTE GetIndex() const noexcept { return _index; }
constexpr BYTE GetR() const noexcept { return _red; }
constexpr BYTE GetG() const noexcept { return _green; }
constexpr BYTE GetB() const noexcept { return _blue; }
constexpr COLORREF GetRGB() const noexcept { return RGB(_red, _green, _blue); }

static constexpr BYTE TransposeLegacyIndex(const size_t index)
{
Expand Down
232 changes: 231 additions & 1 deletion src/buffer/out/textBuffer.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@
#include "UTextAdapter.h"
#include "../../types/inc/GlyphWidth.hpp"
#include "../renderer/base/renderer.hpp"
#include "../types/inc/convert.hpp"
#include "../types/inc/utils.hpp"

using namespace Microsoft::Console;
Expand Down Expand Up @@ -2508,6 +2507,237 @@ void TextBuffer::_AppendRTFText(std::string& contentBuilder, const std::wstring_
}
}

void TextBuffer::Serialize(const wchar_t* destination) const
{
const wil::unique_handle file{ CreateFileW(destination, GENERIC_WRITE, FILE_SHARE_READ | FILE_SHARE_DELETE, nullptr, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, nullptr) };
THROW_LAST_ERROR_IF(!file);

static constexpr size_t writeThreshold = 32 * 1024;
std::wstring buffer;
buffer.reserve(writeThreshold + writeThreshold / 2);
buffer.push_back(L'\uFEFF');

const til::CoordType lastRowWithText = GetLastNonSpaceCharacter(nullptr).y;
CharacterAttributes previousAttr = CharacterAttributes::Unused1;
TextColor previousFg;
TextColor previousBg;
TextColor previousUl;
uint16_t previousHyperlinkId = 0;

// This iterates through each row. The exit condition is at the end
// of the for() loop so that we can properly handle file flushing.
for (til::CoordType currentRow = 0;; currentRow++)
{
const auto& row = GetRowByOffset(currentRow);

if (const auto lr = row.GetLineRendition(); lr != LineRendition::SingleWidth)
{
static constexpr std::wstring_view mappings[] = {
L"\x1b#6", // LineRendition::DoubleWidth
L"\x1b#3", // LineRendition::DoubleHeightTop
L"\x1b#4", // LineRendition::DoubleHeightBottom
};
const auto idx = std::clamp(static_cast<int>(lr) - 1, 0, 2);
buffer.append(til::at(mappings, idx));
}

const auto& runs = row.Attributes().runs();
auto it = runs.begin();
const auto end = runs.end();
const auto last = end - 1;
til::CoordType oldX = 0;

for (; it != end; ++it)
{
const auto attr = it->value.GetCharacterAttributes();
const auto hyperlinkId = it->value.GetHyperlinkId();
const auto fg = it->value.GetForeground();
const auto bg = it->value.GetBackground();
const auto ul = it->value.GetUnderlineColor();

if (previousAttr != attr)
{
const auto attrDelta = attr ^ previousAttr;

{
struct Mapping
{
CharacterAttributes attr;
uint8_t change[2]; // [0] = off, [1] = on
};
static constexpr Mapping mappings[] = {
{ CharacterAttributes::Intense, { 22, 1 } },
{ CharacterAttributes::Italics, { 23, 3 } },
{ CharacterAttributes::Blinking, { 25, 5 } },
{ CharacterAttributes::Invisible, { 28, 8 } },
{ CharacterAttributes::CrossedOut, { 29, 9 } },
{ CharacterAttributes::Faint, { 22, 2 } },
{ CharacterAttributes::TopGridline, { 55, 53 } },
{ CharacterAttributes::ReverseVideo, { 27, 7 } },
{ CharacterAttributes::BottomGridline, { 24, 4 } },
};
for (const auto& mapping : mappings)
{
if (WI_IsAnyFlagSet(attrDelta, mapping.attr))
{
const auto n = til::at(mapping.change, WI_IsAnyFlagSet(attr, mapping.attr));
fmt::format_to(std::back_inserter(buffer), FMT_COMPILE(L"\x1b[{}m"), n);
}
}
}

if (WI_IsAnyFlagSet(attrDelta, CharacterAttributes::UnderlineStyle))
{
static constexpr std::wstring_view mappings[] = {
L"\x1b[24m", // UnderlineStyle::NoUnderline
L"\x1b[4m", // UnderlineStyle::SinglyUnderlined
L"\x1b[21m", // UnderlineStyle::DoublyUnderlined
L"\x1b[4:3m", // UnderlineStyle::CurlyUnderlined
L"\x1b[4:4m", // UnderlineStyle::DottedUnderlined
L"\x1b[4:5m", // UnderlineStyle::DashedUnderlined
};

auto idx = WI_EnumValue(it->value.GetUnderlineStyle());
if (idx >= std::size(mappings))
{
idx = 1; // UnderlineStyle::SinglyUnderlined
}

buffer.append(til::at(mappings, idx));
}

previousAttr = attr;
}

if (previousFg != fg)
{
switch (fg.GetType())
{
case ColorType::IsDefault:
buffer.append(L"\x1b[39m");
break;
case ColorType::IsIndex16:
{
uint8_t index = WI_IsFlagSet(fg.GetIndex(), 8) ? 90 : 30;
index += fg.GetIndex() & 7;
fmt::format_to(std::back_inserter(buffer), FMT_COMPILE(L"\x1b[{}m"), index);
break;
}
case ColorType::IsIndex256:
fmt::format_to(std::back_inserter(buffer), FMT_COMPILE(L"\x1b[38;5;{}m"), fg.GetIndex());
break;
case ColorType::IsRgb:
fmt::format_to(std::back_inserter(buffer), FMT_COMPILE(L"\x1b[38;2;{};{};{}m"), fg.GetR(), fg.GetG(), fg.GetB());
break;
default:
break;
}
previousFg = fg;
}

if (previousBg != bg)
{
switch (bg.GetType())
{
case ColorType::IsDefault:
buffer.append(L"\x1b[49m");
break;
case ColorType::IsIndex16:
{
uint8_t index = WI_IsFlagSet(bg.GetIndex(), 8) ? 100 : 40;
index += bg.GetIndex() & 7;
fmt::format_to(std::back_inserter(buffer), FMT_COMPILE(L"\x1b[{}m"), index);
break;
}
case ColorType::IsIndex256:
fmt::format_to(std::back_inserter(buffer), FMT_COMPILE(L"\x1b[48;5;{}m"), bg.GetIndex());
break;
case ColorType::IsRgb:
fmt::format_to(std::back_inserter(buffer), FMT_COMPILE(L"\x1b[48;2;{};{};{}m"), bg.GetR(), bg.GetG(), bg.GetB());
break;
default:
break;
}
previousBg = bg;
}

if (previousUl != ul)
{
switch (fg.GetType())
{
case ColorType::IsDefault:
buffer.append(L"\x1b[59m");
break;
case ColorType::IsIndex256:
fmt::format_to(std::back_inserter(buffer), FMT_COMPILE(L"\x1b[58:5:{}m"), ul.GetIndex());
break;
case ColorType::IsRgb:
fmt::format_to(std::back_inserter(buffer), FMT_COMPILE(L"\x1b[58:2::{}:{}:{}m"), ul.GetR(), ul.GetG(), ul.GetB());
break;
default:
break;
}
previousUl = ul;
}

if (previousHyperlinkId != hyperlinkId)
{
if (hyperlinkId)
{
const auto uri = GetHyperlinkUriFromId(hyperlinkId);
if (!uri.empty())
{
buffer.append(L"\x1b]8;;");
buffer.append(uri);
buffer.append(L"\x1b\\");
previousHyperlinkId = hyperlinkId;
}
}
else
{
buffer.append(L"\x1b]8;;\x1b\\");
previousHyperlinkId = 0;
}
}

auto newX = oldX + it->length;
// Trim whitespace with default attributes from the end of each line.
if (it == last && it->value == TextAttribute{})
{
// This can result in oldX > newX, but that's okay because GetText()
// is robust against that and returns an empty string.
newX = row.MeasureRight();
}

buffer.append(row.GetText(oldX, newX));
oldX = newX;
}

const auto moreRowsRemaining = currentRow < lastRowWithText;

if (!row.WasWrapForced() || !moreRowsRemaining)
{
buffer.append(L"\r\n");
}

if (buffer.size() >= writeThreshold || !moreRowsRemaining)
{
const auto fileSize = gsl::narrow<DWORD>(buffer.size() * sizeof(wchar_t));
DWORD bytesWritten = 0;
THROW_IF_WIN32_BOOL_FALSE(WriteFile(file.get(), buffer.data(), fileSize, &bytesWritten, nullptr));
if (bytesWritten != fileSize)
{
THROW_WIN32_MSG(ERROR_WRITE_FAULT, "failed to write");
}
}

if (!moreRowsRemaining)
{
break;
}
}
}

// Function Description:
// - Reflow the contents from the old buffer into the new buffer. The new buffer
// can have different dimensions than the old buffer. If it does, then this
Expand Down
2 changes: 2 additions & 0 deletions src/buffer/out/textBuffer.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -319,6 +319,8 @@ class TextBuffer final
const bool isIntenseBold,
std::function<std::tuple<COLORREF, COLORREF, COLORREF>(const TextAttribute&)> GetAttributeColors) const noexcept;

void Serialize(const wchar_t* destination) const;

struct PositionInformation
{
til::CoordType mutableViewportTop{ 0 };
Expand Down
5 changes: 0 additions & 5 deletions src/cascadia/Remoting/GetWindowLayoutArgs.cpp

This file was deleted.

31 changes: 0 additions & 31 deletions src/cascadia/Remoting/GetWindowLayoutArgs.h

This file was deleted.

12 changes: 0 additions & 12 deletions src/cascadia/Remoting/Microsoft.Terminal.RemotingLib.vcxproj
Original file line number Diff line number Diff line change
Expand Up @@ -37,12 +37,6 @@
<ClInclude Include="WindowActivatedArgs.h">
<DependentUpon>Peasant.idl</DependentUpon>
</ClInclude>
<ClInclude Include="GetWindowLayoutArgs.h">
<DependentUpon>Peasant.idl</DependentUpon>
</ClInclude>
<ClInclude Include="QuitAllRequestedArgs.h">
<DependentUpon>Monarch.idl</DependentUpon>
</ClInclude>
<ClInclude Include="pch.h" />
<ClInclude Include="MonarchFactory.h" />
<ClInclude Include="Peasant.h">
Expand Down Expand Up @@ -78,12 +72,6 @@
<ClCompile Include="WindowActivatedArgs.cpp">
<DependentUpon>Peasant.idl</DependentUpon>
</ClCompile>
<ClCompile Include="GetWindowLayoutArgs.cpp">
<DependentUpon>Peasant.idl</DependentUpon>
</ClCompile>
<ClCompile Include="QuitAllRequestedArgs.cpp">
<DependentUpon>Monarch.idl</DependentUpon>
</ClCompile>
<ClCompile Include="pch.cpp">
<PrecompiledHeader>Create</PrecompiledHeader>
</ClCompile>
Expand Down
Loading

0 comments on commit c4c5206

Please sign in to comment.