From 4df1e3c17c631296a9034c533863cf26a2f7a3f9 Mon Sep 17 00:00:00 2001 From: magiblot Date: Sun, 1 Oct 2023 13:24:19 +0200 Subject: [PATCH] platform: find a universal solution for consuming unprocessed input The problem that we fixed for far2l (#49) can be also reproduced with Conpty and win32-input-mode. I figured out that we can get any terminal to reply to us with a Device Status Report. So use that instead of relying on the far2l protocol. This commit also fixes a bug when consuming unprocessed input. With a slow connection, we would fail to read the terminal response, which is why we were forced to add a timeout in 08d83577d47f65b8c9d5d05d965deb206ff39648. This was caused by calling NcursesInput::getEvent, which would at times interpret the response sequence as an Alt+Key combination. In order to prevent confusion when debugging, wgetch is now only called directly from NcursesInputGetter. --- include/tvision/internal/far2l.h | 5 --- include/tvision/internal/ncursinp.h | 23 +++++++--- include/tvision/internal/terminal.h | 6 ++- source/platform/far2l.cpp | 26 ----------- source/platform/ncursinp.cpp | 67 ++++++++++------------------- source/platform/terminal.cpp | 57 +++++++++++++++++++----- 6 files changed, 89 insertions(+), 95 deletions(-) diff --git a/include/tvision/internal/far2l.h b/include/tvision/internal/far2l.h index 1eae5da1..b18ac83c 100644 --- a/include/tvision/internal/far2l.h +++ b/include/tvision/internal/far2l.h @@ -10,9 +10,6 @@ class EventSource; #define far2lEnableSeq "\x1B_far2l1\x1B\\" #define far2lDisableSeq "\x1B_far2l0\x1B\\" -#define far2lPingSeq "\x1B_far2l:FAR=\x1B\\" - -enum { pingTimeout = 200 }; ParseResult parseFar2lAnswer(GetChBuf &, TEvent &, InputState &) noexcept; ParseResult parseFar2lInput(GetChBuf &, TEvent &, InputState &) noexcept; @@ -20,8 +17,6 @@ ParseResult parseFar2lInput(GetChBuf &, TEvent &, InputState &) noexcept; bool setFar2lClipboard(StdioCtl &, TStringView, InputState &) noexcept; bool requestFar2lClipboard(StdioCtl &, InputState &) noexcept; -void waitFar2lPing(EventSource &, InputState &) noexcept; - } // namespace tvision #endif // TVISION_FAR2L_H diff --git a/include/tvision/internal/ncursinp.h b/include/tvision/internal/ncursinp.h index 4658d01f..740d6bc3 100644 --- a/include/tvision/internal/ncursinp.h +++ b/include/tvision/internal/ncursinp.h @@ -10,28 +10,37 @@ #define Uses_TEvent #include +#include + namespace tvision { class NcursesDisplay; -struct InputState; + +struct NcursesInputGetter final : public InputGetter +{ + size_t pendingCount {0}; + + int get() noexcept override; + void unget(int k) noexcept override; +}; class NcursesInput : public InputStrategy { enum : char { KEY_ESC = '\x1B' }; - enum { readTimeout = 10 }; + enum { readTimeoutMs = 10 }; StdioCtl &io; InputState &state; bool mouseEnabled; + NcursesInputGetter in; - static int getch_nb() noexcept; + int getChNb() noexcept; void detectAlt(int keys[4], bool &Alt) noexcept; void parsePrintableChar(TEvent &ev, int keys[4], int &num_keys) noexcept; void readUtf8Char(int keys[4], int &num_keys) noexcept; bool parseCursesMouse(TEvent&) noexcept; - void consumeUnprocessedInput() noexcept; public: @@ -39,9 +48,9 @@ class NcursesInput : public InputStrategy NcursesInput(StdioCtl &io, NcursesDisplay &display, InputState &state, bool mouse) noexcept; ~NcursesInput(); - bool getEvent(TEvent &ev) noexcept; - int getButtonCount() noexcept; - bool hasPendingEvents() noexcept; + bool getEvent(TEvent &ev) noexcept override; + int getButtonCount() noexcept override; + bool hasPendingEvents() noexcept override; }; } // namespace tvision diff --git a/include/tvision/internal/terminal.h b/include/tvision/internal/terminal.h index 0fb24b09..c2c902ec 100644 --- a/include/tvision/internal/terminal.h +++ b/include/tvision/internal/terminal.h @@ -11,7 +11,6 @@ namespace tvision { class StdioCtl; -class EventSource; struct Far2lState { @@ -27,6 +26,7 @@ struct InputState Far2lState far2l; bool hasFullOsc52 {false}; bool bracketedPaste {false}; + bool gotDsrResponse {false}; void (*putPaste)(TStringView) {nullptr}; }; @@ -123,7 +123,7 @@ namespace TermIO void mouseOn(StdioCtl &) noexcept; void mouseOff(StdioCtl &) noexcept; void keyModsOn(StdioCtl &) noexcept; - void keyModsOff(StdioCtl &, EventSource &, InputState &) noexcept; + void keyModsOff(StdioCtl &) noexcept; void normalizeKey(KeyDownEvent &keyDown) noexcept; @@ -141,9 +141,11 @@ namespace TermIO ParseResult parseFixTermKey(const CSIData &csi, TEvent&) noexcept; ParseResult parseDCS(GetChBuf&, InputState&) noexcept; ParseResult parseOSC(GetChBuf&, InputState&) noexcept; + ParseResult parseCPR(const CSIData &csi, InputState&) noexcept; ParseResult parseWin32InputModeKeyOrEscapeSeq(const CSIData &, InputGetter&, TEvent&, InputState&) noexcept; char *readUntilBelOrSt(GetChBuf &) noexcept; + void consumeUnprocessedInput(StdioCtl &, InputGetter &, InputState &) noexcept; } } // namespace tvision diff --git a/source/platform/far2l.cpp b/source/platform/far2l.cpp index a57f0b0d..8b1a6f3b 100644 --- a/source/platform/far2l.cpp +++ b/source/platform/far2l.cpp @@ -6,10 +6,6 @@ #include #include #include -#include - -using std::chrono::milliseconds; -using std::chrono::steady_clock; #include @@ -19,7 +15,6 @@ namespace tvision // Request IDs const char f2lNoAnswer = '\0', - f2lPing = '\x04', f2lClipGetData = '\xA0'; static char f2lClientIdData[32 + 1]; @@ -157,12 +152,6 @@ ParseResult parseFar2lAnswer(GetChBuf &buf, TEvent &ev, InputState &state) noexc state.putPaste(text); } } - else if (decoded.size() > 0 && decoded.back() == f2lPing) - { - ev.what = evNothing; - ev.message.infoPtr = &state.far2l; - res = Accepted; - } free(pDecoded); } free(s); @@ -295,19 +284,4 @@ bool requestFar2lClipboard(StdioCtl &io, InputState &state) noexcept return false; } -void waitFar2lPing(EventSource &source, InputState &state) noexcept -{ - if (state.far2l.enabled) - { - TEvent ev {}; - auto begin = steady_clock::now(); - do - { - source.getEvent(ev); - } - while ( (ev.what != evNothing || ev.message.infoPtr != &state.far2l) && - steady_clock::now() - begin <= milliseconds(pingTimeout) ); - } -} - } // namespace tvision diff --git a/source/platform/ncursinp.cpp b/source/platform/ncursinp.cpp index 99b07487..5547f280 100644 --- a/source/platform/ncursinp.cpp +++ b/source/platform/ncursinp.cpp @@ -7,7 +7,6 @@ #include #include -#include #include #include #include @@ -256,19 +255,19 @@ static const auto fromCursesHighKey = { "kc2", {{kbDown}, 0} }, }); -static class NcursesInputGetter : public InputGetter +int NcursesInputGetter::get() noexcept { - int get() noexcept override - { - int k = wgetch(stdscr); - return k != ERR ? k : -1; - } + int k = wgetch(stdscr); + if (pendingCount > 0) + --pendingCount; + return k != ERR ? k : -1; +} - void unget(int k) noexcept override - { - ungetch(k); - } -} ncInputGetter; +void NcursesInputGetter::unget(int k) noexcept +{ + if (ungetch(k) != ERR) + ++pendingCount; +} NcursesInput::NcursesInput( StdioCtl &aIo, NcursesDisplay &, InputState &aState, bool mouse ) noexcept : @@ -288,7 +287,7 @@ NcursesInput::NcursesInput( StdioCtl &aIo, NcursesDisplay &, // Make getch practically non-blocking. Some terminals may feed input slowly. // Note that we only risk blocking when reading multibyte characters // or parsing escape sequences. - wtimeout(stdscr, readTimeout); + wtimeout(stdscr, readTimeoutMs); /* Do not delay too much on ESC key presses, as the Alt modifier works well * in most modern terminals. Still, this delay helps ncurses distinguish * special key sequences, I believe. */ @@ -303,8 +302,8 @@ NcursesInput::~NcursesInput() { if (mouseEnabled) TermIO::mouseOff(io); - TermIO::keyModsOff(io, *this, state); - consumeUnprocessedInput(); + TermIO::keyModsOff(io); + TermIO::consumeUnprocessedInput(io, in, state); } int NcursesInput::getButtonCount() noexcept @@ -314,28 +313,22 @@ int NcursesInput::getButtonCount() noexcept return mouseEnabled ? 2 : 0; } -int NcursesInput::getch_nb() noexcept +int NcursesInput::getChNb() noexcept { wtimeout(stdscr, 0); - int k = wgetch(stdscr); - wtimeout(stdscr, readTimeout); + int k = in.get(); + wtimeout(stdscr, readTimeoutMs); return k; } bool NcursesInput::hasPendingEvents() noexcept { - int k = getch_nb(); - if (k != ERR) - { - ungetch(k); - return true; - } - return false; + return in.pendingCount > 0; } bool NcursesInput::getEvent(TEvent &ev) noexcept { - GetChBuf buf(ncInputGetter); + GetChBuf buf(in); switch (TermIO::parseEvent(buf, ev, state)) { case Rejected: buf.reject(); break; @@ -343,7 +336,7 @@ bool NcursesInput::getEvent(TEvent &ev) noexcept case Ignored: return false; } - int k = wgetch(stdscr); + int k = in.get(); if (k == KEY_RESIZE) return false; // Handled by SigwinchHandler. @@ -394,7 +387,7 @@ void NcursesInput::detectAlt(int keys[4], bool &Alt) noexcept * we check if another character has been received. If it has, we consider this * an Alt+Key combination. Of course, many other things sent by the terminal * begin with ESC, but ncurses already identifies most of them. */ - int k = getch_nb(); + int k = getChNb(); if (k != ERR) { keys[0] = k; @@ -423,7 +416,7 @@ void NcursesInput::readUtf8Char(int keys[4], int &num_keys) noexcept * have to predict the number of bytes it is composed of, then read as many. */ num_keys += Utf8BytesLeft((char) keys[0]); for (int i = 1; i < num_keys; ++i) - if ( ERR == (keys[i] = wgetch(stdscr)) ) + if ((keys[i] = in.get()) == -1) { num_keys = i; break; @@ -472,7 +465,7 @@ bool NcursesInput::parseCursesMouse(TEvent &ev) noexcept for (auto &parseMouse : {TermIO::parseSGRMouse, TermIO::parseX10Mouse}) { - GetChBuf buf(ncInputGetter); + GetChBuf buf(in); switch (parseMouse(buf, ev, state)) { case Rejected: buf.reject(); break; @@ -484,20 +477,6 @@ bool NcursesInput::parseCursesMouse(TEvent &ev) noexcept } } -void NcursesInput::consumeUnprocessedInput() noexcept -{ - // This may be useful if the terminal has sent us events right before we - // disabled keyboard and mouse extensions, or if we have been killed by - // a signal. - TEvent ev; - wtimeout(stdscr, 0); - auto begin = steady_clock::now(); - while ( getEvent(ev) && - steady_clock::now() - begin <= milliseconds(readTimeout) ) - ; - wtimeout(stdscr, readTimeout); -} - } // namespace tvision #endif // HAVE_NCURSES diff --git a/source/platform/terminal.cpp b/source/platform/terminal.cpp index 7e089c82..e95c15e6 100644 --- a/source/platform/terminal.cpp +++ b/source/platform/terminal.cpp @@ -12,6 +12,8 @@ #include #include +#include + namespace tvision { @@ -305,10 +307,9 @@ void TermIO::keyModsOn(StdioCtl &io) noexcept } } -void TermIO::keyModsOff(StdioCtl &io, EventSource &source, InputState &state) noexcept +void TermIO::keyModsOff(StdioCtl &io) noexcept { - TStringView seq = far2lPingSeq - far2lDisableSeq + TStringView seq = far2lDisableSeq "\x1B[?9001l" // Disable win32-input-mode (Conpty). "\x1B[4m" // Reset modifyOtherKeys (XTerm). @@ -317,10 +318,6 @@ void TermIO::keyModsOff(StdioCtl &io, EventSource &source, InputState &state) no "\x1B[?1036r" // Restore metaSendsEscape (XTerm). ; io.write(seq.data(), seq.size()); - // If we are running across a slow connection, it is highly likely that - // far2l will send us keyUp or mouse events before extensions get disabled. - // Therefore, discard events until we get a ping response. - waitFar2lPing(source, state); } void TermIO::normalizeKey(KeyDownEvent &keyDown) noexcept @@ -386,6 +383,8 @@ ParseResult TermIO::parseEscapeSeq(GetChBuf &buf, TEvent &ev, InputState &state) { case 'u': return parseFixTermKey(csi, ev); + case 'R': + return parseCPR(csi, state); case '_': return parseWin32InputModeKeyOrEscapeSeq(csi, buf.in, ev, state); default: @@ -650,7 +649,6 @@ ParseResult TermIO::parseFixTermKey(const CSIData &csi, TEvent &ev) noexcept // https://sw.kovidgoyal.net/kitty/keyboard-protocol.html // http://www.leonerd.org.uk/hacks/fixterms/ { - if (csi.length < 1 || csi.terminator != 'u') return Rejected; @@ -710,12 +708,21 @@ ParseResult TermIO::parseOSC(GetChBuf &buf, InputState &state) noexcept return Ignored; } -static ParseResult parseWin32InputModeKey(const CSIData &csi, TEvent &ev, InputState &state) noexcept -// https://github.com/microsoft/terminal/blob/main/doc/specs/%234999%20-%20Improved%20keyboard%20handling%20in%20Conpty.md +ParseResult TermIO::parseCPR(const CSIData &csi, InputState &state) noexcept +// Pre: csi.terminator == 'R'. +// We receive a Cursor Position Report as response to the Device Status Report +// request we make in 'consumeUnprocessedInput()'. { - if (csi.terminator != '_') + if (csi.length != 2) return Rejected; + state.gotDsrResponse = true; + return Ignored; +} + +static ParseResult parseWin32InputModeKey(const CSIData &csi, TEvent &ev, InputState &state) noexcept +// https://github.com/microsoft/terminal/blob/main/doc/specs/%234999%20-%20Improved%20keyboard%20handling%20in%20Conpty.md +{ KEY_EVENT_RECORD kev; kev.wVirtualKeyCode = (ushort) csi.getValue(0, 0); kev.wVirtualScanCode = (ushort) csi.getValue(1, 0); @@ -784,6 +791,7 @@ class Win32InputModeUnwrapper : public InputGetter }; ParseResult TermIO::parseWin32InputModeKeyOrEscapeSeq(const CSIData &csi, InputGetter &in, TEvent &ev, InputState &state) noexcept +// Pre: csi.terminator == '_'. { ParseResult res = parseWin32InputModeKey(csi, ev, state); if (res == Accepted && ev.keyDown == 0x001B) @@ -877,4 +885,31 @@ char *TermIO::readUntilBelOrSt(GetChBuf &buf) noexcept return {}; } +void TermIO::consumeUnprocessedInput(StdioCtl &io, InputGetter &in, InputState &state) noexcept +// The terminal might have kept sending us events while the application is +// exiting. This is especially likely to happen when the application is running +// remotely accross a slow connection and terminal extensions are in place +// which report key release events (e.g. far2l and win32-input-mode), or when +// the application gets killed by a signal while the user was dragging the mouse. +// Therefore, we print a DSR request and attempt to read events until we get a +// response to it. This has to be done after disabling keyboard and mouse extensions. +{ + using namespace std::chrono; + auto timeout = milliseconds(200); + + TStringView seq = "\x1B[6n"; // Device Status Report. + io.write(seq.data(), seq.size()); + + TEvent ev {}; + state.gotDsrResponse = false; + auto begin = steady_clock::now(); + do + { + GetChBuf buf {in}; + parseEvent(buf, ev, state); + } + while ( !state.gotDsrResponse && + (steady_clock::now() - begin <= timeout) ); +} + } // namespace tvision