Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions experiments/core/alternate_mode_test.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -144,7 +144,7 @@ static void readers_follow_protocol_transitions(ParseMode mode) {
static void recovery_keeps_private_sequences_separate() {
const std::vector<std::string> ignored{
"\x1b[?31m", "\x1b[?1049;25h", "\x1b[?1049;h", "\x1b[0?1049h",
"\x1b[??1049h", "\x1b[?1049 h", "\x1b[?1049:1h", "\x1b[?25l",
"\x1b[??1049h", "\x1b[?1049 h", "\x1b[?1049:1h", "\x1b[?12l",
std::string("\x1b[?") + std::string(33, '0') + "1049h"};
for (const auto& sequence : ignored) {
Parser parser(2, 8, 1, ParseMode::recovery);
Expand Down Expand Up @@ -181,7 +181,7 @@ static void identical_repeated_text_is_a_new_document(ParseMode mode) {
parser.finish();
}
static void strict_rejects_unsupported_private_modes_atomically() {
for (const auto* input : {"\x1b[?31m", "\x1b[?1049;25h", "\x1b[?1049;1049h", "\x1b[?25l"}) {
for (const auto* input : {"\x1b[?31m", "\x1b[?1049;25h", "\x1b[?1049;1049h", "\x1b[?12l"}) {
Parser parser(2, 8, 1);
parser.feed("\x1b[31;44mMAIN");
bool rejected = false;
Expand Down
12 changes: 12 additions & 0 deletions experiments/core/reply_test.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,18 @@ int main() {
// An unimplemented report must stay unimplemented instead of being faked.
parser.feed("\x1b[5n");
require(parser.take_replies().empty(), "unsupported report produced a reply");
// DECCKM and DECTCEM are state-only modes; strict parsing must accept them.
{
Parser strict_parser(3, 10, 10, ParseMode::strict);
require(strict_parser.cursor_visible() && !strict_parser.application_cursor_keys(),
"default mode bits mismatch");
strict_parser.feed("\x1b[?1h\x1b[?25l");
require(strict_parser.application_cursor_keys() && !strict_parser.cursor_visible(),
"mode bits were not applied");
strict_parser.feed("\x1b[?1l\x1b[?25h");
require(!strict_parser.application_cursor_keys() && strict_parser.cursor_visible(),
"mode bits were not cleared");
}
std::puts("cursor reply contracts passed");
return 0;
}
12 changes: 12 additions & 0 deletions experiments/core/terminal.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,17 @@ class Parser {
std::string encoded;
// Bounded replies for implemented queries; drained by the host after each feed.
std::vector<std::string> pending_replies;
// Bit 0: DECCKM application cursor keys; bit 1: DECTCEM cursor visible.
uint32_t mode_bits_ = 1u << 1;
void dispatch(unsigned char final) {
if (private_csi) {
// DECCKM (?1) and DECTCEM (?25) only change terminal state the host
// reads later; everything else private is still unsupported.
if (params.size() == 1 && (params[0] == 1 || params[0] == 25) && (final == 'h' || final == 'l')) {
const uint32_t bit = params[0] == 1 ? 1u << 0 : 1u << 1;
if (final == 'h') mode_bits_ |= bit; else mode_bits_ &= ~bit;
return;
}
if (params.size() != 1 || (params[0] != 1049 && params[0] != 7) || (final != 'h' && final != 'l'))
throw InputError("unsupported private CSI command");
if (params[0] == 7) {
Expand Down Expand Up @@ -240,6 +249,9 @@ class Parser {
: mode(selected), screen(rows, columns, history) {}
const RecoveryDiagnostics& diagnostics() const { return stats; }
std::vector<std::string> take_replies() { return std::exchange(pending_replies, {}); }
uint32_t mode_bits() const { return mode_bits_; }
bool application_cursor_keys() const { return (mode_bits_ & (1u << 0)) != 0; }
bool cursor_visible() const { return (mode_bits_ & (1u << 1)) != 0; }
size_t buffered_input_bytes() const { return params.size() * sizeof(int) + encoded.size(); }
void feed(std::string_view bytes) {
if (mode == ParseMode::strict) feed_strict(bytes);
Expand Down
3 changes: 2 additions & 1 deletion experiments/core/test_recovery.py
Original file line number Diff line number Diff line change
Expand Up @@ -252,7 +252,8 @@ def test_unterminated_string_consumes_all_until_eof(self):
self.expect(b"A\x1b]hidden\r\nZ", "A", stats={"truncated_sequences": 1})

def test_strict_still_rejects_invalid_input(self):
for data in (b"\x1b7", b"\x1b]title\x07", b"\xff", b"\xe1\x80", b"\x1b[?25h", b"\x1b[31"):
# ?25 is a supported mode now, so the unsupported-private sample uses ?12.
for data in (b"\x1b7", b"\x1b]title\x07", b"\xff", b"\xe1\x80", b"\x1b[?12h", b"\x1b[31"):
result = self.run_input(data, [1], mode="strict")
self.assertEqual(result.returncode, 2)
self.assertEqual(result.stdout, "")
Expand Down
11 changes: 7 additions & 4 deletions experiments/shell/keyboard.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -52,8 +52,8 @@ class KeyboardEncoder {
return out + encoded(unit, alt);
}
// Win32 virtual-key values. Printable/control text stays in WM_CHAR;
// application cursor/keypad modes remain a separate future contract.
static KeyEncoding key_down(uint32_t key, KeyModifiers mods = {}) {
// DECCKM switches the unmodified cursor keys between CSI and SS3 forms.
static KeyEncoding key_down(uint32_t key, KeyModifiers mods = {}, bool application_cursor = false) {
if (mods.alt_graph || (mods.control && mods.alt)) return {};
const int modifier = 1 + (mods.shift ? 1 : 0) + (mods.alt ? 2 : 0) + (mods.control ? 4 : 0);
if (key == 0x09) { // Plain Tab is translated once, by WM_CHAR.
Expand All @@ -71,8 +71,11 @@ class KeyboardEncoder {
case 0x23: final = 'F'; break;
default: break;
}
if (final) return {true, modifier == 1 ? std::string("\x1b[") + final :
std::string("\x1b[1;") + std::to_string(modifier) + final, false};
if (final) {
if (modifier == 1 && application_cursor) return {true, std::string("\x1bO") + final, false};
return {true, modifier == 1 ? std::string("\x1b[") + final :
std::string("\x1b[1;") + std::to_string(modifier) + final, false};
}
int number = 0;
switch (key) {
case 0x2d: number = 2; break; // Insert
Expand Down
15 changes: 15 additions & 0 deletions experiments/shell/keyboard_test.cpp
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
#include "keyboard.hpp"
#include <iostream>
#include <stdexcept>
#include <string>
#include <utility>
#include <vector>
using namespace tf_shell;
static void check(bool value, const char* why) { if (!value) throw std::runtime_error(why); }
int main() {
Expand Down Expand Up @@ -30,6 +33,18 @@ int main() {
const auto encoded = KeyboardEncoder::key_down(pair.first);
check(encoded.handled && encoded.bytes == pair.second && !encoded.skip_translate, "base navigation encoding");
}
// DECCKM only rewrites the unmodified cursor keys to their SS3 form.
const std::vector<std::pair<uint32_t,std::string>> application{
{0x25,"\x1bOD"}, {0x26,"\x1bOA"}, {0x27,"\x1bOC"}, {0x28,"\x1bOB"},
{0x24,"\x1bOH"}, {0x23,"\x1bOF"}};
for (const auto& entry : application) {
const auto encoded = KeyboardEncoder::key_down(entry.first, {}, true);
check(encoded.handled && encoded.bytes == entry.second, "application cursor encoding");
}
check(KeyboardEncoder::key_down(0x24, {false,true,false,false}, true).bytes == "\x1b[1;5H",
"application mode must not rewrite modified cursor keys");
check(KeyboardEncoder::key_down(0x2e, {}, true).bytes == "\x1b[3~",
"application mode must not rewrite editing keys");
check(KeyboardEncoder::key_down(0x24, {false,true,false,false}).bytes == "\x1b[1;5H", "Ctrl Home");
check(KeyboardEncoder::key_down(0x2e, {true,false,true,false}).bytes == "\x1b[3;4~", "Shift Alt Delete");
check(!KeyboardEncoder::key_down(0x09).handled, "plain Tab belongs only to WM_CHAR");
Expand Down
10 changes: 9 additions & 1 deletion experiments/shell/shell.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -827,6 +827,13 @@ class ShellWindow {
}
}
}
#endif
#ifdef TERMFORGE_CANDIDATE_CORE
// The candidate terminal does not expose DECCKM yet; keep the previous CSI
// encoding instead of guessing at its mode state.
bool application_cursor_keys() const { return false; }
#else
bool application_cursor_keys() const { return parser_.application_cursor_keys(); }
#endif
void tick() {
const auto began = Clock::now(); ++result_.ticks;
Expand Down Expand Up @@ -1006,7 +1013,8 @@ class ShellWindow {
if(!mods.control && !mods.alt && wp==VK_F3) {
self->navigate_find(mods.shift?termforge::SearchDirection::previous:termforge::SearchDirection::next); return 0;
}
const auto key=tf_shell::KeyboardEncoder::key_down(static_cast<uint32_t>(wp),mods);
const auto key=tf_shell::KeyboardEncoder::key_down(static_cast<uint32_t>(wp),mods,
self->application_cursor_keys());
if(key.handled) { self->send_input(key.bytes); return 0; }
}
if (message == WM_TIMER && (self->started_ || self->closing_)) { self->tick(); return 0; }
Expand Down