Skip to content

Commit 7b20019

Browse files
Merge pull request #1887 from contour-terminal/fix/1861
Remove empty-string terminfo capabilities that cause input to be swallowed
2 parents c3108bf + d74e6dc commit 7b20019

File tree

3 files changed

+86
-9
lines changed

3 files changed

+86
-9
lines changed

metainfo.xml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,7 @@
107107
<release version="0.6.3" urgency="medium" type="development">
108108
<description>
109109
<ul>
110+
<li>Fixes keyboard input being swallowed in programs like less and bat due to empty-string terminfo capabilities (#1861)</li>
110111
<li>Fixes F3 key not reaching PTY applications when a keybinding matches but its action does not execute, by falling through to the terminal instead of silently consuming the key event</li>
111112
<li>Fixes F3/Shift+F3 search navigation keybindings firing in any mode instead of only when Search mode is active</li>
112113
<li>Fixes build failure on Alpine Linux (musl libc) due to missing close_range() function (#1879)</li>

src/vtbackend/Capabilities.cpp

Lines changed: 1 addition & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -274,10 +274,6 @@ namespace
274274
String { Undefined, "kUP5"sv, "\033[1;5A"sv },
275275
String { Undefined, "kUP6"sv, "\033[1;6A"sv },
276276
String { Undefined, "kUP7"sv, "\033[1;7A"sv },
277-
String { "K1"_tcap, "ka1"sv, ""sv }, // upper left of keypad
278-
String { "K3"_tcap, "ka3"sv, ""sv }, // upper right of keypad
279-
String { "K4"_tcap, "kc1"sv, ""sv }, // center of keypad
280-
String { "K5"_tcap, "kc3"sv, ""sv }, // lower right of keypad
281277
String { "kl"_tcap, "kcub1"sv, "\033OD"sv }, // app: cursor left
282278
String { "kd"_tcap, "kcud1"sv, "\033OB"sv }, // app: cursor left
283279
String { "kr"_tcap, "kcuf1"sv, "\033OC"sv }, // app: cursor right
@@ -347,14 +343,11 @@ namespace
347343
String { "k7"_tcap, "kf7"sv, "\033[18~"sv },
348344
String { "k8"_tcap, "kf8"sv, "\033[19~"sv },
349345
String { "k9"_tcap, "kf9"sv, "\033[20~"sv },
350-
String { "%1"_tcap, "khlp"sv, ""sv },
351346
String { "kh"_tcap, "khome"sv, "\033OH"sv },
352347
String { "kI"_tcap, "kich1"sv, "\033[2~"sv },
353348
String { "Km"_tcap, "kmous"sv, "\033[M"sv },
354349
String { "kN"_tcap, "knp"sv, "\033[6~"sv },
355350
String { "kP"_tcap, "kpp"sv, "\033[5~"sv },
356-
String { "&8"_tcap, "kund"sv, ""sv },
357-
358351
// {{{ Extensions originally introduced by tmux.
359352
//
360353
String { "Ss"_tcap, "Ss"sv, "\033[%p1%d q" }, // Set cursor style.
@@ -492,7 +485,7 @@ string StaticDatabase::terminfo() const
492485
output << " " << cap.name << "#" << cap.value << ",\n";
493486

494487
for (auto const& cap: strings)
495-
if (!cap.name.empty())
488+
if (!cap.name.empty() && !cap.value.empty())
496489
output << " " << cap.name << "=" << crispy::escape(cap.value, crispy::numeric_escape::Octal)
497490
<< ",\n";
498491

src/vtbackend/Capabilities_test.cpp

Lines changed: 84 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,8 @@
55

66
#include <catch2/catch_test_macros.hpp>
77

8-
#include <format>
8+
#include <regex>
9+
#include <string>
910

1011
using namespace std::string_view_literals;
1112
using crispy::fromHexString;
@@ -32,3 +33,85 @@ TEST_CASE("Capabilities.get")
3233
auto const bce = tcap.numericCapability("bce");
3334
REQUIRE(bce);
3435
}
36+
37+
// Issue #1861: Empty string terminfo capabilities cause input to be swallowed in programs
38+
// like less and bat. When capabilities are defined as empty strings (e.g., "ka1=,"),
39+
// buggy parsers match any input against them.
40+
TEST_CASE("Capabilities.terminfo_no_empty_string_values", "[issue-1861]")
41+
{
42+
vtbackend::capabilities::StaticDatabase const tcap;
43+
auto const terminfo = tcap.terminfo();
44+
45+
// The regex matches sequences like "\n name=,\n" where the value after '=' is empty.
46+
// In terminfo format, "name=value," defines a string capability.
47+
// "name=,\n" means the value is an empty string — this must never appear.
48+
// Note: We avoid std::regex::multiline because MSVC does not support it.
49+
auto const emptyValuePattern = std::regex(R"(\n\s+(\w+)=,\n)");
50+
51+
auto begin = std::sregex_iterator(terminfo.begin(), terminfo.end(), emptyValuePattern);
52+
auto end = std::sregex_iterator();
53+
54+
std::string emptyCapNames;
55+
for (auto it = begin; it != end; ++it)
56+
{
57+
if (!emptyCapNames.empty())
58+
emptyCapNames += ", ";
59+
emptyCapNames += (*it)[1].str();
60+
}
61+
62+
INFO("Capabilities with empty string values: " << emptyCapNames);
63+
CHECK(emptyCapNames.empty());
64+
}
65+
66+
// Verify that previously-empty keypad capabilities (ka1, ka3, kc1, kc3) are no longer
67+
// present in the terminfo output at all — they should be omitted, not set to empty.
68+
TEST_CASE("Capabilities.keypad_caps_not_in_terminfo", "[issue-1861]")
69+
{
70+
vtbackend::capabilities::StaticDatabase const tcap;
71+
auto const terminfo = tcap.terminfo();
72+
73+
CHECK(terminfo.find("ka1=") == std::string::npos);
74+
CHECK(terminfo.find("ka3=") == std::string::npos);
75+
CHECK(terminfo.find("kc1=") == std::string::npos);
76+
CHECK(terminfo.find("kc3=") == std::string::npos);
77+
}
78+
79+
// Verify that khlp and kund (which were also empty) are omitted.
80+
TEST_CASE("Capabilities.help_undo_caps_not_in_terminfo", "[issue-1861]")
81+
{
82+
vtbackend::capabilities::StaticDatabase const tcap;
83+
auto const terminfo = tcap.terminfo();
84+
85+
CHECK(terminfo.find("khlp=") == std::string::npos);
86+
CHECK(terminfo.find("kund=") == std::string::npos);
87+
}
88+
89+
// Verify that non-empty string capabilities are still present in terminfo output.
90+
TEST_CASE("Capabilities.non_empty_caps_still_present", "[issue-1861]")
91+
{
92+
vtbackend::capabilities::StaticDatabase const tcap;
93+
auto const terminfo = tcap.terminfo();
94+
95+
// These are well-known capabilities that must still be present.
96+
CHECK(terminfo.find("bold=") != std::string::npos);
97+
CHECK(terminfo.find("clear=") != std::string::npos);
98+
CHECK(terminfo.find("kcub1=") != std::string::npos);
99+
CHECK(terminfo.find("kcud1=") != std::string::npos);
100+
CHECK(terminfo.find("kf1=") != std::string::npos);
101+
CHECK(terminfo.find("smkx=") != std::string::npos);
102+
CHECK(terminfo.find("rmkx=") != std::string::npos);
103+
}
104+
105+
// Verify that the stringCapability() API returns an empty view for removed capabilities,
106+
// confirming they are no longer part of the static database.
107+
TEST_CASE("Capabilities.removed_caps_return_empty_from_api", "[issue-1861]")
108+
{
109+
vtbackend::capabilities::StaticDatabase const tcap;
110+
111+
CHECK(tcap.stringCapability("ka1").empty());
112+
CHECK(tcap.stringCapability("ka3").empty());
113+
CHECK(tcap.stringCapability("kc1").empty());
114+
CHECK(tcap.stringCapability("kc3").empty());
115+
CHECK(tcap.stringCapability("khlp").empty());
116+
CHECK(tcap.stringCapability("kund").empty());
117+
}

0 commit comments

Comments
 (0)