/* -*- C++ -*- * This file is part of Zutty. * Copyright (C) 2020 Tom Szilagyi * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * See the file LICENSE for the full license. */ #include "log.h"#include "pty.h" #include <algorithm>#include <sstream> // for the debug/step facility:#include <chrono>#include <thread>#include <sys/types.h>#include <signal.h> namespace zutty{#ifdef DEBUG inline void Vterm::debugKey () { switch (debugStep) { case 0: debugStep = 1; break; case 1: debugStep = 10; break; case 10: debugStep = 100; break; case 100: debugStep = 0; break; } debugCnt = debugStep; logT << "*** DEBUG step=" << debugStep << std::endl; } inline void Vterm::debugBreak () { if (!debugStep || --debugCnt > 0) return; debugCnt = debugStep; logT << "*** DEBUG STOP (step=" << debugStep << "), " << readPos + 1 - lastStopPos << " bytes since last:\n " << dumpBuffer (inputBuf + lastStopPos, inputBuf + readPos + 1); lastStopPos = readPos + 1; logT << "Issue 'kill -CONT " << getpid () << "' or 'fg' to continue." << std::endl; redraw (); using namespace std::chrono_literals; std::this_thread::sleep_for (100ms); kill (getpid (), SIGSTOP); } #define DEBUG_BREAK debugBreak() #define TRACE_FUN \ do { \ logT << __FUNCTION__ << " ["; \ for (size_t k = 0; k < nInputOps; ++k) { \ if (k) { \ vlog << ";"; \ } \ vlog << inputOps [k]; \ } \ vlog << "] \t" \ << "p(" << posY << "," << posX << ") " \ << "d(" << nRows << "," << nCols << ") " \ << "mgn[" << marginTop << "," << marginBottom << ") " \ << "hmgn:" << horizMarginMode \ << " [" << hMargin << "," << nColsEff << ")" \ << std::endl; \ } while (0)#else #define DEBUG_BREAK #define TRACE_FUN#endif // DEBUG inline void Vterm::unhandledInput (unsigned char ch) { logE << "Unhandled input char '" << ch << "' (" << (int)ch << ") in state " << strInputState (inputState) << ". Escape sequence so far: " << dumpBuffer (inputBuf + lastEscBegin, inputBuf + readPos + 1); setState (InputState::Normal); } inline void Vterm::traceNormalInput () { #ifdef DEBUG if (lastNormalBegin < readPos) { auto dumpbufs = dumpBuffer (inputBuf + lastNormalBegin, inputBuf + readPos); if (dumpbufs.length ()) { logT << "Inserted: " << dumpbufs; } } lastNormalBegin = readPos + 1; #endif // DEBUG } inline void Vterm::redraw () { onRefresh (* cf); cf->resetDamage (); } inline const MouseTrackingState& Vterm::getMouseTrackingState () const { return mouseTrk; } inline void Vterm::setHasFocus (bool hasFocus_) { hasFocus = hasFocus_; showCursor (); redraw (); } inline void Vterm::pageUp () { if (altScrollMode && altScreenBufferMode) { inputOps [0] = 1; nInputOps = 1; for (int k = 0; k < (marginBottom - marginTop) / 2; ++k) writePty (VtKey::Up); } else { cf->pageUp (nRows / 2); redraw (); } } inline void Vterm::pageDown () { if (altScrollMode && altScreenBufferMode) { inputOps [0] = 1; nInputOps = 1; for (int k = 0; k < (marginBottom - marginTop) / 2; ++k) writePty (VtKey::Down); } else { cf->pageDown (nRows / 2); redraw (); } } inline void Vterm::mouseWheelUp () { if (altScrollMode && altScreenBufferMode) { inputOps [0] = 1; nInputOps = 1; writePty (VtKey::Up); } else { cf->pageUp (5); redraw (); } } inline void Vterm::mouseWheelDown () { if (altScrollMode && altScreenBufferMode) { inputOps [0] = 1; nInputOps = 1; writePty (VtKey::Down); } else { cf->pageDown (5); redraw (); } } inline void Vterm::resetTerminal () { resetScreen (); resetAttrs (); switchColMode (ColMode::C80); cf->dropScrollbackHistory (); cf->resetMargins (marginTop, marginBottom); clearScreen (); switchScreenBufferMode (false); altScrollMode = opts.altScrollMode; altSendsEscape = opts.altSendsEscape; modifyOtherKeys = opts.modifyOtherKeys; horizMarginMode = false; hMargin = 0; nColsEff = nCols; setState (InputState::Normal); if (haveOscHandler) { // reset title argBuf.clear (); argBuf.push_back ('0'); argBuf.push_back (';'); for (const char* p = opts.title; *p != '\0'; ++p) argBuf.push_back (*p); handle_OSC (); } } inline void Vterm::resetScreen () { showCursorMode = true; cursorStyle = CharVdev::Cursor::Style::filled_block; autoWrapMode = true; autoNewlineMode = false; keyboardLocked = false; insertMode = false; bkspSendsDel = true; localEcho = false; bracketedPasteMode = false; compatLevel = CompatibilityLevel::VT400; cursorKeyMode = CursorKeyMode::ANSI; keypadMode = KeypadMode::Normal; originMode = OriginMode::Absolute; charsetState = CharsetState {}; savedCursor_SCO.isSet = false; savedCursor_DEC->isSet = false; mouseTrk = MouseTrackingState {}; tabStops.clear (); cf->getSelection ().clear (); } inline void Vterm::resetAttrs () { reverseVideo = false; fg = &attrs.fg; bg = &attrs.bg; inputOps [0] = 0; nInputOps = 1; csi_SGR (); } inline void Vterm::clearScreen () { posX = 0; posY = 0; lastCol = false; fillScreen (' '); } inline void Vterm::fillScreen (uint16_t ch) { cf->fillCells (ch, attrs); } inline void Vterm::switchColMode (ColMode colMode_) { if (colMode == colMode_) return; resetScreen (); clearScreen (); if (colMode_ == ColMode::C80) { logT << "DECCOLM: Selected 80 columns per line" << std::endl; } else { logT << "DECCOLM: Selected 132 columns per line" << std::endl; } colMode = colMode_; } inline void Vterm::switchScreenBufferMode (bool altScreenBufferMode_) { if (altScreenBufferMode == altScreenBufferMode_) return; if (altScreenBufferMode_) { frame_alt = Frame (winPx, winPy, nCols, nRows, marginTop, marginBottom); cf = &frame_alt; cf->expose (); savedCursor_DEC = &savedCursor_DEC_alt; altScreenBufferMode = true; } else { frame_pri.resize (winPx, winPy, nCols, nRows, marginTop, marginBottom); cf = &frame_pri; cf->expose (); frame_alt.freeCells (); savedCursor_DEC_alt.isSet = false; savedCursor_DEC = &savedCursor_DEC_pri; altScreenBufferMode = false; cf->getSelection ().clear (); } } inline void Vterm::setState (InputState newState) { if (newState == inputState) return; if (newState == InputState::Normal) { DEBUG_BREAK; nInputOps = 0; inputOps [0] = 0; lastNormalBegin = readPos + 1; } else if (inputState == InputState::Normal) { traceNormalInput (); } inputState = newState; } inline bool Vterm::readPty () { static bool first = true; ssize_t n = read (ptyFd, inputBuf, sizeof (inputBuf)); if (n < 0) return true; else if (n == 0) return !first; if (first) { // Mitigate the race condition between shell process startup // and first window size configuration happening in parallel: // the signal could get delivered before the shell is ready // for it, and thus get lost. pty_resize (ptyFd, nCols, nRows); first = false; } logT << "pty read: " << dumpBuffer (inputBuf, inputBuf + n); processInput (inputBuf, n); return false; } inline void Vterm::normalizeCursorPos () { if (nColsEff < posX + 1) posX = nColsEff - 1; if (nRows < posY + 1) posY = nRows - 1; lastCol = false; } inline bool Vterm::isCursorInsideMargins () { return posX >= hMargin && posX < nColsEff && posY >= marginTop && posY < marginBottom; } inline void Vterm::eraseRow (uint16_t pY) { cf->eraseInRow (pY, hMargin, nColsEff - hMargin, attrs); } inline void Vterm::eraseRows (uint16_t startY, uint16_t count) { for (uint16_t pY = startY; pY < startY + count; ++pY) eraseRow (pY); } inline void Vterm::copyRow (uint16_t dstY, uint16_t srcY) { cf->copyRow (dstY, srcY, hMargin, nColsEff - hMargin); } // insert blank rows at and below startY, within the scrolling area inline void Vterm::insertRows (uint16_t startY, uint16_t count) { for (uint16_t pY = marginBottom - count - 1; pY >= startY; --pY) { copyRow (pY + count, pY); if (!pY) break; } for (uint16_t pY = startY; pY < startY + count; ++pY) eraseRow (pY); } // delete rows at and below startY, within the scrolling area inline void Vterm::deleteRows (uint16_t startY, uint16_t count) { for (uint16_t pY = startY; pY < marginBottom - count; ++pY) copyRow (pY, pY + count); for (uint16_t pY = marginBottom - count; pY < marginBottom; ++pY) eraseRow (pY); } // insert blank cols at and to the right of startX, within the scrolling area inline void Vterm::insertCols (uint16_t startX, uint16_t count) { for (uint16_t r = marginTop; r < marginBottom; ++r) { cf->moveInRow (r, startX + count, startX, nColsEff - startX - count); cf->eraseInRow (r, startX, count, attrs); } } // delete cols at and to the right of startX, within the scrolling area inline void Vterm::deleteCols (uint16_t startX, uint16_t count) { for (uint16_t r = marginTop; r < marginBottom; ++r) { cf->moveInRow (r, startX, startX + count, nColsEff - startX - count); cf->eraseInRow (r, nColsEff - count, count, attrs); } } inline void Vterm::inputGraphicChar (unsigned char ch) { if ((ch & 0x80) == 0) { utf8dec.checkPrematureEOS (); // GL range Charset cs; if (charsetState.ss) { cs = charsetState.g [charsetState.ss]; charsetState.ss = 0; } else cs = charsetState.g [charsetState.gl]; if (cs == Charset::UTF8) utf8dec.onUnicode (ch < 127 ? ch : 0); else if (ch >= 32 && (cs == Charset::IsoLatin1 || ch < 127)) utf8dec.onUnicode (charCodes [(uint8_t)cs] [ch - 32]); } else { // GR rangeLines 1–500 of 2022. Load more
/* -*- C++ -*- * This file is part of Zutty. * Copyright (C) 2020 Tom Szilagyi * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * See the file LICENSE for the full license. */ #include "log.h" #include "pty.h" #include <algorithm> #include <sstream> // for the debug/step facility: #include <chrono> #include <thread> #include <sys/types.h> #include <signal.h> namespace zutty { #ifdef DEBUG inline void Vterm::debugKey () { switch (debugStep) { case 0: debugStep = 1; break; case 1: debugStep = 10; break; case 10: debugStep = 100; break; case 100: debugStep = 0; break; } debugCnt = debugStep; logT << "*** DEBUG step=" << debugStep << std::endl; } inline void Vterm::debugBreak () { if (!debugStep || --debugCnt > 0) return; debugCnt = debugStep; logT << "*** DEBUG STOP (step=" << debugStep << "), " << readPos + 1 - lastStopPos << " bytes since last:\n " << dumpBuffer (inputBuf + lastStopPos, inputBuf + readPos + 1); lastStopPos = readPos + 1; logT << "Issue 'kill -CONT " << getpid () << "' or 'fg' to continue." << std::endl; redraw (); using namespace std::chrono_literals; std::this_thread::sleep_for (100ms); kill (getpid (), SIGSTOP); } #define DEBUG_BREAK debugBreak() #define TRACE_FUN \ do { \ logT << __FUNCTION__ << " ["; \ for (size_t k = 0; k < nInputOps; ++k) { \ if (k) { \ vlog << ";"; \ } \ vlog << inputOps [k]; \ } \ vlog << "] \t" \ << "p(" << posY << "," << posX << ") " \ << "d(" << nRows << "," << nCols << ") " \ << "mgn[" << marginTop << "," << marginBottom << ") " \ << "hmgn:" << horizMarginMode \ << " [" << hMargin << "," << nColsEff << ")" \ << std::endl; \ } while (0) #else #define DEBUG_BREAK #define TRACE_FUN #endif // DEBUG inline void Vterm::unhandledInput (unsigned char ch) { logE << "Unhandled input char '" << ch << "' (" << (int)ch << ") in state " << strInputState (inputState) << ". Escape sequence so far: " << dumpBuffer (inputBuf + lastEscBegin, inputBuf + readPos + 1); setState (InputState::Normal); } inline void Vterm::traceNormalInput () { #ifdef DEBUG if (lastNormalBegin < readPos) { auto dumpbufs = dumpBuffer (inputBuf + lastNormalBegin, inputBuf + readPos); if (dumpbufs.length ()) { logT << "Inserted: " << dumpbufs; } } lastNormalBegin = readPos + 1; #endif // DEBUG } inline void Vterm::redraw () { onRefresh (* cf); cf->resetDamage (); } inline const MouseTrackingState& Vterm::getMouseTrackingState () const { return mouseTrk; } inline void Vterm::setHasFocus (bool hasFocus_) { hasFocus = hasFocus_; showCursor (); redraw (); } inline void Vterm::pageUp () { if (altScrollMode && altScreenBufferMode) { inputOps [0] = 1; nInputOps = 1; for (int k = 0; k < (marginBottom - marginTop) / 2; ++k) writePty (VtKey::Up); } else { cf->pageUp (nRows / 2); redraw (); } } inline void Vterm::pageDown () { if (altScrollMode && altScreenBufferMode) { inputOps [0] = 1; nInputOps = 1; for (int k = 0; k < (marginBottom - marginTop) / 2; ++k) writePty (VtKey::Down); } else { cf->pageDown (nRows / 2); redraw (); } } inline void Vterm::mouseWheelUp () { if (altScrollMode && altScreenBufferMode) { inputOps [0] = 1; nInputOps = 1; writePty (VtKey::Up); } else { cf->pageUp (5); redraw (); } } inline void Vterm::mouseWheelDown () { if (altScrollMode && altScreenBufferMode) { inputOps [0] = 1; nInputOps = 1; writePty (VtKey::Down); } else { cf->pageDown (5); redraw (); } } inline void Vterm::resetTerminal () { resetScreen (); resetAttrs (); switchColMode (ColMode::C80); cf->dropScrollbackHistory (); cf->resetMargins (marginTop, marginBottom); clearScreen (); switchScreenBufferMode (false); altScrollMode = opts.altScrollMode; altSendsEscape = opts.altSendsEscape; modifyOtherKeys = opts.modifyOtherKeys; horizMarginMode = false; hMargin = 0; nColsEff = nCols; setState (InputState::Normal); if (haveOscHandler) { // reset title argBuf.clear (); argBuf.push_back ('0'); argBuf.push_back (';'); for (const char* p = opts.title; *p != '\0'; ++p) argBuf.push_back (*p); handle_OSC (); } } inline void Vterm::resetScreen () { showCursorMode = true; cursorStyle = CharVdev::Cursor::Style::filled_block; autoWrapMode = true; autoNewlineMode = false; keyboardLocked = false; insertMode = false; bkspSendsDel = true; localEcho = false; bracketedPasteMode = false; compatLevel = CompatibilityLevel::VT400; cursorKeyMode = CursorKeyMode::ANSI; keypadMode = KeypadMode::Normal; originMode = OriginMode::Absolute; charsetState = CharsetState {}; savedCursor_SCO.isSet = false; savedCursor_DEC->isSet = false; mouseTrk = MouseTrackingState {}; tabStops.clear (); cf->getSelection ().clear (); } inline void Vterm::resetAttrs () { reverseVideo = false; fg = &attrs.fg; bg = &attrs.bg; inputOps [0] = 0; nInputOps = 1; csi_SGR (); } inline void Vterm::clearScreen () { posX = 0; posY = 0; lastCol = false; fillScreen (' '); } inline void Vterm::fillScreen (uint16_t ch) { cf->fillCells (ch, attrs); } inline void Vterm::switchColMode (ColMode colMode_) { if (colMode == colMode_) return; resetScreen (); clearScreen (); if (colMode_ == ColMode::C80) { logT << "DECCOLM: Selected 80 columns per line" << std::endl; } else { logT << "DECCOLM: Selected 132 columns per line" << std::endl; } colMode = colMode_; } inline void Vterm::switchScreenBufferMode (bool altScreenBufferMode_) { if (altScreenBufferMode == altScreenBufferMode_) return; if (altScreenBufferMode_) { frame_alt = Frame (winPx, winPy, nCols, nRows, marginTop, marginBottom); cf = &frame_alt; cf->expose (); savedCursor_DEC = &savedCursor_DEC_alt; altScreenBufferMode = true; } else { frame_pri.resize (winPx, winPy, nCols, nRows, marginTop, marginBottom); cf = &frame_pri; cf->expose (); frame_alt.freeCells (); savedCursor_DEC_alt.isSet = false; savedCursor_DEC = &savedCursor_DEC_pri; altScreenBufferMode = false; cf->getSelection ().clear (); } } inline void Vterm::setState (InputState newState) { if (newState == inputState) return; if (newState == InputState::Normal) { DEBUG_BREAK; nInputOps = 0; inputOps [0] = 0; lastNormalBegin = readPos + 1; } else if (inputState == InputState::Normal) { traceNormalInput (); } inputState = newState; } inline bool Vterm::readPty () { static bool first = true; ssize_t n = read (ptyFd, inputBuf, sizeof (inputBuf)); if (n < 0) return true; else if (n == 0) return !first; if (first) { // Mitigate the race condition between shell process startup // and first window size configuration happening in parallel: // the signal could get delivered before the shell is ready // for it, and thus get lost. pty_resize (ptyFd, nCols, nRows); first = false; } logT << "pty read: " << dumpBuffer (inputBuf, inputBuf + n); processInput (inputBuf, n); return false; } inline void Vterm::normalizeCursorPos () { if (nColsEff < posX + 1) posX = nColsEff - 1; if (nRows < posY + 1) posY = nRows - 1; lastCol = false; } inline bool Vterm::isCursorInsideMargins () { return posX >= hMargin && posX < nColsEff && posY >= marginTop && posY < marginBottom; } inline void Vterm::eraseRow (uint16_t pY) { cf->eraseInRow (pY, hMargin, nColsEff - hMargin, attrs); } inline void Vterm::eraseRows (uint16_t startY, uint16_t count) { for (uint16_t pY = startY; pY < startY + count; ++pY) eraseRow (pY); } inline void Vterm::copyRow (uint16_t dstY, uint16_t srcY) { cf->copyRow (dstY, srcY, hMargin, nColsEff - hMargin); } // insert blank rows at and below startY, within the scrolling area inline void Vterm::insertRows (uint16_t startY, uint16_t count) { for (uint16_t pY = marginBottom - count - 1; pY >= startY; --pY) { copyRow (pY + count, pY); if (!pY) break; } for (uint16_t pY = startY; pY < startY + count; ++pY) eraseRow (pY); } // delete rows at and below startY, within the scrolling area inline void Vterm::deleteRows (uint16_t startY, uint16_t count) { for (uint16_t pY = startY; pY < marginBottom - count; ++pY) copyRow (pY, pY + count); for (uint16_t pY = marginBottom - count; pY < marginBottom; ++pY) eraseRow (pY); } // insert blank cols at and to the right of startX, within the scrolling area inline void Vterm::insertCols (uint16_t startX, uint16_t count) { for (uint16_t r = marginTop; r < marginBottom; ++r) { cf->moveInRow (r, startX + count, startX, nColsEff - startX - count); cf->eraseInRow (r, startX, count, attrs); } } // delete cols at and to the right of startX, within the scrolling area inline void Vterm::deleteCols (uint16_t startX, uint16_t count) { for (uint16_t r = marginTop; r < marginBottom; ++r) { cf->moveInRow (r, startX, startX + count, nColsEff - startX - count); cf->eraseInRow (r, nColsEff - count, count, attrs); } } inline void Vterm::inputGraphicChar (unsigned char ch) { if ((ch & 0x80) == 0) { utf8dec.checkPrematureEOS (); // GL range Charset cs; if (charsetState.ss) { cs = charsetState.g [charsetState.ss]; charsetState.ss = 0; } else cs = charsetState.g [charsetState.gl]; if (cs == Charset::UTF8) utf8dec.onUnicode (ch < 127 ? ch : 0); else if (ch >= 32 && (cs == Charset::IsoLatin1 || ch < 127)) utf8dec.onUnicode (charCodes [(uint8_t)cs] [ch - 32]); } else { // GR range