|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
#include "Scraper.h" |
|
|
|
#include <windows.h> |
|
|
|
#include <stdint.h> |
|
|
|
#include <algorithm> |
|
#include <utility> |
|
|
|
#include "../shared/WinptyAssert.h" |
|
#include "../shared/winpty_snprintf.h" |
|
|
|
#include "ConsoleFont.h" |
|
#include "Win32Console.h" |
|
#include "Win32ConsoleBuffer.h" |
|
|
|
namespace { |
|
|
|
template <typename T> |
|
T constrained(T min, T val, T max) { |
|
ASSERT(min <= max); |
|
return std::min(std::max(min, val), max); |
|
} |
|
|
|
} |
|
|
|
Scraper::Scraper( |
|
Win32Console &console, |
|
Win32ConsoleBuffer &buffer, |
|
std::unique_ptr<Terminal> terminal, |
|
Coord initialSize) : |
|
m_console(console), |
|
m_terminal(std::move(terminal)), |
|
m_ptySize(initialSize) |
|
{ |
|
m_consoleBuffer = &buffer; |
|
|
|
resetConsoleTracking(Terminal::OmitClear, buffer.windowRect().top()); |
|
|
|
m_bufferData.resize(BUFFER_LINE_COUNT); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
setSmallFont(buffer.conout(), initialSize.X, m_console.isNewW10()); |
|
buffer.moveWindow(SmallRect(0, 0, 1, 1)); |
|
buffer.resizeBufferRange(Coord(initialSize.X, BUFFER_LINE_COUNT)); |
|
const auto largest = GetLargestConsoleWindowSize(buffer.conout()); |
|
buffer.moveWindow(SmallRect( |
|
0, 0, |
|
std::min(initialSize.X, largest.X), |
|
std::min(initialSize.Y, largest.Y))); |
|
buffer.setCursorPosition(Coord(0, 0)); |
|
|
|
|
|
|
|
buffer.setTextAttribute(Win32ConsoleBuffer::kDefaultAttributes); |
|
buffer.clearAllLines(m_consoleBuffer->bufferInfo()); |
|
|
|
m_consoleBuffer = nullptr; |
|
} |
|
|
|
Scraper::~Scraper() |
|
{ |
|
} |
|
|
|
|
|
void Scraper::resizeWindow(Win32ConsoleBuffer &buffer, |
|
Coord newSize, |
|
ConsoleScreenBufferInfo &finalInfoOut) |
|
{ |
|
m_consoleBuffer = &buffer; |
|
m_ptySize = newSize; |
|
syncConsoleContentAndSize(true, finalInfoOut); |
|
m_consoleBuffer = nullptr; |
|
} |
|
|
|
|
|
void Scraper::scrapeBuffer(Win32ConsoleBuffer &buffer, |
|
ConsoleScreenBufferInfo &finalInfoOut) |
|
{ |
|
m_consoleBuffer = &buffer; |
|
syncConsoleContentAndSize(false, finalInfoOut); |
|
m_consoleBuffer = nullptr; |
|
} |
|
|
|
void Scraper::resetConsoleTracking( |
|
Terminal::SendClearFlag sendClear, int64_t scrapedLineCount) |
|
{ |
|
for (ConsoleLine &line : m_bufferData) { |
|
line.reset(); |
|
} |
|
m_syncRow = -1; |
|
m_scrapedLineCount = scrapedLineCount; |
|
m_scrolledCount = 0; |
|
m_maxBufferedLine = -1; |
|
m_dirtyWindowTop = -1; |
|
m_dirtyLineCount = 0; |
|
m_terminal->reset(sendClear, m_scrapedLineCount); |
|
} |
|
|
|
|
|
|
|
|
|
void Scraper::markEntireWindowDirty(const SmallRect &windowRect) |
|
{ |
|
m_dirtyLineCount = std::max(m_dirtyLineCount, |
|
windowRect.top() + windowRect.height()); |
|
} |
|
|
|
|
|
|
|
void Scraper::scanForDirtyLines(const SmallRect &windowRect) |
|
{ |
|
const int w = m_readBuffer.rect().width(); |
|
ASSERT(m_dirtyLineCount >= 1); |
|
const CHAR_INFO *const prevLine = |
|
m_readBuffer.lineData(m_dirtyLineCount - 1); |
|
WORD prevLineAttr = prevLine[w - 1].Attributes; |
|
const int stopLine = windowRect.top() + windowRect.height(); |
|
|
|
for (int line = m_dirtyLineCount; line < stopLine; ++line) { |
|
const CHAR_INFO *lineData = m_readBuffer.lineData(line); |
|
for (int col = 0; col < w; ++col) { |
|
const WORD colAttr = lineData[col].Attributes; |
|
if (lineData[col].Char.UnicodeChar != L' ' || |
|
colAttr != prevLineAttr) { |
|
m_dirtyLineCount = line + 1; |
|
break; |
|
} |
|
} |
|
prevLineAttr = lineData[w - 1].Attributes; |
|
} |
|
} |
|
|
|
|
|
|
|
void Scraper::clearBufferLines( |
|
const int firstRow, |
|
const int count) |
|
{ |
|
ASSERT(!m_directMode); |
|
for (int row = firstRow; row < firstRow + count; ++row) { |
|
const int64_t bufLine = row + m_scrolledCount; |
|
m_maxBufferedLine = std::max(m_maxBufferedLine, bufLine); |
|
m_bufferData[bufLine % BUFFER_LINE_COUNT].blank( |
|
Win32ConsoleBuffer::kDefaultAttributes); |
|
} |
|
} |
|
|
|
static bool cursorInWindow(const ConsoleScreenBufferInfo &info) |
|
{ |
|
return info.dwCursorPosition.Y >= info.srWindow.Top && |
|
info.dwCursorPosition.Y <= info.srWindow.Bottom; |
|
} |
|
|
|
void Scraper::resizeImpl(const ConsoleScreenBufferInfo &origInfo) |
|
{ |
|
ASSERT(m_console.frozen()); |
|
const int cols = m_ptySize.X; |
|
const int rows = m_ptySize.Y; |
|
Coord finalBufferSize; |
|
|
|
{ |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const Coord origBufferSize = origInfo.bufferSize(); |
|
const SmallRect origWindowRect = origInfo.windowRect(); |
|
|
|
if (m_directMode) { |
|
for (ConsoleLine &line : m_bufferData) { |
|
line.reset(); |
|
} |
|
} else { |
|
m_consoleBuffer->clearLines(0, origWindowRect.Top, origInfo); |
|
clearBufferLines(0, origWindowRect.Top); |
|
if (m_syncRow != -1) { |
|
createSyncMarker(std::min( |
|
m_syncRow, |
|
BUFFER_LINE_COUNT - rows |
|
- SYNC_MARKER_LEN |
|
- SYNC_MARKER_MARGIN)); |
|
} |
|
} |
|
|
|
finalBufferSize = Coord( |
|
cols, |
|
|
|
|
|
|
|
(origWindowRect.height() == origBufferSize.Y) |
|
? rows |
|
: std::max<int>(rows, origBufferSize.Y)); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
m_console.setFrozen(false); |
|
setSmallFont(m_consoleBuffer->conout(), cols, m_console.isNewW10()); |
|
} |
|
|
|
|
|
|
|
const auto largest = |
|
GetLargestConsoleWindowSize(m_consoleBuffer->conout()); |
|
const short visibleCols = std::min<short>(cols, largest.X); |
|
const short visibleRows = std::min<short>(rows, largest.Y); |
|
|
|
{ |
|
|
|
|
|
m_console.setFrozen(true); |
|
const auto info = m_consoleBuffer->bufferInfo(); |
|
const auto &bufferSize = info.dwSize; |
|
const int tmpWindowWidth = std::min(bufferSize.X, visibleCols); |
|
const int tmpWindowHeight = std::min(bufferSize.Y, visibleRows); |
|
SmallRect tmpWindowRect( |
|
0, |
|
std::min<int>(bufferSize.Y - tmpWindowHeight, |
|
info.windowRect().Top), |
|
tmpWindowWidth, |
|
tmpWindowHeight); |
|
if (cursorInWindow(info)) { |
|
tmpWindowRect = tmpWindowRect.ensureLineIncluded( |
|
info.cursorPosition().Y); |
|
} |
|
m_consoleBuffer->moveWindow(tmpWindowRect); |
|
} |
|
|
|
{ |
|
|
|
m_console.setFrozen(false); |
|
m_consoleBuffer->resizeBufferRange(finalBufferSize); |
|
} |
|
|
|
{ |
|
|
|
m_console.setFrozen(true); |
|
const ConsoleScreenBufferInfo info = m_consoleBuffer->bufferInfo(); |
|
|
|
SmallRect finalWindowRect( |
|
0, |
|
std::min<int>(info.bufferSize().Y - visibleRows, |
|
info.windowRect().Top), |
|
visibleCols, |
|
visibleRows); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if (!m_directMode && m_dirtyLineCount > finalWindowRect.Bottom + 1) { |
|
|
|
|
|
|
|
|
|
finalWindowRect = SmallRect( |
|
0, m_dirtyLineCount - visibleRows, |
|
visibleCols, visibleRows); |
|
} |
|
|
|
|
|
if (cursorInWindow(info)) { |
|
finalWindowRect = finalWindowRect.ensureLineIncluded( |
|
info.cursorPosition().Y); |
|
} |
|
|
|
m_consoleBuffer->moveWindow(finalWindowRect); |
|
m_dirtyWindowTop = finalWindowRect.Top; |
|
} |
|
|
|
ASSERT(m_console.frozen()); |
|
} |
|
|
|
void Scraper::syncConsoleContentAndSize( |
|
bool forceResize, |
|
ConsoleScreenBufferInfo &finalInfoOut) |
|
{ |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if (!m_console.isNewW10() || forceResize) { |
|
m_console.setFrozen(true); |
|
} |
|
|
|
const ConsoleScreenBufferInfo info = m_consoleBuffer->bufferInfo(); |
|
bool cursorVisible = true; |
|
CONSOLE_CURSOR_INFO cursorInfo = {}; |
|
if (!GetConsoleCursorInfo(GetStdHandle(STD_OUTPUT_HANDLE), &cursorInfo)) { |
|
trace("GetConsoleCursorInfo failed"); |
|
} else { |
|
cursorVisible = cursorInfo.bVisible != 0; |
|
} |
|
|
|
|
|
|
|
const bool newDirectMode = (info.bufferSize().Y != BUFFER_LINE_COUNT); |
|
if (newDirectMode != m_directMode) { |
|
trace("Entering %s mode", newDirectMode ? "direct" : "scrolling"); |
|
resetConsoleTracking(Terminal::SendClear, |
|
newDirectMode ? 0 : info.windowRect().top()); |
|
m_directMode = newDirectMode; |
|
|
|
|
|
|
|
if (!m_directMode) { |
|
m_console.setFrozen(true); |
|
forceResize = true; |
|
} |
|
} |
|
|
|
if (m_directMode) { |
|
|
|
|
|
if (forceResize) { |
|
resizeImpl(info); |
|
} |
|
directScrapeOutput(info, cursorVisible); |
|
} else { |
|
if (!m_console.frozen()) { |
|
if (!scrollingScrapeOutput(info, cursorVisible, true)) { |
|
m_console.setFrozen(true); |
|
} |
|
} |
|
if (m_console.frozen()) { |
|
scrollingScrapeOutput(info, cursorVisible, false); |
|
} |
|
|
|
|
|
|
|
if (forceResize) { |
|
resizeImpl(info); |
|
} |
|
} |
|
|
|
finalInfoOut = forceResize ? m_consoleBuffer->bufferInfo() : info; |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
WORD Scraper::attributesMask() |
|
{ |
|
const auto WINPTY_ENABLE_VIRTUAL_TERMINAL_PROCESSING = 0x4u; |
|
const auto WINPTY_ENABLE_LVB_GRID_WORLDWIDE = 0x10u; |
|
const auto WINPTY_COMMON_LVB_REVERSE_VIDEO = 0x4000u; |
|
const auto WINPTY_COMMON_LVB_UNDERSCORE = 0x8000u; |
|
|
|
const auto cp = GetConsoleOutputCP(); |
|
const auto isCjk = (cp == 932 || cp == 936 || cp == 949 || cp == 950); |
|
|
|
const DWORD outputMode = [this]{ |
|
ASSERT(this->m_consoleBuffer != nullptr); |
|
DWORD mode = 0; |
|
if (!GetConsoleMode(this->m_consoleBuffer->conout(), &mode)) { |
|
mode = 0; |
|
} |
|
return mode; |
|
}(); |
|
const bool hasEnableLvbGridWorldwide = |
|
(outputMode & WINPTY_ENABLE_LVB_GRID_WORLDWIDE) != 0; |
|
const bool hasEnableVtProcessing = |
|
(outputMode & WINPTY_ENABLE_VIRTUAL_TERMINAL_PROCESSING) != 0; |
|
|
|
|
|
|
|
|
|
const auto isReverseSupported = |
|
isCjk || hasEnableLvbGridWorldwide || hasEnableVtProcessing || m_console.isNewW10(); |
|
const auto isUnderscoreSupported = |
|
isCjk || hasEnableLvbGridWorldwide || hasEnableVtProcessing; |
|
|
|
WORD mask = ~0; |
|
if (!isReverseSupported) { mask &= ~WINPTY_COMMON_LVB_REVERSE_VIDEO; } |
|
if (!isUnderscoreSupported) { mask &= ~WINPTY_COMMON_LVB_UNDERSCORE; } |
|
return mask; |
|
} |
|
|
|
void Scraper::directScrapeOutput(const ConsoleScreenBufferInfo &info, |
|
bool consoleCursorVisible) |
|
{ |
|
const SmallRect windowRect = info.windowRect(); |
|
|
|
const SmallRect scrapeRect( |
|
windowRect.left(), windowRect.top(), |
|
std::min<SHORT>(std::min(windowRect.width(), m_ptySize.X), |
|
MAX_CONSOLE_WIDTH), |
|
std::min<SHORT>(std::min(windowRect.height(), m_ptySize.Y), |
|
BUFFER_LINE_COUNT)); |
|
const int w = scrapeRect.width(); |
|
const int h = scrapeRect.height(); |
|
|
|
const Coord cursor = info.cursorPosition(); |
|
const bool showTerminalCursor = |
|
consoleCursorVisible && scrapeRect.contains(cursor); |
|
const int cursorColumn = !showTerminalCursor ? -1 : cursor.X - scrapeRect.Left; |
|
const int cursorLine = !showTerminalCursor ? -1 : cursor.Y - scrapeRect.Top; |
|
|
|
if (!showTerminalCursor) { |
|
m_terminal->hideTerminalCursor(); |
|
} |
|
|
|
largeConsoleRead(m_readBuffer, *m_consoleBuffer, scrapeRect, attributesMask()); |
|
|
|
for (int line = 0; line < h; ++line) { |
|
const CHAR_INFO *const curLine = |
|
m_readBuffer.lineData(scrapeRect.top() + line); |
|
ConsoleLine &bufLine = m_bufferData[line]; |
|
if (bufLine.detectChangeAndSetLine(curLine, w)) { |
|
const int lineCursorColumn = |
|
line == cursorLine ? cursorColumn : -1; |
|
m_terminal->sendLine(line, curLine, w, lineCursorColumn); |
|
} |
|
} |
|
|
|
if (showTerminalCursor) { |
|
m_terminal->showTerminalCursor(cursorColumn, cursorLine); |
|
} |
|
} |
|
|
|
bool Scraper::scrollingScrapeOutput(const ConsoleScreenBufferInfo &info, |
|
bool consoleCursorVisible, |
|
bool tentative) |
|
{ |
|
const Coord cursor = info.cursorPosition(); |
|
const SmallRect windowRect = info.windowRect(); |
|
|
|
if (m_syncRow != -1) { |
|
|
|
|
|
const int markerRow = findSyncMarker(); |
|
if (markerRow == -1) { |
|
if (tentative) { |
|
|
|
|
|
return false; |
|
} |
|
|
|
trace("Sync marker has disappeared -- resetting the terminal" |
|
" (m_syncCounter=%u)", |
|
m_syncCounter); |
|
resetConsoleTracking(Terminal::SendClear, windowRect.top()); |
|
} else if (markerRow != m_syncRow) { |
|
ASSERT(markerRow < m_syncRow); |
|
m_scrolledCount += (m_syncRow - markerRow); |
|
m_syncRow = markerRow; |
|
|
|
markEntireWindowDirty(windowRect); |
|
} |
|
} |
|
|
|
|
|
|
|
const int newSyncRow = |
|
static_cast<int>(windowRect.top()) - SYNC_MARKER_LEN - SYNC_MARKER_MARGIN; |
|
const bool shouldCreateSyncRow = |
|
newSyncRow >= m_syncRow + SYNC_MARKER_LEN + SYNC_MARKER_MARGIN; |
|
if (tentative && shouldCreateSyncRow) { |
|
|
|
|
|
return false; |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
if (m_dirtyWindowTop != -1) { |
|
if (windowRect.top() > m_dirtyWindowTop) { |
|
|
|
markEntireWindowDirty(windowRect); |
|
} else if (windowRect.top() < m_dirtyWindowTop) { |
|
if (tentative) { |
|
|
|
|
|
return false; |
|
} |
|
|
|
|
|
|
|
trace("Window moved upward -- resetting the terminal" |
|
" (m_syncCounter=%u)", |
|
m_syncCounter); |
|
resetConsoleTracking(Terminal::SendClear, windowRect.top()); |
|
} |
|
} |
|
m_dirtyWindowTop = windowRect.top(); |
|
m_dirtyLineCount = std::max(m_dirtyLineCount, cursor.Y + 1); |
|
m_dirtyLineCount = std::max(m_dirtyLineCount, (int)windowRect.top()); |
|
|
|
|
|
ASSERT(m_dirtyLineCount >= 1); |
|
|
|
|
|
const int64_t firstVirtLine = std::min(m_scrapedLineCount, |
|
windowRect.top() + m_scrolledCount); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const int firstReadLine = std::min<int>(firstVirtLine - m_scrolledCount, |
|
m_dirtyLineCount - 1); |
|
const int stopReadLine = std::max(windowRect.top() + windowRect.height(), |
|
m_dirtyLineCount); |
|
ASSERT(firstReadLine >= 0 && stopReadLine > firstReadLine); |
|
largeConsoleRead(m_readBuffer, |
|
*m_consoleBuffer, |
|
SmallRect(0, firstReadLine, |
|
std::min<SHORT>(info.bufferSize().X, |
|
MAX_CONSOLE_WIDTH), |
|
stopReadLine - firstReadLine), |
|
attributesMask()); |
|
|
|
|
|
|
|
|
|
|
|
|
|
if (tentative) { |
|
const auto infoCheck = m_consoleBuffer->bufferInfo(); |
|
if (info.bufferSize() != infoCheck.bufferSize() || |
|
info.windowRect() != infoCheck.windowRect() || |
|
info.cursorPosition() != infoCheck.cursorPosition()) { |
|
return false; |
|
} |
|
if (m_syncRow != -1 && m_syncRow != findSyncMarker()) { |
|
return false; |
|
} |
|
} |
|
|
|
if (shouldCreateSyncRow) { |
|
ASSERT(!tentative); |
|
createSyncMarker(newSyncRow); |
|
} |
|
|
|
|
|
|
|
|
|
|
|
scanForDirtyLines(windowRect); |
|
|
|
|
|
|
|
|
|
|
|
const int64_t stopVirtLine = |
|
std::min(m_dirtyLineCount, windowRect.top() + windowRect.height()) + |
|
m_scrolledCount; |
|
|
|
const bool showTerminalCursor = |
|
consoleCursorVisible && windowRect.contains(cursor); |
|
const int64_t cursorLine = !showTerminalCursor ? -1 : cursor.Y + m_scrolledCount; |
|
const int cursorColumn = !showTerminalCursor ? -1 : cursor.X; |
|
|
|
if (!showTerminalCursor) { |
|
m_terminal->hideTerminalCursor(); |
|
} |
|
|
|
bool sawModifiedLine = false; |
|
|
|
const int w = m_readBuffer.rect().width(); |
|
for (int64_t line = firstVirtLine; line < stopVirtLine; ++line) { |
|
const CHAR_INFO *curLine = |
|
m_readBuffer.lineData(line - m_scrolledCount); |
|
ConsoleLine &bufLine = m_bufferData[line % BUFFER_LINE_COUNT]; |
|
if (line > m_maxBufferedLine) { |
|
m_maxBufferedLine = line; |
|
sawModifiedLine = true; |
|
} |
|
if (sawModifiedLine) { |
|
bufLine.setLine(curLine, w); |
|
} else { |
|
sawModifiedLine = bufLine.detectChangeAndSetLine(curLine, w); |
|
} |
|
if (sawModifiedLine) { |
|
const int lineCursorColumn = |
|
line == cursorLine ? cursorColumn : -1; |
|
m_terminal->sendLine(line, curLine, w, lineCursorColumn); |
|
} |
|
} |
|
|
|
m_scrapedLineCount = windowRect.top() + m_scrolledCount; |
|
|
|
if (showTerminalCursor) { |
|
m_terminal->showTerminalCursor(cursorColumn, cursorLine); |
|
} |
|
|
|
return true; |
|
} |
|
|
|
void Scraper::syncMarkerText(CHAR_INFO (&output)[SYNC_MARKER_LEN]) |
|
{ |
|
|
|
|
|
char str[SYNC_MARKER_LEN + 1]; |
|
winpty_snprintf(str, "S*Y*N*C*%08x", m_syncCounter); |
|
for (int i = 0; i < SYNC_MARKER_LEN; ++i) { |
|
output[i].Char.UnicodeChar = str[i]; |
|
output[i].Attributes = 7; |
|
} |
|
} |
|
|
|
int Scraper::findSyncMarker() |
|
{ |
|
ASSERT(m_syncRow >= 0); |
|
CHAR_INFO marker[SYNC_MARKER_LEN]; |
|
CHAR_INFO column[BUFFER_LINE_COUNT]; |
|
syncMarkerText(marker); |
|
SmallRect rect(0, 0, 1, m_syncRow + SYNC_MARKER_LEN); |
|
m_consoleBuffer->read(rect, column); |
|
int i; |
|
for (i = m_syncRow; i >= 0; --i) { |
|
int j; |
|
for (j = 0; j < SYNC_MARKER_LEN; ++j) { |
|
if (column[i + j].Char.UnicodeChar != marker[j].Char.UnicodeChar) |
|
break; |
|
} |
|
if (j == SYNC_MARKER_LEN) |
|
return i; |
|
} |
|
return -1; |
|
} |
|
|
|
void Scraper::createSyncMarker(int row) |
|
{ |
|
ASSERT(row >= 1); |
|
|
|
|
|
|
|
m_consoleBuffer->clearLines(row - 1, SYNC_MARKER_LEN + 1, |
|
m_consoleBuffer->bufferInfo()); |
|
|
|
|
|
m_syncCounter++; |
|
CHAR_INFO marker[SYNC_MARKER_LEN]; |
|
syncMarkerText(marker); |
|
m_syncRow = row; |
|
SmallRect markerRect(0, m_syncRow, 1, SYNC_MARKER_LEN); |
|
m_consoleBuffer->write(markerRect, marker); |
|
} |
|
|