1 // Copyright (c) 2011-2016 Ryan Prichard
3 // Permission is hereby granted, free of charge, to any person obtaining a copy
4 // of this software and associated documentation files (the "Software"), to
5 // deal in the Software without restriction, including without limitation the
6 // rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
7 // sell copies of the Software, and to permit persons to whom the Software is
8 // furnished to do so, subject to the following conditions:
10 // The above copyright notice and this permission notice shall be included in
11 // all copies or substantial portions of the Software.
13 // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14 // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15 // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16 // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17 // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
18 // FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
30 #include "../shared/WinptyAssert.h"
31 #include "../shared/winpty_snprintf.h"
33 #include "ConsoleFont.h"
34 #include "Win32Console.h"
35 #include "Win32ConsoleBuffer.h"
40 T constrained(T min, T val, T max) {
42 return std::min(std::max(min, val), max);
45 } // anonymous namespace
48 Win32Console &console,
49 Win32ConsoleBuffer &buffer,
50 std::unique_ptr<Terminal> terminal,
53 m_terminal(std::move(terminal)),
54 m_ptySize(initialSize)
56 m_consoleBuffer = &buffer;
58 resetConsoleTracking(Terminal::OmitClear, buffer.windowRect().top());
60 m_bufferData.resize(BUFFER_LINE_COUNT);
62 // Setup the initial screen buffer and window size.
64 // Use SetConsoleWindowInfo to shrink the console window as much as
65 // possible -- to a 1x1 cell at the top-left. This call always succeeds.
66 // Prior to the new Windows 10 console, it also actually resizes the GUI
67 // window to 1x1 cell. Nevertheless, even though the GUI window can
68 // therefore be narrower than its minimum, calling
69 // SetConsoleScreenBufferSize with a 1x1 size still fails.
71 // While the small font intends to support large buffers, a user could
72 // still hit a limit imposed by their monitor width, so cap the new window
73 // size to GetLargestConsoleWindowSize().
74 setSmallFont(buffer.conout(), initialSize.X, m_console.isNewW10());
75 buffer.moveWindow(SmallRect(0, 0, 1, 1));
76 buffer.resizeBufferRange(Coord(initialSize.X, BUFFER_LINE_COUNT));
77 const auto largest = GetLargestConsoleWindowSize(buffer.conout());
78 buffer.moveWindow(SmallRect(
80 std::min(initialSize.X, largest.X),
81 std::min(initialSize.Y, largest.Y)));
82 buffer.setCursorPosition(Coord(0, 0));
84 // For the sake of the color translation heuristic, set the console color
85 // to LtGray-on-Black.
86 buffer.setTextAttribute(Win32ConsoleBuffer::kDefaultAttributes);
87 buffer.clearAllLines(m_consoleBuffer->bufferInfo());
89 m_consoleBuffer = nullptr;
96 // Whether or not the agent is frozen on entry, it will be frozen on exit.
97 void Scraper::resizeWindow(Win32ConsoleBuffer &buffer,
99 ConsoleScreenBufferInfo &finalInfoOut)
101 m_consoleBuffer = &buffer;
103 syncConsoleContentAndSize(true, finalInfoOut);
104 m_consoleBuffer = nullptr;
107 // This function may freeze the agent, but it will not unfreeze it.
108 void Scraper::scrapeBuffer(Win32ConsoleBuffer &buffer,
109 ConsoleScreenBufferInfo &finalInfoOut)
111 m_consoleBuffer = &buffer;
112 syncConsoleContentAndSize(false, finalInfoOut);
113 m_consoleBuffer = nullptr;
116 void Scraper::resetConsoleTracking(
117 Terminal::SendClearFlag sendClear, int64_t scrapedLineCount)
119 for (ConsoleLine &line : m_bufferData) {
123 m_scrapedLineCount = scrapedLineCount;
125 m_maxBufferedLine = -1;
126 m_dirtyWindowTop = -1;
127 m_dirtyLineCount = 0;
128 m_terminal->reset(sendClear, m_scrapedLineCount);
131 // Detect window movement. If the window moves down (presumably as a
132 // result of scrolling), then assume that all screen buffer lines down to
133 // the bottom of the window are dirty.
134 void Scraper::markEntireWindowDirty(const SmallRect &windowRect)
136 m_dirtyLineCount = std::max(m_dirtyLineCount,
137 windowRect.top() + windowRect.height());
140 // Scan the screen buffer and advance the dirty line count when we find
142 void Scraper::scanForDirtyLines(const SmallRect &windowRect)
144 const int w = m_readBuffer.rect().width();
145 ASSERT(m_dirtyLineCount >= 1);
146 const CHAR_INFO *const prevLine =
147 m_readBuffer.lineData(m_dirtyLineCount - 1);
148 WORD prevLineAttr = prevLine[w - 1].Attributes;
149 const int stopLine = windowRect.top() + windowRect.height();
151 for (int line = m_dirtyLineCount; line < stopLine; ++line) {
152 const CHAR_INFO *lineData = m_readBuffer.lineData(line);
153 for (int col = 0; col < w; ++col) {
154 const WORD colAttr = lineData[col].Attributes;
155 if (lineData[col].Char.UnicodeChar != L' ' ||
156 colAttr != prevLineAttr) {
157 m_dirtyLineCount = line + 1;
161 prevLineAttr = lineData[w - 1].Attributes;
165 // Clear lines in the line buffer. The `firstRow` parameter is in
166 // screen-buffer coordinates.
167 void Scraper::clearBufferLines(
171 ASSERT(!m_directMode);
172 for (int row = firstRow; row < firstRow + count; ++row) {
173 const int64_t bufLine = row + m_scrolledCount;
174 m_maxBufferedLine = std::max(m_maxBufferedLine, bufLine);
175 m_bufferData[bufLine % BUFFER_LINE_COUNT].blank(
176 Win32ConsoleBuffer::kDefaultAttributes);
180 static bool cursorInWindow(const ConsoleScreenBufferInfo &info)
182 return info.dwCursorPosition.Y >= info.srWindow.Top &&
183 info.dwCursorPosition.Y <= info.srWindow.Bottom;
186 void Scraper::resizeImpl(const ConsoleScreenBufferInfo &origInfo)
188 ASSERT(m_console.frozen());
189 const int cols = m_ptySize.X;
190 const int rows = m_ptySize.Y;
191 Coord finalBufferSize;
195 // To accommodate Windows 10, erase all lines up to the top of the
196 // visible window. It's hard to tell whether this is strictly
197 // necessary. It ensures that the sync marker won't move downward,
198 // and it ensures that we won't repeat lines that have already scrolled
199 // up into the scrollback.
201 // It *is* possible for these blank lines to reappear in the visible
202 // window (e.g. if the window is made taller), but because we blanked
203 // the lines in the line buffer, we still don't output them again.
205 const Coord origBufferSize = origInfo.bufferSize();
206 const SmallRect origWindowRect = origInfo.windowRect();
209 for (ConsoleLine &line : m_bufferData) {
213 m_consoleBuffer->clearLines(0, origWindowRect.Top, origInfo);
214 clearBufferLines(0, origWindowRect.Top);
215 if (m_syncRow != -1) {
216 createSyncMarker(std::min(
218 BUFFER_LINE_COUNT - rows
220 - SYNC_MARKER_MARGIN));
224 finalBufferSize = Coord(
226 // If there was previously no scrollback (e.g. a full-screen app
227 // in direct mode) and we're reducing the window height, then
228 // reduce the console buffer's height too.
229 (origWindowRect.height() == origBufferSize.Y)
231 : std::max<int>(rows, origBufferSize.Y));
233 // Reset the console font size. We need to do this before shrinking
234 // the window, because we might need to make the font bigger to permit
235 // a smaller window width. Making the font smaller could expand the
236 // screen buffer, which would hang the conhost process in the
237 // Windows 10 (10240 build) if the console selection is in progress, so
238 // unfreeze it first.
239 m_console.setFrozen(false);
240 setSmallFont(m_consoleBuffer->conout(), cols, m_console.isNewW10());
243 // We try to make the font small enough so that the entire screen buffer
244 // fits on the monitor, but it can't be guaranteed.
246 GetLargestConsoleWindowSize(m_consoleBuffer->conout());
247 const short visibleCols = std::min<short>(cols, largest.X);
248 const short visibleRows = std::min<short>(rows, largest.Y);
251 // Make the window small enough. We want the console frozen during
252 // this step so we don't accidentally move the window above the cursor.
253 m_console.setFrozen(true);
254 const auto info = m_consoleBuffer->bufferInfo();
255 const auto &bufferSize = info.dwSize;
256 const int tmpWindowWidth = std::min(bufferSize.X, visibleCols);
257 const int tmpWindowHeight = std::min(bufferSize.Y, visibleRows);
258 SmallRect tmpWindowRect(
260 std::min<int>(bufferSize.Y - tmpWindowHeight,
261 info.windowRect().Top),
264 if (cursorInWindow(info)) {
265 tmpWindowRect = tmpWindowRect.ensureLineIncluded(
266 info.cursorPosition().Y);
268 m_consoleBuffer->moveWindow(tmpWindowRect);
272 // Resize the buffer to the final desired size.
273 m_console.setFrozen(false);
274 m_consoleBuffer->resizeBufferRange(finalBufferSize);
278 // Expand the window to its full size.
279 m_console.setFrozen(true);
280 const ConsoleScreenBufferInfo info = m_consoleBuffer->bufferInfo();
282 SmallRect finalWindowRect(
284 std::min<int>(info.bufferSize().Y - visibleRows,
285 info.windowRect().Top),
290 // Once a line in the screen buffer is "dirty", it should stay visible
291 // in the console window, so that we continue to update its content in
292 // the terminal. This code is particularly (only?) necessary on
293 // Windows 10, where making the buffer wider can rewrap lines and move
294 // the console window upward.
296 if (!m_directMode && m_dirtyLineCount > finalWindowRect.Bottom + 1) {
297 // In theory, we avoid ensureLineIncluded, because, a massive
298 // amount of output could have occurred while the console was
299 // unfrozen, so that the *top* of the window is now below the
300 // dirtiest tracked line.
301 finalWindowRect = SmallRect(
302 0, m_dirtyLineCount - visibleRows,
303 visibleCols, visibleRows);
306 // Highest priority constraint: ensure that the cursor remains visible.
307 if (cursorInWindow(info)) {
308 finalWindowRect = finalWindowRect.ensureLineIncluded(
309 info.cursorPosition().Y);
312 m_consoleBuffer->moveWindow(finalWindowRect);
313 m_dirtyWindowTop = finalWindowRect.Top;
316 ASSERT(m_console.frozen());
319 void Scraper::syncConsoleContentAndSize(
321 ConsoleScreenBufferInfo &finalInfoOut)
323 // We'll try to avoid freezing the console by reading large chunks (or
324 // all!) of the screen buffer without otherwise attempting to synchronize
325 // with the console application. We can only do this on Windows 10 and up
327 // - Prior to Windows 8, the size of a ReadConsoleOutputW call was limited
328 // by the ~32KB RPC buffer.
329 // - Prior to Windows 10, an out-of-range read region crashes the caller.
330 // (See misc/WindowsBugCrashReader.cc.)
332 if (!m_console.isNewW10() || forceResize) {
333 m_console.setFrozen(true);
336 const ConsoleScreenBufferInfo info = m_consoleBuffer->bufferInfo();
337 bool cursorVisible = true;
338 CONSOLE_CURSOR_INFO cursorInfo = {};
339 if (!GetConsoleCursorInfo(GetStdHandle(STD_OUTPUT_HANDLE), &cursorInfo)) {
340 trace("GetConsoleCursorInfo failed");
342 cursorVisible = cursorInfo.bVisible != 0;
345 // If an app resizes the buffer height, then we enter "direct mode", where
346 // we stop trying to track incremental console changes.
347 const bool newDirectMode = (info.bufferSize().Y != BUFFER_LINE_COUNT);
348 if (newDirectMode != m_directMode) {
349 trace("Entering %s mode", newDirectMode ? "direct" : "scrolling");
350 resetConsoleTracking(Terminal::SendClear,
351 newDirectMode ? 0 : info.windowRect().top());
352 m_directMode = newDirectMode;
354 // When we switch from direct->scrolling mode, make sure the console is
357 m_console.setFrozen(true);
363 // In direct-mode, resizing the console redraws the terminal, so do it
368 directScrapeOutput(info, cursorVisible);
370 if (!m_console.frozen()) {
371 if (!scrollingScrapeOutput(info, cursorVisible, true)) {
372 m_console.setFrozen(true);
375 if (m_console.frozen()) {
376 scrollingScrapeOutput(info, cursorVisible, false);
378 // In scrolling mode, we want to scrape before resizing, because we'll
379 // erase everything in the console buffer up to the top of the console
386 finalInfoOut = forceResize ? m_consoleBuffer->bufferInfo() : info;
389 // Try to match Windows' behavior w.r.t. to the LVB attribute flags. In some
390 // situations, Windows ignores the LVB flags on a character cell because of
391 // backwards compatibility -- apparently some programs set the flags without
392 // intending to enable reverse-video or underscores.
394 // [rprichard 2017-01-15] I haven't actually noticed any old programs that need
395 // this treatment -- the motivation for this function comes from the MSDN
396 // documentation for SetConsoleMode and ENABLE_LVB_GRID_WORLDWIDE.
397 WORD Scraper::attributesMask()
399 const auto WINPTY_ENABLE_VIRTUAL_TERMINAL_PROCESSING = 0x4u;
400 const auto WINPTY_ENABLE_LVB_GRID_WORLDWIDE = 0x10u;
401 const auto WINPTY_COMMON_LVB_REVERSE_VIDEO = 0x4000u;
402 const auto WINPTY_COMMON_LVB_UNDERSCORE = 0x8000u;
404 const auto cp = GetConsoleOutputCP();
405 const auto isCjk = (cp == 932 || cp == 936 || cp == 949 || cp == 950);
407 const DWORD outputMode = [this]{
408 ASSERT(this->m_consoleBuffer != nullptr);
410 if (!GetConsoleMode(this->m_consoleBuffer->conout(), &mode)) {
415 const bool hasEnableLvbGridWorldwide =
416 (outputMode & WINPTY_ENABLE_LVB_GRID_WORLDWIDE) != 0;
417 const bool hasEnableVtProcessing =
418 (outputMode & WINPTY_ENABLE_VIRTUAL_TERMINAL_PROCESSING) != 0;
420 // The new Windows 10 console (as of 14393) seems to respect
421 // COMMON_LVB_REVERSE_VIDEO even in CP437 w/o the other enabling modes, so
422 // try to match that behavior.
423 const auto isReverseSupported =
424 isCjk || hasEnableLvbGridWorldwide || hasEnableVtProcessing || m_console.isNewW10();
425 const auto isUnderscoreSupported =
426 isCjk || hasEnableLvbGridWorldwide || hasEnableVtProcessing;
429 if (!isReverseSupported) { mask &= ~WINPTY_COMMON_LVB_REVERSE_VIDEO; }
430 if (!isUnderscoreSupported) { mask &= ~WINPTY_COMMON_LVB_UNDERSCORE; }
434 void Scraper::directScrapeOutput(const ConsoleScreenBufferInfo &info,
435 bool consoleCursorVisible)
437 const SmallRect windowRect = info.windowRect();
439 const SmallRect scrapeRect(
440 windowRect.left(), windowRect.top(),
441 std::min<SHORT>(std::min(windowRect.width(), m_ptySize.X),
443 std::min<SHORT>(std::min(windowRect.height(), m_ptySize.Y),
445 const int w = scrapeRect.width();
446 const int h = scrapeRect.height();
448 const Coord cursor = info.cursorPosition();
449 const bool showTerminalCursor =
450 consoleCursorVisible && scrapeRect.contains(cursor);
451 const int cursorColumn = !showTerminalCursor ? -1 : cursor.X - scrapeRect.Left;
452 const int cursorLine = !showTerminalCursor ? -1 : cursor.Y - scrapeRect.Top;
454 if (!showTerminalCursor) {
455 m_terminal->hideTerminalCursor();
458 largeConsoleRead(m_readBuffer, *m_consoleBuffer, scrapeRect, attributesMask());
460 for (int line = 0; line < h; ++line) {
461 const CHAR_INFO *const curLine =
462 m_readBuffer.lineData(scrapeRect.top() + line);
463 ConsoleLine &bufLine = m_bufferData[line];
464 if (bufLine.detectChangeAndSetLine(curLine, w)) {
465 const int lineCursorColumn =
466 line == cursorLine ? cursorColumn : -1;
467 m_terminal->sendLine(line, curLine, w, lineCursorColumn);
471 if (showTerminalCursor) {
472 m_terminal->showTerminalCursor(cursorColumn, cursorLine);
476 bool Scraper::scrollingScrapeOutput(const ConsoleScreenBufferInfo &info,
477 bool consoleCursorVisible,
480 const Coord cursor = info.cursorPosition();
481 const SmallRect windowRect = info.windowRect();
483 if (m_syncRow != -1) {
484 // If a synchronizing marker was placed into the history, look for it
485 // and adjust the scroll count.
486 const int markerRow = findSyncMarker();
487 if (markerRow == -1) {
489 // I *think* it's possible to keep going, but it's simple to
493 // Something has happened. Reset the terminal.
494 trace("Sync marker has disappeared -- resetting the terminal"
495 " (m_syncCounter=%u)",
497 resetConsoleTracking(Terminal::SendClear, windowRect.top());
498 } else if (markerRow != m_syncRow) {
499 ASSERT(markerRow < m_syncRow);
500 m_scrolledCount += (m_syncRow - markerRow);
501 m_syncRow = markerRow;
502 // If the buffer has scrolled, then the entire window is dirty.
503 markEntireWindowDirty(windowRect);
507 // Creating a new sync row requires clearing part of the console buffer, so
508 // avoid doing it if there's already a sync row that's good enough.
509 const int newSyncRow =
510 static_cast<int>(windowRect.top()) - SYNC_MARKER_LEN - SYNC_MARKER_MARGIN;
511 const bool shouldCreateSyncRow =
512 newSyncRow >= m_syncRow + SYNC_MARKER_LEN + SYNC_MARKER_MARGIN;
513 if (tentative && shouldCreateSyncRow) {
514 // It's difficult even in principle to put down a new marker if the
515 // console can scroll an arbitrarily amount while we're writing.
519 // Update the dirty line count:
520 // - If the window has moved, the entire window is dirty.
521 // - Everything up to the cursor is dirty.
522 // - All lines above the window are dirty.
523 // - Any non-blank lines are dirty.
524 if (m_dirtyWindowTop != -1) {
525 if (windowRect.top() > m_dirtyWindowTop) {
526 // The window has moved down, presumably as a result of scrolling.
527 markEntireWindowDirty(windowRect);
528 } else if (windowRect.top() < m_dirtyWindowTop) {
530 // I *think* it's possible to keep going, but it's simple to
534 // The window has moved upward. This is generally not expected to
535 // happen, but the CMD/PowerShell CLS command will move the window
536 // to the top as part of clearing everything else in the console.
537 trace("Window moved upward -- resetting the terminal"
538 " (m_syncCounter=%u)",
540 resetConsoleTracking(Terminal::SendClear, windowRect.top());
543 m_dirtyWindowTop = windowRect.top();
544 m_dirtyLineCount = std::max(m_dirtyLineCount, cursor.Y + 1);
545 m_dirtyLineCount = std::max(m_dirtyLineCount, (int)windowRect.top());
547 // There will be at least one dirty line, because there is a cursor.
548 ASSERT(m_dirtyLineCount >= 1);
550 // The first line to scrape, in virtual line coordinates.
551 const int64_t firstVirtLine = std::min(m_scrapedLineCount,
552 windowRect.top() + m_scrolledCount);
554 // Read all the data we will need from the console. Start reading with the
555 // first line to scrape, but adjust the the read area upward to account for
556 // scanForDirtyLines' need to read the previous attribute. Read to the
557 // bottom of the window. (It's not clear to me whether the
558 // m_dirtyLineCount adjustment here is strictly necessary. It isn't
559 // necessary so long as the cursor is inside the current window.)
560 const int firstReadLine = std::min<int>(firstVirtLine - m_scrolledCount,
561 m_dirtyLineCount - 1);
562 const int stopReadLine = std::max(windowRect.top() + windowRect.height(),
564 ASSERT(firstReadLine >= 0 && stopReadLine > firstReadLine);
565 largeConsoleRead(m_readBuffer,
567 SmallRect(0, firstReadLine,
568 std::min<SHORT>(info.bufferSize().X,
570 stopReadLine - firstReadLine),
573 // If we're scraping the buffer without freezing it, we have to query the
574 // buffer position data separately from the buffer content, so the two
575 // could easily be out-of-sync. If they *are* out-of-sync, abort the
576 // scrape operation and restart it frozen. (We may have updated the
577 // dirty-line high-water-mark, but that should be OK.)
579 const auto infoCheck = m_consoleBuffer->bufferInfo();
580 if (info.bufferSize() != infoCheck.bufferSize() ||
581 info.windowRect() != infoCheck.windowRect() ||
582 info.cursorPosition() != infoCheck.cursorPosition()) {
585 if (m_syncRow != -1 && m_syncRow != findSyncMarker()) {
590 if (shouldCreateSyncRow) {
592 createSyncMarker(newSyncRow);
595 // At this point, we're finished interacting (reading or writing) the
596 // console, and we just need to convert our collected data into terminal
599 scanForDirtyLines(windowRect);
601 // Note that it's possible for all the lines on the current window to
604 // The line to stop scraping at, in virtual line coordinates.
605 const int64_t stopVirtLine =
606 std::min(m_dirtyLineCount, windowRect.top() + windowRect.height()) +
609 const bool showTerminalCursor =
610 consoleCursorVisible && windowRect.contains(cursor);
611 const int64_t cursorLine = !showTerminalCursor ? -1 : cursor.Y + m_scrolledCount;
612 const int cursorColumn = !showTerminalCursor ? -1 : cursor.X;
614 if (!showTerminalCursor) {
615 m_terminal->hideTerminalCursor();
618 bool sawModifiedLine = false;
620 const int w = m_readBuffer.rect().width();
621 for (int64_t line = firstVirtLine; line < stopVirtLine; ++line) {
622 const CHAR_INFO *curLine =
623 m_readBuffer.lineData(line - m_scrolledCount);
624 ConsoleLine &bufLine = m_bufferData[line % BUFFER_LINE_COUNT];
625 if (line > m_maxBufferedLine) {
626 m_maxBufferedLine = line;
627 sawModifiedLine = true;
629 if (sawModifiedLine) {
630 bufLine.setLine(curLine, w);
632 sawModifiedLine = bufLine.detectChangeAndSetLine(curLine, w);
634 if (sawModifiedLine) {
635 const int lineCursorColumn =
636 line == cursorLine ? cursorColumn : -1;
637 m_terminal->sendLine(line, curLine, w, lineCursorColumn);
641 m_scrapedLineCount = windowRect.top() + m_scrolledCount;
643 if (showTerminalCursor) {
644 m_terminal->showTerminalCursor(cursorColumn, cursorLine);
650 void Scraper::syncMarkerText(CHAR_INFO (&output)[SYNC_MARKER_LEN])
652 // XXX: The marker text generated here could easily collide with ordinary
653 // console output. Does it make sense to try to avoid the collision?
654 char str[SYNC_MARKER_LEN + 1];
655 winpty_snprintf(str, "S*Y*N*C*%08x", m_syncCounter);
656 for (int i = 0; i < SYNC_MARKER_LEN; ++i) {
657 output[i].Char.UnicodeChar = str[i];
658 output[i].Attributes = 7;
662 int Scraper::findSyncMarker()
664 ASSERT(m_syncRow >= 0);
665 CHAR_INFO marker[SYNC_MARKER_LEN];
666 CHAR_INFO column[BUFFER_LINE_COUNT];
667 syncMarkerText(marker);
668 SmallRect rect(0, 0, 1, m_syncRow + SYNC_MARKER_LEN);
669 m_consoleBuffer->read(rect, column);
671 for (i = m_syncRow; i >= 0; --i) {
673 for (j = 0; j < SYNC_MARKER_LEN; ++j) {
674 if (column[i + j].Char.UnicodeChar != marker[j].Char.UnicodeChar)
677 if (j == SYNC_MARKER_LEN)
683 void Scraper::createSyncMarker(int row)
687 // Clear the lines around the marker to ensure that Windows 10's rewrapping
688 // does not affect the marker.
689 m_consoleBuffer->clearLines(row - 1, SYNC_MARKER_LEN + 1,
690 m_consoleBuffer->bufferInfo());
692 // Write a new marker.
694 CHAR_INFO marker[SYNC_MARKER_LEN];
695 syncMarkerText(marker);
697 SmallRect markerRect(0, m_syncRow, 1, SYNC_MARKER_LEN);
698 m_consoleBuffer->write(markerRect, marker);