terminal/src/inc/til/size.h
Mike Griese 7612044363
Implement a pair of shims for cls, Clear-Host in conpty mode (#5627)
## Summary of the Pull Request

This PR implements a pair of shims for `cmd` and `powershell`, so that their `cls` and `Clear-Host` functions will clear the entire terminal buffer (like they do in conhost), instead of just the viewport. With the conpty viewport and buffer being the same size, there's effectively no way to know if an application is calling these API's in this way with the intention of clearing the buffer or the viewport. We absolutely have to guess. 

Each of these shims checks to see if the way that the API is being called exactly matches the way `cmd` or `powershell` would call these APIs. If it does, we manually write a `^[[3J` to the connected terminal, to get he Terminal to clear it's own scrollback.

~~_⚠️ If another application were trying to clear the **viewport** with an exactly similar API call, this would also cause the terminal scrollback to get cleared ⚠️_~~

* [x] Should these shims be restricted to when the process that's calling them is actually `cmd.exe` or `powershell.exe`? Can I even do this? I think we've done such a good job of isolating the client process information from the rest of the host code that I can't figure out how to do this.
  - YES, this can be done, and I did it.
* [ ] **TODO**: _While I'm here_, should I have `DoSrvPrivateEraseAll` (the implementation for `^[[2J`, in `getset.cpp`) also manually trigger a EraseAll in the terminal in conpty mode?

## PR Checklist
* [x] Closes #3126
* [x] Actually closes #1305 too, which is really the same thing, but probably deserves a callout
* [x] I work here
* [x] Tests added/passed
* [n/a] Requires documentation to be updated

## Validation Steps Performed
* ran tests
* checked `cls` in the Terminal
* checked `Clear-Host` in the Terminal
* Checked running `powershell clear-host` from `cmd.exe`
2020-04-30 21:53:31 +00:00

341 lines
12 KiB
C++

// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.
#pragma once
#ifdef UNIT_TESTING
class SizeTests;
#endif
namespace til // Terminal Implementation Library. Also: "Today I Learned"
{
class size
{
public:
constexpr size() noexcept :
size(0, 0)
{
}
// On 64-bit processors, int and ptrdiff_t are different fundamental types.
// On 32-bit processors, they're the same which makes this a double-definition
// with the `ptrdiff_t` one below.
#if defined(_M_AMD64) || defined(_M_ARM64)
constexpr size(int width, int height) noexcept :
size(static_cast<ptrdiff_t>(width), static_cast<ptrdiff_t>(height))
{
}
constexpr size(ptrdiff_t width, int height) noexcept :
size(width, static_cast<ptrdiff_t>(height))
{
}
constexpr size(int width, ptrdiff_t height) noexcept :
size(static_cast<ptrdiff_t>(width), height)
{
}
#endif
size(size_t width, size_t height)
{
THROW_HR_IF(E_ABORT, !base::MakeCheckedNum(width).AssignIfValid(&_width));
THROW_HR_IF(E_ABORT, !base::MakeCheckedNum(height).AssignIfValid(&_height));
}
constexpr size(ptrdiff_t width, ptrdiff_t height) noexcept :
_width(width),
_height(height)
{
}
// This template will convert to size from anything that has an X and a Y field that appear convertible to an integer value
template<typename TOther>
constexpr size(const TOther& other, std::enable_if_t<std::is_integral_v<decltype(std::declval<TOther>().X)> && std::is_integral_v<decltype(std::declval<TOther>().Y)>, int> /*sentinel*/ = 0) :
size(static_cast<ptrdiff_t>(other.X), static_cast<ptrdiff_t>(other.Y))
{
}
// This template will convert to size from anything that has a cx and a cy field that appear convertible to an integer value
template<typename TOther>
constexpr size(const TOther& other, std::enable_if_t<std::is_integral_v<decltype(std::declval<TOther>().cx)> && std::is_integral_v<decltype(std::declval<TOther>().cy)>, int> /*sentinel*/ = 0) :
size(static_cast<ptrdiff_t>(other.cx), static_cast<ptrdiff_t>(other.cy))
{
}
// This template will convert to size from anything that has a X and a Y field that are floating-point;
// a math type is required. If you _don't_ provide one, you're going to
// get a compile-time error about "cannot convert from initializer-list to til::size"
template<typename TilMath, typename TOther>
constexpr size(TilMath, const TOther& other, std::enable_if_t<std::is_floating_point_v<decltype(std::declval<TOther>().X)> && std::is_floating_point_v<decltype(std::declval<TOther>().Y)>, int> /*sentinel*/ = 0) :
size(TilMath::template cast<ptrdiff_t>(other.X), TilMath::template cast<ptrdiff_t>(other.Y))
{
}
// This template will convert to size from anything that has a cx and a cy field that are floating-point;
// a math type is required. If you _don't_ provide one, you're going to
// get a compile-time error about "cannot convert from initializer-list to til::size"
template<typename TilMath, typename TOther>
constexpr size(TilMath, const TOther& other, std::enable_if_t<std::is_floating_point_v<decltype(std::declval<TOther>().cx)> && std::is_floating_point_v<decltype(std::declval<TOther>().cy)>, int> /*sentinel*/ = 0) :
size(TilMath::template cast<ptrdiff_t>(other.cx), TilMath::template cast<ptrdiff_t>(other.cy))
{
}
// This template will convert to size from anything that has a Width and a Height field that are floating-point;
// a math type is required. If you _don't_ provide one, you're going to
// get a compile-time error about "cannot convert from initializer-list to til::size"
template<typename TilMath, typename TOther>
constexpr size(TilMath, const TOther& other, std::enable_if_t<std::is_floating_point_v<decltype(std::declval<TOther>().Width)> && std::is_floating_point_v<decltype(std::declval<TOther>().Height)>, int> /*sentinel*/ = 0) :
size(TilMath::template cast<ptrdiff_t>(other.Width), TilMath::template cast<ptrdiff_t>(other.Height))
{
}
// This template will convert to size from floating-point args;
// a math type is required. If you _don't_ provide one, you're going to
// get a compile-time error about "cannot convert from initializer-list to til::size"
template<typename TilMath, typename TOther>
constexpr size(TilMath, const TOther& width, const TOther& height, std::enable_if_t<std::is_floating_point_v<TOther>, int> /*sentinel*/ = 0) :
size(TilMath::template cast<ptrdiff_t>(width), TilMath::template cast<ptrdiff_t>(height))
{
}
constexpr bool operator==(const size& other) const noexcept
{
return _width == other._width &&
_height == other._height;
}
constexpr bool operator!=(const size& other) const noexcept
{
return !(*this == other);
}
constexpr explicit operator bool() const noexcept
{
return _width > 0 && _height > 0;
}
size operator+(const size& other) const
{
ptrdiff_t width;
THROW_HR_IF(E_ABORT, !base::CheckAdd(_width, other._width).AssignIfValid(&width));
ptrdiff_t height;
THROW_HR_IF(E_ABORT, !base::CheckAdd(_height, other._height).AssignIfValid(&height));
return size{ width, height };
}
size operator-(const size& other) const
{
ptrdiff_t width;
THROW_HR_IF(E_ABORT, !base::CheckSub(_width, other._width).AssignIfValid(&width));
ptrdiff_t height;
THROW_HR_IF(E_ABORT, !base::CheckSub(_height, other._height).AssignIfValid(&height));
return size{ width, height };
}
size operator*(const size& other) const
{
ptrdiff_t width;
THROW_HR_IF(E_ABORT, !base::CheckMul(_width, other._width).AssignIfValid(&width));
ptrdiff_t height;
THROW_HR_IF(E_ABORT, !base::CheckMul(_height, other._height).AssignIfValid(&height));
return size{ width, height };
}
template<typename TilMath>
size scale(TilMath, const float scale) const
{
struct
{
float Width, Height;
} sz;
THROW_HR_IF(E_ABORT, !base::CheckMul(scale, _width).AssignIfValid(&sz.Width));
THROW_HR_IF(E_ABORT, !base::CheckMul(scale, _height).AssignIfValid(&sz.Height));
return til::size(TilMath(), sz);
}
size operator/(const size& other) const
{
ptrdiff_t width;
THROW_HR_IF(E_ABORT, !base::CheckDiv(_width, other._width).AssignIfValid(&width));
ptrdiff_t height;
THROW_HR_IF(E_ABORT, !base::CheckDiv(_height, other._height).AssignIfValid(&height));
return size{ width, height };
}
size divide_ceil(const size& other) const
{
// Divide normally to get the floor.
const size floor = *this / other;
ptrdiff_t adjWidth = 0;
ptrdiff_t adjHeight = 0;
// Check for width remainder, anything not 0.
// If we multiply the floored number with the other, it will equal
// the old width if there was no remainder.
if (other._width * floor._width != _width)
{
// If there was any remainder,
// Grow the magnitude by 1 in the
// direction of the sign.
if (floor.width() >= 0)
{
++adjWidth;
}
else
{
--adjWidth;
}
}
// Check for height remainder, anything not 0.
// If we multiply the floored number with the other, it will equal
// the old width if there was no remainder.
if (other._height * floor._height != _height)
{
// If there was any remainder,
// Grow the magnitude by 1 in the
// direction of the sign.
if (_height >= 0)
{
++adjHeight;
}
else
{
--adjHeight;
}
}
return floor + size{ adjWidth, adjHeight };
}
constexpr ptrdiff_t width() const noexcept
{
return _width;
}
template<typename T>
T width() const
{
T ret;
THROW_HR_IF(E_ABORT, !base::MakeCheckedNum(width()).AssignIfValid(&ret));
return ret;
}
constexpr ptrdiff_t height() const noexcept
{
return _height;
}
template<typename T>
T height() const
{
T ret;
THROW_HR_IF(E_ABORT, !base::MakeCheckedNum(height()).AssignIfValid(&ret));
return ret;
}
ptrdiff_t area() const
{
ptrdiff_t result;
THROW_HR_IF(E_ABORT, !base::CheckMul(_width, _height).AssignIfValid(&result));
return result;
}
template<typename T>
T area() const
{
T ret;
THROW_HR_IF(E_ABORT, !base::CheckMul(_width, _height).AssignIfValid(&ret));
return ret;
}
#ifdef _WINCONTYPES_
operator COORD() const
{
COORD ret;
THROW_HR_IF(E_ABORT, !base::MakeCheckedNum(_width).AssignIfValid(&ret.X));
THROW_HR_IF(E_ABORT, !base::MakeCheckedNum(_height).AssignIfValid(&ret.Y));
return ret;
}
#endif
#ifdef _WINDEF_
operator SIZE() const
{
SIZE ret;
THROW_HR_IF(E_ABORT, !base::MakeCheckedNum(_width).AssignIfValid(&ret.cx));
THROW_HR_IF(E_ABORT, !base::MakeCheckedNum(_height).AssignIfValid(&ret.cy));
return ret;
}
#endif
#ifdef DCOMMON_H_INCLUDED
constexpr operator D2D1_SIZE_F() const noexcept
{
return D2D1_SIZE_F{ gsl::narrow_cast<float>(_width), gsl::narrow_cast<float>(_height) };
}
#endif
std::wstring to_string() const
{
return wil::str_printf<std::wstring>(L"[W:%td, H:%td]", width(), height());
}
protected:
ptrdiff_t _width;
ptrdiff_t _height;
#ifdef UNIT_TESTING
friend class ::SizeTests;
#endif
};
};
#ifdef __WEX_COMMON_H__
namespace WEX::TestExecution
{
template<>
class VerifyOutputTraits<::til::size>
{
public:
static WEX::Common::NoThrowString ToString(const ::til::size& size)
{
return WEX::Common::NoThrowString(size.to_string().c_str());
}
};
template<>
class VerifyCompareTraits<::til::size, ::til::size>
{
public:
static bool AreEqual(const ::til::size& expected, const ::til::size& actual) noexcept
{
return expected == actual;
}
static bool AreSame(const ::til::size& expected, const ::til::size& actual) noexcept
{
return &expected == &actual;
}
static bool IsLessThan(const ::til::size& expectedLess, const ::til::size& expectedGreater) = delete;
static bool IsGreaterThan(const ::til::size& expectedGreater, const ::til::size& expectedLess) = delete;
static bool IsNull(const ::til::size& object) noexcept
{
return object == til::size{};
}
};
};
#endif