/* This file is part of Konsole, an X terminal. SPDX-FileCopyrightText: 2007-2008 Robert Knight SPDX-FileCopyrightText: 1997, 1998 Lars Doelle SPDX-License-Identifier: GPL-2.0-or-later This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. */ // Own #include "Screen.h" // Standard #include #include #include #include #include // Qt #include #include // KDE // #include // Konsole #include "TerminalCharacterDecoder.h" #include "konsole_wcwidth.h" using namespace Konsole; // FIXME: this is emulation specific. Use false for xterm, true for ANSI. // FIXME: see if we can get this from terminfo. #define BS_CLEARS false // Macro to convert x,y position on screen to position within an image. // // Originally the image was stored as one large contiguous block of // memory, so a position within the image could be represented as an // offset from the beginning of the block. For efficiency reasons this // is no longer the case. // Many internal parts of this class still use this representation for parameters and so on, // notably moveImage() and clearImage(). // This macro converts from an X,Y position into an image offset. #ifndef loc #define loc(X, Y) ((Y)*columns + (X)) #endif Character Screen::defaultChar = Character(' ', CharacterColor(COLOR_SPACE_DEFAULT, DEFAULT_FORE_COLOR), CharacterColor(COLOR_SPACE_DEFAULT, DEFAULT_BACK_COLOR), DEFAULT_RENDITION); // #define REVERSE_WRAPPED_LINES // for wrapped line debug Screen::Screen(int l, int c) : lines(l) , columns(c) , screenLines(lines + 1) , _scrolledLines(0) , _droppedLines(0) , history(std::make_unique()) , cuX(0) , cuY(0) , currentRendition(0) , _topMargin(0) , _bottomMargin(0) , selBegin(0) , selTopLeft(0) , selBottomRight(0) , blockSelectionMode(false) , effectiveForeground(CharacterColor()) , effectiveBackground(CharacterColor()) , effectiveRendition(0) , lastPos(-1) { lineProperties.resize(lines + 1); for (int i = 0; i < lines + 1; i++) lineProperties[i] = LINE_DEFAULT; initTabStops(); clearSelection(); reset(); } /*! Destructor */ Screen::~Screen() { } void Screen::cursorUp(int n) //=CUU { if (n == 0) n = 1; // Default int stop = cuY < _topMargin ? 0 : _topMargin; cuX = qMin(columns - 1, cuX); // nowrap! cuY = qMax(stop, cuY - n); } void Screen::cursorDown(int n) //=CUD { if (n == 0) n = 1; // Default int stop = cuY > _bottomMargin ? lines - 1 : _bottomMargin; cuX = qMin(columns - 1, cuX); // nowrap! cuY = qMin(stop, cuY + n); } void Screen::cursorLeft(int n) //=CUB { if (n == 0) n = 1; // Default cuX = qMin(columns - 1, cuX); // nowrap! cuX = qMax(0, cuX - n); } void Screen::cursorRight(int n) //=CUF { if (n == 0) n = 1; // Default cuX = qMin(columns - 1, cuX + n); } void Screen::setMargins(int top, int bot) //=STBM { if (top == 0) top = 1; // Default if (bot == 0) bot = lines; // Default top = top - 1; // Adjust to internal lineno bot = bot - 1; // Adjust to internal lineno if (!(0 <= top && top < bot && bot < lines)) { // Debug()<<" setRegion("< 0) cuY -= 1; } void Screen::nextLine() //=NEL { toStartOfLine(); index(); } void Screen::eraseChars(int n) { if (n == 0) n = 1; // Default int p = qMax(0, qMin(cuX + n - 1, columns - 1)); clearImage(loc(cuX, cuY), loc(p, cuY), ' '); } void Screen::deleteChars(int n) { Q_ASSERT(n >= 0); // always delete at least one char if (n == 0) n = 1; // if cursor is beyond the end of the line there is nothing to do if (cuX >= screenLines[cuY].size()) return; if (cuX + n > screenLines[cuY].size()) n = screenLines[cuY].size() - cuX; Q_ASSERT(n >= 0); Q_ASSERT(cuX + n <= screenLines[cuY].size()); screenLines[cuY].remove(cuX, n); } void Screen::insertChars(int n) { if (n == 0) n = 1; // Default if (screenLines[cuY].size() < cuX) screenLines[cuY].resize(cuX); screenLines[cuY].insert(cuX, n, ' '); if (screenLines[cuY].size() > columns) screenLines[cuY].resize(columns); } void Screen::repeatChars(int count) //=REP { if (count == 0) { count = 1; } /** * From ECMA-48 version 5, section 8.3.103 * If the character preceding REP is a control function or part of a * control function, the effect of REP is not defined by this Standard. * * So, a "normal" program should always use REP immediately after a visible * character (those other than escape sequences). So, lastDrawnChar can be * safely used. */ for (int i = 0; i < count; i++) { displayCharacter(lastDrawnChar); } } void Screen::deleteLines(int n) { if (n == 0) n = 1; // Default scrollUp(cuY, n); } void Screen::insertLines(int n) { if (n == 0) n = 1; // Default scrollDown(cuY, n); } void Screen::setMode(int m) { currentModes[m] = true; switch (m) { case MODE_Origin: cuX = 0; cuY = _topMargin; break; // FIXME: home } } void Screen::resetMode(int m) { currentModes[m] = false; switch (m) { case MODE_Origin: cuX = 0; cuY = 0; break; // FIXME: home } } void Screen::saveMode(int m) { savedModes[m] = currentModes[m]; } void Screen::restoreMode(int m) { currentModes[m] = savedModes[m]; } bool Screen::getMode(int m) const { return currentModes[m]; } void Screen::saveCursor() { savedState.cursorColumn = cuX; savedState.cursorLine = cuY; savedState.rendition = currentRendition; savedState.foreground = currentForeground; savedState.background = currentBackground; } void Screen::restoreCursor() { cuX = qMin(savedState.cursorColumn, columns - 1); cuY = qMin(savedState.cursorLine, lines - 1); currentRendition = savedState.rendition; currentForeground = savedState.foreground; currentBackground = savedState.background; updateEffectiveRendition(); } void Screen::resizeImage(int new_lines, int new_columns) { if ((new_lines == lines) && (new_columns == columns)) return; if (cuY > new_lines - 1) { // attempt to preserve focus and lines _bottomMargin = lines - 1; // FIXME: margin lost for (int i = 0; i < cuY - (new_lines - 1); i++) { addHistLine(); scrollUp(0, 1); } } // create new screen lines and copy from old to new auto newScreenLines = QVector(new_lines + 1); for (int i = 0; i < qMin(lines, new_lines + 1); i++) newScreenLines[i] = screenLines[i]; for (int i = lines; (i > 0) && (i < new_lines + 1); i++) newScreenLines[i].resize(new_columns); lineProperties.resize(new_lines + 1); for (int i = lines; (i > 0) && (i < new_lines + 1); i++) lineProperties[i] = LINE_DEFAULT; clearSelection(); screenLines.clear(); screenLines = std::move(newScreenLines); lines = new_lines; columns = new_columns; cuX = qMin(cuX, columns - 1); cuY = qMin(cuY, lines - 1); // FIXME: try to keep values, evtl. _topMargin = 0; _bottomMargin = lines - 1; initTabStops(); clearSelection(); } void Screen::setDefaultMargins() { _topMargin = 0; _bottomMargin = lines - 1; } /* Clarifying rendition here and in the display. currently, the display's color table is 0 1 2 .. 9 10 .. 17 dft_fg, dft_bg, dim 0..7, intensive 0..7 currentForeground, currentBackground contain values 0..8; - 0 = default color - 1..8 = ansi specified color re_fg, re_bg contain values 0..17 due to the TerminalDisplay's color table rendition attributes are attr widget screen -------------- ------ ------ RE_UNDERLINE XX XX affects foreground only RE_BLINK XX XX affects foreground only RE_BOLD XX XX affects foreground only RE_REVERSE -- XX RE_TRANSPARENT XX -- affects background only RE_INTENSIVE XX -- affects foreground only Note that RE_BOLD is used in both widget and screen rendition. Since xterm/vt102 is to poor to distinguish between bold (which is a font attribute) and intensive (which is a color attribute), we translate this and RE_BOLD in falls eventually appart into RE_BOLD and RE_INTENSIVE. */ void Screen::reverseRendition(Character &p) const { CharacterColor f = p.foregroundColor; CharacterColor b = p.backgroundColor; p.foregroundColor = b; p.backgroundColor = f; // p->r &= ~RE_TRANSPARENT; } void Screen::updateEffectiveRendition() { effectiveRendition = currentRendition; if (currentRendition & RE_REVERSE) { effectiveForeground = currentBackground; effectiveBackground = currentForeground; } else { effectiveForeground = currentForeground; effectiveBackground = currentBackground; } if (currentRendition & RE_BOLD) effectiveForeground.setIntensive(); } void Screen::copyFromHistory(std::span dest, int startLine, int count) const { Q_ASSERT(startLine >= 0 && count > 0 && startLine + count <= history->getLines()); for (int line = startLine; line < startLine + count; line++) { const int length = qMin(columns, history->getLineLen(line)); const int destLineOffset = (line - startLine) * columns; history->getCells(line, 0, length, dest.subspan(destLineOffset)); for (int column = length; column < columns; column++) dest[destLineOffset + column] = defaultChar; // invert selected text if (selBegin != -1) { for (int column = 0; column < columns; column++) { if (isSelected(column, line)) { reverseRendition(dest[destLineOffset + column]); } } } } } void Screen::copyFromScreen(std::span dest, int startLine, int count) const { Q_ASSERT(startLine >= 0 && count > 0 && startLine + count <= lines); for (int line = startLine; line < (startLine + count); line++) { int srcLineStartIndex = line * columns; int destLineStartIndex = (line - startLine) * columns; for (int column = 0; column < columns; column++) { int srcIndex = srcLineStartIndex + column; int destIndex = destLineStartIndex + column; dest[destIndex] = screenLines[srcIndex / columns].value(srcIndex % columns, defaultChar); // invert selected text if (selBegin != -1 && isSelected(column, line + history->getLines())) reverseRendition(dest[destIndex]); } } } void Screen::getImage(std::span dest, int size, int startLine, int endLine) const { Q_ASSERT(startLine >= 0); Q_ASSERT(endLine >= startLine && endLine < history->getLines() + lines); const int mergedLines = endLine - startLine + 1; Q_ASSERT(size >= mergedLines * columns); Q_UNUSED(size); const int linesInHistoryBuffer = qBound(0, history->getLines() - startLine, mergedLines); const int linesInScreenBuffer = mergedLines - linesInHistoryBuffer; // copy lines from history buffer if (linesInHistoryBuffer > 0) copyFromHistory(dest, startLine, linesInHistoryBuffer); // copy lines from screen buffer if (linesInScreenBuffer > 0) copyFromScreen(dest.subspan(linesInHistoryBuffer * columns), startLine + linesInHistoryBuffer - history->getLines(), linesInScreenBuffer); // invert display when in screen mode if (getMode(MODE_Screen)) { for (int i = 0; i < mergedLines * columns; i++) reverseRendition(dest[i]); // for reverse display } // mark the character at the current cursor position int cursorIndex = loc(cuX, cuY + linesInHistoryBuffer); if (getMode(MODE_Cursor) && cursorIndex < columns * mergedLines) dest[cursorIndex].rendition |= RE_CURSOR; } QVector Screen::getLineProperties(int startLine, int endLine) const { Q_ASSERT(startLine >= 0); Q_ASSERT(endLine >= startLine && endLine < history->getLines() + lines); const int mergedLines = endLine - startLine + 1; const int linesInHistory = qBound(0, history->getLines() - startLine, mergedLines); const int linesInScreen = mergedLines - linesInHistory; QVector result(mergedLines); int index = 0; // copy properties for lines in history for (int line = startLine; line < startLine + linesInHistory; line++) { // TODO Support for line properties other than wrapped lines if (history->isWrappedLine(line)) { result[index] = (LineProperty)(result[index] | LINE_WRAPPED); } index++; } // copy properties for lines in screen buffer const int firstScreenLine = startLine + linesInHistory - history->getLines(); for (int line = firstScreenLine; line < firstScreenLine + linesInScreen; line++) { result[index] = lineProperties[line]; index++; } return result; } void Screen::reset(bool clearScreen) { setMode(MODE_Wrap); saveMode(MODE_Wrap); // wrap at end of margin resetMode(MODE_Origin); saveMode(MODE_Origin); // position refere to [1,1] resetMode(MODE_Insert); saveMode(MODE_Insert); // overstroke setMode(MODE_Cursor); // cursor visible resetMode(MODE_Screen); // screen not inverse resetMode(MODE_NewLine); _topMargin = 0; _bottomMargin = lines - 1; setDefaultRendition(); saveCursor(); if (clearScreen) clear(); } void Screen::clear() { clearEntireScreen(); home(); } void Screen::backspace() { cuX = qMin(columns - 1, cuX); // nowrap! cuX = qMax(0, cuX - 1); if (screenLines[cuY].size() < cuX + 1) screenLines[cuY].resize(cuX + 1); if (BS_CLEARS) screenLines[cuY][cuX].character = u' '; } void Screen::tab(int n) { // note that TAB is a format effector (does not write ' '); if (n == 0) n = 1; while ((n > 0) && (cuX < columns - 1)) { cursorRight(1); while ((cuX < columns - 1) && !tabStops[cuX]) cursorRight(1); n--; } } void Screen::backtab(int n) { // note that TAB is a format effector (does not write ' '); if (n == 0) n = 1; while ((n > 0) && (cuX > 0)) { cursorLeft(1); while ((cuX > 0) && !tabStops[cuX]) cursorLeft(1); n--; } } void Screen::clearTabStops() { for (int i = 0; i < columns; i++) tabStops[i] = false; } void Screen::changeTabStop(bool set) { if (cuX >= columns) return; tabStops[cuX] = set; } void Screen::initTabStops() { tabStops.resize(columns); // Arrg! The 1st tabstop has to be one longer than the other. // i.e. the kids start counting from 0 instead of 1. // Other programs might behave correctly. Be aware. for (int i = 0; i < columns; i++) tabStops[i] = (i % 8 == 0 && i != 0); } void Screen::newLine() { if (getMode(MODE_NewLine)) toStartOfLine(); index(); } void Screen::checkSelection(int from, int to) { if (selBegin == -1) return; int scr_TL = loc(0, history->getLines()); // Clear entire selection if it overlaps region [from, to] if ((selBottomRight >= (from + scr_TL)) && (selTopLeft <= (to + scr_TL))) clearSelection(); } void Screen::displayCharacter(QChar c) { // Note that VT100 does wrapping BEFORE putting the character. // This has impact on the assumption of valid cursor positions. // We indicate the fact that a newline has to be triggered by // putting the cursor one right to the last column of the screen. int w = konsole_wcwidth(c); if (w <= 0) return; if (cuX + w > columns) { if (getMode(MODE_Wrap)) { lineProperties[cuY] = (LineProperty)(lineProperties[cuY] | LINE_WRAPPED); nextLine(); } else cuX = columns - w; } // ensure current line vector has enough elements int size = screenLines[cuY].size(); if (size < cuX + w) { screenLines[cuY].resize(cuX + w); } if (getMode(MODE_Insert)) insertChars(w); lastPos = loc(cuX, cuY); // check if selection is still valid. checkSelection(lastPos, lastPos); Character ¤tChar = screenLines[cuY][cuX]; currentChar.character = c; currentChar.foregroundColor = effectiveForeground; currentChar.backgroundColor = effectiveBackground; currentChar.rendition = effectiveRendition; lastDrawnChar = c; int i = 0; int newCursorX = cuX + w--; while (w) { i++; if (screenLines[cuY].size() < cuX + i + 1) screenLines[cuY].resize(cuX + i + 1); Character &ch = screenLines[cuY][cuX + i]; ch.character = QChar(0); ch.foregroundColor = effectiveForeground; ch.backgroundColor = effectiveBackground; ch.rendition = effectiveRendition; w--; } cuX = newCursorX; } void Screen::compose(const QString & /*compose*/) { Q_ASSERT(0 /*Not implemented yet*/); /* if (lastPos == -1) return; QChar c(image[lastPos].character); compose.prepend(c); //compose.compose(); ### FIXME! image[lastPos].character = compose[0].unicode();*/ } int Screen::scrolledLines() const { return _scrolledLines; } int Screen::droppedLines() const { return _droppedLines; } void Screen::resetDroppedLines() { _droppedLines = 0; } void Screen::resetScrolledLines() { _scrolledLines = 0; } void Screen::scrollUp(int n) { if (n == 0) n = 1; // Default if (_topMargin == 0) addHistLine(); // history.history scrollUp(_topMargin, n); } QRect Screen::lastScrolledRegion() const { return _lastScrolledRegion; } void Screen::scrollUp(int from, int n) { if (n <= 0) return; if (from > _bottomMargin) return; if (from + n > _bottomMargin) n = _bottomMargin + 1 - from; _scrolledLines -= n; _lastScrolledRegion = QRect(0, _topMargin, columns - 1, (_bottomMargin - _topMargin)); // FIXME: make sure `topMargin', `bottomMargin', `from', `n' is in bounds. moveImage(loc(0, from), loc(0, from + n), loc(columns, _bottomMargin)); clearImage(loc(0, _bottomMargin - n + 1), loc(columns - 1, _bottomMargin), ' '); } void Screen::scrollDown(int n) { if (n == 0) n = 1; // Default scrollDown(_topMargin, n); } void Screen::scrollDown(int from, int n) { _scrolledLines += n; // FIXME: make sure `topMargin', `bottomMargin', `from', `n' is in bounds. if (n <= 0) return; if (from > _bottomMargin) return; if (from + n > _bottomMargin) n = _bottomMargin - from; moveImage(loc(0, from + n), loc(0, from), loc(columns - 1, _bottomMargin - n)); clearImage(loc(0, from), loc(columns - 1, from + n - 1), ' '); } void Screen::setCursorYX(int y, int x) { setCursorY(y); setCursorX(x); } void Screen::setCursorX(int x) { if (x == 0) x = 1; // Default x -= 1; // Adjust cuX = qMax(0, qMin(columns - 1, x)); } void Screen::setCursorY(int y) { if (y == 0) y = 1; // Default y -= 1; // Adjust cuY = qMax(0, qMin(lines - 1, y + (getMode(MODE_Origin) ? _topMargin : 0))); } void Screen::home() { cuX = 0; cuY = 0; } void Screen::toStartOfLine() { cuX = 0; } int Screen::getCursorX() const { return cuX; } int Screen::getCursorY() const { return cuY; } void Screen::clearImage(int loca, int loce, char c) { int scr_TL = loc(0, history->getLines()); // FIXME: check positions // Clear entire selection if it overlaps region to be moved... if ((selBottomRight > (loca + scr_TL)) && (selTopLeft < (loce + scr_TL))) { clearSelection(); } int topLine = loca / columns; int bottomLine = loce / columns; Character clearCh(c, currentForeground, currentBackground, DEFAULT_RENDITION); // if the character being used to clear the area is the same as the // default character, the affected lines can simply be shrunk. bool isDefaultCh = (clearCh == Character()); for (int y = topLine; y <= bottomLine; y++) { lineProperties[y] = 0; int endCol = (y == bottomLine) ? loce % columns : columns - 1; int startCol = (y == topLine) ? loca % columns : 0; QVector &line = screenLines[y]; if (isDefaultCh && endCol == columns - 1) { line.resize(startCol); } else { if (line.size() < endCol + 1) line.resize(endCol + 1); Character *data = line.data(); for (int i = startCol; i <= endCol; i++) data[i] = clearCh; } } } void Screen::moveImage(int dest, int sourceBegin, int sourceEnd) { Q_ASSERT(sourceBegin <= sourceEnd); int lines = (sourceEnd - sourceBegin) / columns; // move screen image and line properties: // the source and destination areas of the image may overlap, // so it matters that we do the copy in the right order - // forwards if dest < sourceBegin or backwards otherwise. //(search the web for 'memmove implementation' for details) if (dest < sourceBegin) { for (int i = 0; i <= lines; i++) { screenLines[(dest / columns) + i] = screenLines[(sourceBegin / columns) + i]; lineProperties[(dest / columns) + i] = lineProperties[(sourceBegin / columns) + i]; } } else { for (int i = lines; i >= 0; i--) { screenLines[(dest / columns) + i] = screenLines[(sourceBegin / columns) + i]; lineProperties[(dest / columns) + i] = lineProperties[(sourceBegin / columns) + i]; } } if (lastPos != -1) { int diff = dest - sourceBegin; // Scroll by this amount lastPos += diff; if ((lastPos < 0) || (lastPos >= (lines * columns))) lastPos = -1; } // Adjust selection to follow scroll. if (selBegin != -1) { bool beginIsTL = (selBegin == selTopLeft); int diff = dest - sourceBegin; // Scroll by this amount int scr_TL = loc(0, history->getLines()); int srca = sourceBegin + scr_TL; // Translate index from screen to global int srce = sourceEnd + scr_TL; // Translate index from screen to global int desta = srca + diff; int deste = srce + diff; if ((selTopLeft >= srca) && (selTopLeft <= srce)) selTopLeft += diff; else if ((selTopLeft >= desta) && (selTopLeft <= deste)) selBottomRight = -1; // Clear selection (see below) if ((selBottomRight >= srca) && (selBottomRight <= srce)) selBottomRight += diff; else if ((selBottomRight >= desta) && (selBottomRight <= deste)) selBottomRight = -1; // Clear selection (see below) if (selBottomRight < 0) { clearSelection(); } else { if (selTopLeft < 0) selTopLeft = 0; } if (beginIsTL) selBegin = selTopLeft; else selBegin = selBottomRight; } } void Screen::clearToEndOfScreen() { clearImage(loc(cuX, cuY), loc(columns - 1, lines - 1), ' '); } void Screen::clearToBeginOfScreen() { clearImage(loc(0, 0), loc(cuX, cuY), ' '); } void Screen::clearEntireScreen() { // Add entire screen to history for (int i = 0; i < (lines - 1); i++) { addHistLine(); scrollUp(0, 1); } clearImage(loc(0, 0), loc(columns - 1, lines - 1), ' '); } /*! fill screen with 'E' This is to aid screen alignment */ void Screen::helpAlign() { clearImage(loc(0, 0), loc(columns - 1, lines - 1), 'E'); } void Screen::clearToEndOfLine() { clearImage(loc(cuX, cuY), loc(columns - 1, cuY), ' '); } void Screen::clearToBeginOfLine() { clearImage(loc(0, cuY), loc(cuX, cuY), ' '); } void Screen::clearEntireLine() { clearImage(loc(0, cuY), loc(columns - 1, cuY), ' '); } void Screen::setRendition(int re) { currentRendition |= re; updateEffectiveRendition(); } void Screen::resetRendition(int re) { currentRendition &= ~re; updateEffectiveRendition(); } void Screen::setDefaultRendition() { setForeColor(COLOR_SPACE_DEFAULT, DEFAULT_FORE_COLOR); setBackColor(COLOR_SPACE_DEFAULT, DEFAULT_BACK_COLOR); currentRendition = DEFAULT_RENDITION; updateEffectiveRendition(); } void Screen::setForeColor(int space, int color) { currentForeground = CharacterColor(space, color); if (currentForeground.isValid()) updateEffectiveRendition(); else setForeColor(COLOR_SPACE_DEFAULT, DEFAULT_FORE_COLOR); } void Screen::setBackColor(int space, int color) { currentBackground = CharacterColor(space, color); if (currentBackground.isValid()) updateEffectiveRendition(); else setBackColor(COLOR_SPACE_DEFAULT, DEFAULT_BACK_COLOR); } void Screen::clearSelection() { selBottomRight = -1; selTopLeft = -1; selBegin = -1; } void Screen::getSelectionStart(int &column, int &line) const { if (selTopLeft != -1) { column = selTopLeft % columns; line = selTopLeft / columns; } else { column = cuX + getHistLines(); line = cuY + getHistLines(); } } void Screen::getSelectionEnd(int &column, int &line) const { if (selBottomRight != -1) { column = selBottomRight % columns; line = selBottomRight / columns; } else { column = cuX + getHistLines(); line = cuY + getHistLines(); } } void Screen::setSelectionStart(const int x, const int y, const bool mode) { selBegin = loc(x, y); /* FIXME, HACK to correct for x too far to the right... */ if (x == columns) selBegin--; selBottomRight = selBegin; selTopLeft = selBegin; blockSelectionMode = mode; } void Screen::setSelectionEnd(const int x, const int y) { if (selBegin == -1) return; int endPos = loc(x, y); if (endPos < selBegin) { selTopLeft = endPos; selBottomRight = selBegin; } else { /* FIXME, HACK to correct for x too far to the right... */ if (x == columns) endPos--; selTopLeft = selBegin; selBottomRight = endPos; } // Normalize the selection in column mode if (blockSelectionMode) { int topRow = selTopLeft / columns; int topColumn = selTopLeft % columns; int bottomRow = selBottomRight / columns; int bottomColumn = selBottomRight % columns; selTopLeft = loc(qMin(topColumn, bottomColumn), topRow); selBottomRight = loc(qMax(topColumn, bottomColumn), bottomRow); } } bool Screen::isSelected(const int x, const int y) const { bool columnInSelection = true; if (blockSelectionMode) { columnInSelection = x >= (selTopLeft % columns) && x <= (selBottomRight % columns); } int pos = loc(x, y); return pos >= selTopLeft && pos <= selBottomRight && columnInSelection; } QString Screen::selectedText(bool preserveLineBreaks) const { QString result; QTextStream stream(&result, QIODevice::ReadWrite); PlainTextDecoder decoder; decoder.begin(&stream); writeSelectionToStream(&decoder, preserveLineBreaks); decoder.end(); return result; } bool Screen::isSelectionValid() const { return selTopLeft >= 0 && selBottomRight >= 0; } void Screen::writeSelectionToStream(TerminalCharacterDecoder *decoder, bool preserveLineBreaks) const { if (!isSelectionValid()) return; writeToStream(decoder, selTopLeft, selBottomRight, preserveLineBreaks); } void Screen::writeToStream(TerminalCharacterDecoder *decoder, int startIndex, int endIndex, bool preserveLineBreaks) const { int top = startIndex / columns; int left = startIndex % columns; int bottom = endIndex / columns; int right = endIndex % columns; Q_ASSERT(top >= 0 && left >= 0 && bottom >= 0 && right >= 0); for (int y = top; y <= bottom; y++) { int start = 0; if (y == top || blockSelectionMode) start = left; int count = -1; if (y == bottom || blockSelectionMode) count = right - start + 1; const bool appendNewLine = (y != bottom); int copied = copyLineToStream(y, start, count, decoder, appendNewLine, preserveLineBreaks); // if the selection goes beyond the end of the last line then // append a new line character. // // this makes it possible to 'select' a trailing new line character after // the text on a line. if (y == bottom && copied < count) { Character newLineChar('\n'); decoder->decodeLine(std::span(&newLineChar, 1), 0); } } } int Screen::copyLineToStream(int line, int start, int count, TerminalCharacterDecoder *decoder, bool appendNewLine, bool preserveLineBreaks) const { // buffer to hold characters for decoding // the buffer is static to avoid initialising every // element on each call to copyLineToStream //(which is unnecessary since all elements will be overwritten anyway) static const int MAX_CHARS = 1024; static std::array characterBuffer; Q_ASSERT(count < MAX_CHARS); LineProperty currentLineProperties = 0; // determine if the line is in the history buffer or the screen image if (line < history->getLines()) { const int lineLength = history->getLineLen(line); // ensure that start position is before end of line start = qMin(start, qMax(0, lineLength - 1)); // retrieve line from history buffer. It is assumed // that the history buffer does not store trailing white space // at the end of the line, so it does not need to be trimmed here if (count == -1) { count = lineLength - start; } else { count = qMin(start + count, lineLength) - start; } // safety checks Q_ASSERT(start >= 0); Q_ASSERT(count >= 0); Q_ASSERT((start + count) <= history->getLineLen(line)); history->getCells(line, start, count, characterBuffer); if (history->isWrappedLine(line)) currentLineProperties |= LINE_WRAPPED; } else { if (count == -1) count = columns - start; Q_ASSERT(count >= 0); const int screenLine = line - history->getLines(); auto data = screenLines[screenLine].data(); int length = screenLines[screenLine].size(); // retrieve line from screen image for (int i = start; i < qMin(start + count, length); i++) { characterBuffer[i - start] = data[i]; } // count cannot be any greater than length count = qBound(0, length - start, count); Q_ASSERT(screenLine < lineProperties.size()); currentLineProperties |= lineProperties[screenLine]; } // add new line character at end const bool omitLineBreak = (currentLineProperties & LINE_WRAPPED) || !preserveLineBreaks; if (!omitLineBreak && appendNewLine && (count + 1 < MAX_CHARS)) { characterBuffer[count] = '\n'; count++; } // decode line and write to text stream decoder->decodeLine(std::span(characterBuffer).subspan(0, count), currentLineProperties); return count; } void Screen::writeLinesToStream(TerminalCharacterDecoder *decoder, int fromLine, int toLine) const { writeToStream(decoder, loc(0, fromLine), loc(columns - 1, toLine)); } void Screen::addHistLine() { // add line to history buffer // we have to take care about scrolling, too... if (hasScroll()) { int oldHistLines = history->getLines(); history->addCellsVector(screenLines[0]); history->addLine(lineProperties[0] & LINE_WRAPPED); int newHistLines = history->getLines(); bool beginIsTL = (selBegin == selTopLeft); // If the history is full, increment the count // of dropped lines if (newHistLines == oldHistLines) _droppedLines++; // Adjust selection for the new point of reference if (newHistLines > oldHistLines) { if (selBegin != -1) { selTopLeft += columns; selBottomRight += columns; } } if (selBegin != -1) { // Scroll selection in history up int top_BR = loc(0, 1 + newHistLines); if (selTopLeft < top_BR) selTopLeft -= columns; if (selBottomRight < top_BR) selBottomRight -= columns; if (selBottomRight < 0) clearSelection(); else { if (selTopLeft < 0) selTopLeft = 0; } if (beginIsTL) selBegin = selTopLeft; else selBegin = selBottomRight; } } } int Screen::getHistLines() const { return history->getLines(); } void Screen::setScroll(const HistoryType &t, bool copyPreviousScroll) { clearSelection(); if (copyPreviousScroll) history = t.scroll(std::move(history)); else { auto oldScroll = std::move(history); history = t.scroll(nullptr); } } bool Screen::hasScroll() const { return history->hasScroll(); } const HistoryType &Screen::getScroll() const { return history->getType(); } void Screen::setLineProperty(LineProperty property, bool enable) { if (enable) lineProperties[cuY] = (LineProperty)(lineProperties[cuY] | property); else lineProperties[cuY] = (LineProperty)(lineProperties[cuY] & ~property); } void Screen::fillWithDefaultChar(std::span dest, int count) { for (int i = 0; i < count; i++) dest[i] = defaultChar; }