| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| |
|
| | #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); |
| | } |
| |
|