bool Scraper::scrollingScrapeOutput()

in src/agent/Scraper.cc [476:648]


bool Scraper::scrollingScrapeOutput(const ConsoleScreenBufferInfo &info,
                                    bool consoleCursorVisible,
                                    bool tentative)
{
    const Coord cursor = info.cursorPosition();
    const SmallRect windowRect = info.windowRect();

    if (m_syncRow != -1) {
        // If a synchronizing marker was placed into the history, look for it
        // and adjust the scroll count.
        const int markerRow = findSyncMarker();
        if (markerRow == -1) {
            if (tentative) {
                // I *think* it's possible to keep going, but it's simple to
                // bail out.
                return false;
            }
            // Something has happened.  Reset the terminal.
            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;
            // If the buffer has scrolled, then the entire window is dirty.
            markEntireWindowDirty(windowRect);
        }
    }

    // Creating a new sync row requires clearing part of the console buffer, so
    // avoid doing it if there's already a sync row that's good enough.
    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) {
        // It's difficult even in principle to put down a new marker if the
        // console can scroll an arbitrarily amount while we're writing.
        return false;
    }

    // Update the dirty line count:
    //  - If the window has moved, the entire window is dirty.
    //  - Everything up to the cursor is dirty.
    //  - All lines above the window are dirty.
    //  - Any non-blank lines are dirty.
    if (m_dirtyWindowTop != -1) {
        if (windowRect.top() > m_dirtyWindowTop) {
            // The window has moved down, presumably as a result of scrolling.
            markEntireWindowDirty(windowRect);
        } else if (windowRect.top() < m_dirtyWindowTop) {
            if (tentative) {
                // I *think* it's possible to keep going, but it's simple to
                // bail out.
                return false;
            }
            // The window has moved upward.  This is generally not expected to
            // happen, but the CMD/PowerShell CLS command will move the window
            // to the top as part of clearing everything else in the console.
            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());

    // There will be at least one dirty line, because there is a cursor.
    ASSERT(m_dirtyLineCount >= 1);

    // The first line to scrape, in virtual line coordinates.
    const int64_t firstVirtLine = std::min(m_scrapedLineCount,
                                           windowRect.top() + m_scrolledCount);

    // Read all the data we will need from the console.  Start reading with the
    // first line to scrape, but adjust the the read area upward to account for
    // scanForDirtyLines' need to read the previous attribute.  Read to the
    // bottom of the window.  (It's not clear to me whether the
    // m_dirtyLineCount adjustment here is strictly necessary.  It isn't
    // necessary so long as the cursor is inside the current window.)
    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 we're scraping the buffer without freezing it, we have to query the
    // buffer position data separately from the buffer content, so the two
    // could easily be out-of-sync.  If they *are* out-of-sync, abort the
    // scrape operation and restart it frozen.  (We may have updated the
    // dirty-line high-water-mark, but that should be OK.)
    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);
    }

    // At this point, we're finished interacting (reading or writing) the
    // console, and we just need to convert our collected data into terminal
    // output.

    scanForDirtyLines(windowRect);

    // Note that it's possible for all the lines on the current window to
    // be non-dirty.

    // The line to stop scraping at, in virtual line coordinates.
    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;
}